以下基础题目,需要在好好表述下,就把对方想象成一个不怎么懂前端的小伙伴,你去面对面的跟她聊,语言的组织和表达尤为重要
- http缓存
- webpack打包编译过程
- 闭包的理解和实践
- 异步编程解决方案(Promise/Generator/Async和await)以及各自的优缺点
- var/const/let横向对比
- H5的新特新都有哪些
- vue3.0中v-if和v-for哪个优先级高
- http中的长连接和短连接keep-alive
- websocket持久化链接
- url输入到显示经历了什么
- vuex是基于什么模式
- 发布订阅模式跟观察者模式区别
下面开始手写答案,尽量精简切中要害
websocket
一种持久化的网络通信协议,属于应用层,基于TCP传输,复用Http握手通道,是一种全双工通信,最大特点就是服务器阔以主动向客户端发现信息,客户端也阔以向服务端主动发送信息,是真正的双向平等对话,属于服务器推送技术的一种。有三个优势
- 支持双向通信,实时性更强
- 更好的二进制支持
- 较少的控制开销,ws客户端和服务端进行数据交换时,协议控制的数据包头部较少
其他特点包括:
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
http中的长连接和短连接
在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话
而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:
1 | connection: keep-alive |
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。
HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。
url输入到显示经历的过程
- 是否命中本地强缓存或者协商缓存
- DNS解析
- 建立TCP链接,三次握手
- 服务器处理请求并返回Http报文
- 浏览器解析渲染页面
- 断开TCP链接,四次挥手
浏览器解析渲染页面
- 根据HTML解析DOM树
- 根据CSS解析生成CSS规则树
- 结合DOM树和CSS树生成Render Tree
- 根据渲染树计算每一个节点信息(Paint)
- 根据计算好的信息绘制页面(Display)
reflow回流和repaint重绘
rsflow 回流
当render Tree中的部分或者全部元素的尺寸、结构、位置发生改变的时候,浏览器重新渲染部分或者全部文档的过程称为回流1
2
3
4
5
6
7
8- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的DOM元素
- 激活CSS伪类(例如::hover)
- 查询某些属性或调用某些方法一些常规导致回流的属性和方法
1
2
3
4
5
6
7clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()repaint 重绘
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
异步编程解决方案(Promise/Generator/Async和await)以及各自的优缺点
难以启齿,不知怎么描述
回调函数callBack
回调函数是异步操作最基本的方法
1 | this.$ajax(url, () => {}) |
回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:
1 | this.$ajax(url, () => { |
回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。此外它不能使用 try catch 捕获错误,不能直接 return。
Promise
Promise本意是承诺,承诺一段时间后会给你结果,一段时间就是指异步操作,一般指比较长的时间才有结果,比如网络请求,读取文件
三种状态
- Pending—-Promise对象实例创建时候的初始状态
- fulfilled—-可以理解为成功的状态
- Rejected—-可以理解为失败的状态
状态一旦改变就不能再次更改状态
链式调用
- 每次调用返回的都是一个新的Promise实例(这就是then可用链式调用的原因)
- 如果then中返回的是一个结果的话会把这个结果传递下一次then中的成功回调
- 如果then中出现异常,会走下一个then的失败回调
Promise不仅能够捕获错误,而且也很好地解决了回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:
1 | this.$ajax(url) |
缺点
无法取消 Promise,错误需要通过回调函数捕获
Generator/yield
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,Generator 最大的特点就是可以控制函数的执行。(ES6协程的一种实现)
- 语法上,把他理解成Generator是一个状态机,里面封装了多个状态
- 除了状态机,还是一个遍历器生成对象
- 利用yield关键字阔以暂停函数的执行,每次返回yield后面表达式的值
- yield本身没有返回值或者说总是返回undefined,next方法阔以带一个参数,该参数会被当做上一个yield表达式返回的值(好难理解,做题巩固下)
1 | function *foo(x) { |
- 首先 Generator 函数调用和普通函数不同,它会返回一个迭代器
- 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
- 当执行第二次 next 时,传入的参数12就会被当作上一个yield表达式的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
- 当执行第三次 next 时,传入的参数13就会被当作上一个yield表达式的返回值,所以 z = 13, x = 5, y = 24,相加等于 42
使用
Generator 函数会返回一个遍历器对象,即具有Symbol.iterator属性,并且返回给自己
1 | function* gen(){} |
阔以通过 yield 关键字可以暂停generator函数返回的遍历器对象的状态
通过next方法才会遍历到下一个内部状态,其运行逻辑如下:
- 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
- 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式
- 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
- 如果该函数没有return语句,则返回的对象的value属性值为undefined
使用场景
- Generator、async需要与promise对象搭配处理异步情况
- async实质是Generator的语法糖,相当于会自动执行Generator函数
async/await
- 基于Promise实现,不能用于普通的回调函数
- 与Promise一样,是非阻塞的
- 将异步代码以同步的形式进行编写,是处理异步编程的最终方案
一个函数如果加上 async ,那么该函数就会返回一个 Promise
所有的 async 函数的返回值都是 Promise。
1 | async function test() {return '1'} |
async/await 函数优势
实际上是Generator函数的语法糖,就是将 Generator 函数和自动执行器,包装在一个函数里
我们都知道,Promise解决了回调地狱的问题,但是如果遇到复杂的业务,代码里面会包含大量的 then 函数,使得代码依然不是太容易阅读。
基于这个原因,ES7 引入了 async/await,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰,而且还支持 try-catch 来捕获异常,非常符合人的线性思维
async/await函数相对于Promise,优势体现在:
- 处理 then 的调用链,能够更清晰准确的写出代码 代码可维护性
- 能优雅解决回调地狱问题
async/await函数的缺点
await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式(请求之间有依赖性最好用async/await方式)
找了半天也没看到合理的说到async/await的缺点
async/await对Generator函数的改进
- 内置执行器, Generator 函数的执行必须依靠执行器,而 async 函数自带执行器,无需手动执行 next() 方法
- 更广的适用性( async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)
- 更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
- 返回值是promise async函数返回值是Promise对象,比Generator函数返回的Iterator对象方便,阔以直接使用then方法进行调用
async/await是这样
1 | async function fn(args) {} |
生成器函数则对应是这样
1 | function fn(args) { |
babel中对async/await中的转义就是的
Generator函数暂停恢复执行原理
那你首先要了解协程的概念。
一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。
协程是一种比线程更加轻量级的存在。普通线程是抢先式的,会争夺cpu资源,而协程是合作的,可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。它的运行流程大致如下:
- 协程A开始执行
- 协程A执行到某个阶段,进入暂停,执行权转移到协程B
- 协程B执行完成或暂停,将执行权交还A
- 协程A恢复执行
协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。
async/await执行顺序
我们知道async隐式返回 Promise 作为结果的函数,那么可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出async函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。我们来看个例子:
1 | console.log('script start') |
如果await 后面直接跟的为一个变量,比如:await 1;这种情况的话相当于直接把await后面的代码注册为一个微任务,可以简单理解为promise.then(await下面的代码)。然后跳出async1函数,执行其他代码,当遇到promise函数的时候,会注册promise.then()函数到微任务队列,注意此时微任务队列里面已经存在await后面的微任务。所以这种情况会先执行await后面的代码(async1 end),再执行async1函数后面注册的微任务代码(promise1,promise2)。
如果await后面跟的是一个异步函数的调用,比如上面的代码,将代码改成这样:
1 | console.log('script start') |
JavaScript的异步编写方式,从 回调函数 到 Promise、Generator 再到 Async/Await。表面上只是写法的变化,但本质上则是语言层的一次次抽象。让我们可以用更简单的方式实现同样的功能,而不需要去考虑代码是如何执行的。换句话说就是:异步编程的最高境界,就是根本不用关心它是不是异步。