前言
大家好,我是暗淡蓝点。由于年后打算去面试,因此年前在复习 javascript 知识,以便应对年后的面试。复习过程中,要把复习过的知识点进行总结,方便记忆,同时以文字的形式进行输出,加深理解,如有错误,敬请指正。
Event Loop
event loop,也叫事件循环 / 事件轮询,是异步回调的实现原理,搞懂 event loop,能有效的理解代码的执行顺序。这个知识点是面试过程中经常问到的,因此我复习之后进行总结输出。
javascript 是单线程运行的,异步要基于回调来实现。
在说 event loop 之前,先介绍一下宏任务和微任务。宏任务和微任务都是异步操作。
- 宏任务(浏览器定义):setTimeout、setIntervel、AJAX、DOM 事件;
- 微任务(ES6定义):promise、async / await;
讲了宏任务和微任务的概念之后,接下来将 event loop 的执行。
如上图所示,javascript 代码从上往下执行,先执行完同步代码,在执行异步代码。每执行一行代码时,被执行的代码就会被推入到 call satck 中,执行完成后 call stack 就清除这一行代码。碰到异步代码时,就把异步的回调函数存储到 Web APIS 中,这里说 Web APIS 并不准确,后面再讲。当所有的同步代码执行完成,call stack 清空后,event loop 就开始轮询。当 Web APIS 中的异步代码可以执行时,异步代码就被推入到 Callback Queen中,event loop 轮询后就把 Callback Queen 中的代码一行行推入到 Call Stack 中执行,当 WEB APIS 和 Callback Queen 中的代码都执行完后,event loop 就停止了。
以上图为例,代码先被推入到 call stack 中,运行后再 Browser Console 中打印 Hi,然后执行 setTimeout 行,因为 setTimeout 是异步,所以回调函数 cb1 被推入到 Web APIS 中,等待 2s 后被推入 Callback Queen 中执行。然后最后一行的 console 代码推入到 call stack 中,执行后打印 goodbay,所有的同步代码执行完毕,然后 event loop 开始轮询,由于 Callback Queen 中暂无代码,因此 call stack 中没有代码执行。2s 后,setTimeout 的回调函数 cb1 被推入到 Callback Queen 中,轮询后被推入到 call stack 中执行,然后打印 cb1。
以上就是 event loop 同步和异步的运行过程。接下来介绍 event loop 如何处理宏任务和微任务的。
宏任务
宏任务是由浏览器定义的,包括 setTimeout、setInterval、AJAX、DOM 事件。
微任务
微任务是由 ES6 语法定义的,包括 promise、async / await。微任务要比宏任务先执行。既然说到微任务,就终点介绍一下 promise。
promise
promise 是 ES6 语法退出的异步解决方案,消除了异步回调函数的回调地狱,而是使用链式调用俩执行回调。promise 共用三种状态:pending(初始状态)、resovled(成功)、rejected(失败)。promise 的状态变化时单向的,只能由 pending -> resolved / rejected,且状态不可逆,就是说 pending 状态改变后,就只能维持 resovled / rejected 状态,不能改变成别的状态。
同时,resovled 状态触发后续的 then 回调函数,rejected 状态触发后续的 catch 回调函数。下面来看一下 promise 的程序题。
如上图所示,resovle 函数返回的是 resovled 状态的 promise,因此执行 then 回调,在浏览器输出数字 1,然后throw 抛出异常,返回 rejected 状态的 promsie,执行 catch 回调,在浏览器输出数字2,然后返回 resovled 状态的 promsie,执行 then 回调,浏览器输出数字3。
注意在这里有一个坑,throw new Error(‘error’) 抛出异常,promise 会返回 rejected 状态的 promise,如果在 promsie 中这样返回:return new Error(‘erroe’),那结果又是什么呢?
看到没有,浏览器输出的是1,3,reutrn 返回的错误并没有被 promise 捕获到,这又是为什么呢?因为,then 或 catch 回调函数返回的又是一个 promise,返回的 promise 状态,由前一个函数的状态决定,return 返回的异常,被 promsie 包裹了起来,相当于 promise.resovle(return new Error(‘error1’)),也就是一个 resovled 状态的 promise,因此不会执行 catch 回调。
说完了 promsie,下面就来说一下宏任务和微任务的执行顺序以及原因。
async /await
async / await 是语法糖,用同步的写法来取代回调函数,从语法层面消灭了异步回调。
- 执行 async 函数,返回的是 promsie 对象;
- await 相当于 promise 中的 then 方法,await 中的内容都是回调函数的内容;
- try … catch 可捕获异常,替代了 promise 中的 catch 回调;
还是用代码来讲解,下面我们来一步一步分析代码的执行过程。
首先,函数定义可以先不看,待函数执行时再来看。代码从上到下依次执行:
- promise 构造函数中的函数会同步执行(立即执行),先执行第一个 promise 构造函数;
- 执行 console.log,浏览器输出 3;
- 执行第二个 promise 构造函数,浏览器输出 7,setTimeout 是宏任务,异步函数被推入到 Web APIS 中,等待执行,resovle 函数返回 resovle 状态、值是 1 的 promsie 给 变量 p;
- resovle 函数返回 resovle 状态、值是 2 的 promsie 给变量 first,变量 p 后面的回调函数被存入 micro task queen 中,等待执行,第一个 promise 的构造函数执行完;
- 变量 first 的回调函数通用被存入 micro task queen,等待执行;
- 执行 console.log,浏览器输出 4,同步代码执行完成,event loop 开始轮询;
- 首先执行微任务(micro task queen),微任务中的执行顺序也是先进先出,先执行 p 的回调函数,浏览器输出 1;
- 然后执行 first 的回调函数,浏览器输出 2;
- 微任务执行完,在执行宏任务(setTimeout),浏览器输出 5;
- 剩下的以后 resovle(6),不会被执行,因为 promise 的状态是单向的,已经先执行了 resovle(1),状态 由 pending -> resolved,不会在变化;
- 最后,浏览器输出的就是 3,7,4,1,2,5。
再来看一段代码,解释其执行顺序,然后解释微任务比宏任务先执行的原因。
定义函数的代码可以先不看,等函数执行时再看。
- 执行 console.log,浏览器输出 ‘script start’;
- setTimeout 是宏任务,回调函数推入到 Web APIS 中;
- 执行 async1 函数,先执行 console.log,浏览器输出 ‘async1 start’;
- 再执行 async2 函数,console.log 浏览器输出 ‘async2’;
- await 后面的代码相当于回调函数,推入 micro task queen,等待执行,async1 函数执行完;
- 执行第一个 promise 构造函数,浏览器输出 ‘function’;
- 执行第二个 promise 构造函数,浏览器输出 ‘promise1’,resovle 函数返回 resovled 状态的 promise,then 回调函数推入到 micro task queen,等待执行;
- 执行 console.log,浏览器输出 ‘script end’,同步代码执行完毕;
- event loop 开始轮询,首先是微任务(micro task queen),执行 await 后的代码,浏览器输出 ‘async1 end’;
- 然后执行 promsie 的 then 回调函数,浏览器输出 ‘promise2’,微任务执行完;
- 执行宏任务 setTimeout 的回调函数,浏览器输出 ‘setTimeout’;
- 浏览器最后输出 ‘script start’、’async1 start’、’async2’、’function’,’promise1’,’script end’,’async1 end’,’promsie2’,’setTimeout’;
微任务比宏任务先执行
讲完了 promise 和 async / awiat,下面来讲讲为什么微任务比宏任务先执行执行。首先要说的还是 event loop 的轮询机制。
如上图,先一行一行的执行同步代码,同步代码执行完成后,call stack 就清空了。然后开始执行异步任务,首先执行当前的微任务,微任务(micro task queue)中的代码被推入到 Callback Quue 中,call stack 开始一行行的执行微任务,微任务执行完成后,浏览器开始尝试 DOM 渲染,如果有 DOM 节点需要更新,就进行 DOM 渲染,没有则跳过。接下来开始执行 event loop,再执行宏任务。
也就是说,微任务比宏任务先执行,DOM 渲染也比宏任务先执行。原理讲完了,接下来上代码演示。
- js 代码从上到下依次执行,先执行前面 4 行代码,之后 div 元素下面有 3 个子元素;
- promise 的回调是微任务,存储到 micro task queue 中,等待执行;
- setTimeout 的回调函数式宏任务,推入到 Web APIS 中,等待执行;
- 同步代码执行完毕,call stack 被清空,首先执行micro task queue 中的代码;
- promise 的回调函数一行行被推入 call stack 中,因为此时 div 中已经有 3 个子元素 p 标签,因为浏览器输出为3,alert 弹出 ‘Promise then’,然后代码被阻断执行,此时页面上为加载出 div 的 3 个子元素 p 标签(也就是 DOM 为渲染);
- 微任务执行完,DOM 开始渲染,因为 div 元素有更新,执行 DOM 渲染,DOM 渲染完后,用户才能在浏览器上看到改变的元素;
- DOM 渲染完,event loop 执行轮询,Web APIS 中 setTimeout 的回调被推入到 callback queue 中执行,div 元素的子元素有 3 个 p 标签,因此浏览器输出为3,alert 弹出 ‘setTimeout’,代码被组单,此时 DOM 渲染完成,更新的 DOM 元素用户才看得到;
event loop 讲完了,宏任务、微任务、DOM 渲染的执行顺序和执行过程都讲完了。整理的 event loop 学习笔记也清楚了,下一篇讲讲浏览器的缓存机制。