Event Loop 我感觉我又行了(浏览器篇)

前端开发
2022年07月09日
1689

我们先看看事件循环是怎么回事:

image-20220519140516690

名词解释:

  • 执行栈:所有代码都会放到这里执行;
  • 微任务:语言标准(ECMA262)提供的API运行,Promise、MutationObserve、process.nextTick、setImmediate;
  • GUI渲染:渲染DOM;
  • 宏任务:宿主提供的异步方法和任务,setTimeout、setInterval、script、UI渲染、ajax、DOM的事件;

举个粟子

我们直接通过分析各种例子,来了解浏览器的 Event Loop 的过程。

例子1

html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Event Loop</title> </head> <body> <div id="J_wrapper" style="width: 200px; height: 30px; background: orange"></div> <script src="./1.js"></script> </body> </html>

1.js

js
const oWrapper = document.querySelector('#J_wrapper') console.log('start') oWrapper.style.background = 'blue' Promise.resolve().then(() => { console.log(1) for (let i = 1; i < 10000000000; i++) { let a = 1 } }) setTimeout(() => { console.log(2) }, 0) console.log('end')

我们先不运行代码,结合事件循环图来分析一下流程:

例子1

  1. 因为 script 是在后面引入,DOM 肯定会先渲染,所以页面上应该呈现一个橙色的 div;

  2. script 执行;

    <执行栈>:

    <微任务>:

    <GUI渲染>:

    <宏任务>:

  3. const oWrapper = document.querySelector('#J_wrapper') 语句执行;

  4. console.log('start') 语句执行,输出 start;

    <执行栈>:

    start

    <微任务>:

    <GUI渲染>:

    <宏任务>:

  5. oWrapper.style.background = 'blue' 语句被放入 GUI 渲染线程;

    <执行栈>:

    start

    <微任务>:

    <GUI渲染>:

    oWrapper.style.background = ‘blue’

    <宏任务>:

  6. Promise.resolve.then 语句执行,回调被放入微任务队列;

    <执行栈>:

    start

    <微任务>:

    Promise.resolve.then 的回调

    <GUI渲染>:

    oWrapper.style.background = ‘blue’

    <宏任务>:

  7. setTimeout 语句执行,回调被放入宏任务队列;

    <执行栈>:

    start

    <微任务>:

    Promise.resolve.then 的回调

    <GUI渲染>:

    oWrapper.style.background = ‘blue’

    <宏任务>:

    setTimeout 的回调

  8. console.log(end) 语句执行,输出 end;

    <执行栈>:

    start

    end

    <微任务>:

    Promise.resolve.then 的回调

    <GUI渲染>:

    oWrapper.style.background = ‘blue’

    <宏任务>:

    setTimeout 的回调

  9. 清空微任务(本轮所有的微任务回调被放入执行栈执行);

    <执行栈>:

    start

    end

    1

    <微任务>:

    Promise.resolve.then 的回调

    <GUI渲染>:

    oWrapper.style.background = ‘blue’

    <宏任务>:

    setTimeout 的回调

  10. GUI渲染,页面显示 div 背景色变成 blue;

    <执行栈>:

    start

    end

    1

    <微任务>:

    Promise.resolve.then 的回调

    <GUI渲染>:

    oWrapper.style.background = ‘blue’

    <宏任务>:

    setTimeout 的回调

  11. 取一个宏任务回调放入执行栈执行(前提是等待时间已到);

    <执行栈>:

    start

    end

    1

    2

    <微任务>:

    Promise.resolve.then 的回调

    <GUI渲染>:

    oWrapper.style.background = ‘blue’

    <宏任务>:

    setTimeout 的回调

  12. 完成一轮循环,已无其它任务,完事。

最终我们可以看到浏览器中输出的结果:

markdown
start end 1 2

并且可以看到在输出1之后,会有一段等待 for 循环执行过程,之后才会把 div 的颜色变成蓝色。

例子2

js
console.log(1); // setTimeout1 setTimeout(() => { console.log(2) // Promise1.then Promise.resolve().then(() => { console.log(3) }) }, 0); // Promise2 new Promise((resolve, reject) => { console.log(4) // setTimeout2 setTimeout(() => { console.log(5) resolve(6); }, 0); // Promise2.then1 }).then((res) => { console.log(7); // setTimeout3 setTimeout(() => { console.log(res) }, 0); // Promise2.then2 }).then(() => { console.log(8); // Promise2.then3 }).then(() => { console.log(9); });

同样,我们还是先不运行代码,为了方便区分,上面代码加上了一些注释标识,对照着 Event Loop 图示来分析一下:

  1. script 执行

    <执行栈>:

    <微任务>:

    <GUI渲染>:

    <宏任务>:

  2. console.log(1) 放入执行栈,输出 1;

    <执行栈>:

    1

    <微任务>:

    <GUI渲染>:

    <宏任务>:

  3. setTimeout1 放入执行栈,回调被放入宏任务队列;

    <执行栈>:

    1

    <微任务>:

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

  4. Promise2 放入执行栈,Executor 中的代码是同步执行的,所以 console.log(4) 会执行,输出4,之后 setTimeout2 执行,回调被放入宏任务队列;

    <执行栈>:

    1

    4

    <微任务>:

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  5. 因为 Promise2 中的状态没变成 Fulfilled,所以 then 还不会被执行;

  6. 执行栈空闲,微任务为空,GUI渲染为空,取一个宏任务 setTimeout1 的回调放入执行栈,输出2,之后 Promise1.then 回调被放入微任务队列;

    <执行栈>:

    1

    4

    2

    <微任务>:

    Promise1.then 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  7. 继续下一轮,取所有微任务回调放入执行栈,console.log(3) 执行,输出3;

    <执行栈>:

    1

    4

    2

    3

    <微任务>:

    Promise1.then 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  8. GUI渲染为空,取宏任务 setTimeout2 的回调执行,console.log(5) 执行,输出5,之后 Promise2 的状态变成 Fulfilled,Promise2.then1 的回调被放入微任务队列;

    <执行栈>:

    1

    4

    2

    3

    5

    <微任务>:

    Promise1.then 的回调

    Promise2.then1 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  9. 继续下一轮,清空当前所有微任务;Promise2.then1 中的 console.log(7) 执行,输出7;之后 setTimeout3 中的回调被放入宏任务队列;Promise2.then2 的回调放入微任务队列;

    <执行栈>:

    1

    4

    2

    3

    5

    7

    <微任务>:

    Promise1.then 的回调

    Promise2.then1 的回调

    Promise2.then2 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

    setTimeout3 的回调

  10. 继续执行 Promise2.then2 的回调console.log(8) ,输出8;Promise2.then3 的回调被放入微任务队列;

    <执行栈>:

    1

    4

    2

    3

    5

    7

    8

    <微任务>:

    Promise1.then 的回调

    Promise2.then1 的回调

    Promise2.then2 的回调

    Promise2.then3 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

    setTimeout3 的回调

  11. 继续执行 Promise2.then3 的回调console.log(9), 输出9;至此,本轮所有微任务被清空完毕;

    <执行栈>:

    1

    4

    2

    3

    5

    7

    8

    9

    <微任务>:

    Promise1.then 的回调

    Promise2.then1 的回调

    Promise2.then2 的回调

    Promise2.then3 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

    setTimeout3 的回调

  12. 没有 GUI渲染,取宏任务 setTimeout3 的回调,console.log(res) 执行,输出6;

    <执行栈>:

    1

    4

    2

    3

    5

    7

    8

    9

    6

    <微任务>:

    Promise1.then 的回调

    Promise2.then1 的回调

    Promise2.then2 的回调

    Promise2.then3 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

    setTimeout3 的回调

  13. 完事。

最终我们可以看到控制台输出如下:

image-20220521152257505

例子3

js
let p = new Promise(resolve => { resolve(1); Promise.resolve().then(() => console.log(2)); console.log(4); }).then(t => console.log(t)); console.log(3);

这个题比较有意思。我们需要知道 Promise 中的 Executor 里面的代码是同步执行的,并且在 resolve 之后的代码也还是会继续执行的,并且只有 then 被调用的时候,回调才会被放入微任务队列。所以我们可以简单地分析一下代码的运行。

  1. Promise 的 Executor 执行,resolve(1)

    <执行栈>:

    <微任务>:

    <GUI渲染>:

    <宏任务>:

  2. Promise.resolve().then 执行,回调被放入微任务队列;之后 console.log(4) 执行,输出4;

    <执行栈>:

    4

    <微任务>:

    Promise.resolve().then 的回调

    <GUI渲染>:

    <宏任务>:

  3. Promise.then 执行,回调 t => console.log(t) 被放入微任务队列;

    <执行栈>:

    4

    <微任务>:

    Promise.resolve().then 的回调

    Promise.then 的回调

    <GUI渲染>:

    <宏任务>:

  4. console.log(3) 执行,输出3;

    <执行栈>:

    4

    3

    <微任务>:

    Promise.resolve().then 的回调

    Promise.then 的回调

    <GUI渲染>:

    <宏任务>:

  5. 取当前轮所有微任务回调执行,Promise.resolve().then 先被执行 console.log(2),输出2;之后 Promise.then 执行 console.log(t) ,输出1;

    <执行栈>:

    4

    3

    2

    1

    <微任务>:

    Promise.resolve().then 的回调

    Promise.then 的回调

    <GUI渲染>:

    <宏任务>:

  6. 完事

image-20220521153537039

这里需要注意到,在 Promise 中,当且仅当状态变更, then 被调用的时候,它的回调才会被放入微任务队列。

例子4

js
// setTimeout1 setTimeout(() => { console.log('A'); }, 0); var obj = { func: function() { // setTimeout2 setTimeout(function() { console.log('B'); }, 0); return new Promise(function(resolve) { console.log('C'); resolve(); }); }, }; obj.func().then(function() { console.log('D'); }); console.log('E');

同样还是先分析一下代码。

  1. script 执行。

    <执行栈>:

    <微任务>:

    <GUI渲染>:

    <宏任务>:

  2. setTimeout1 执行,回调被放入宏任务队列;

    <执行栈>:

    <微任务>:

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

  3. obj.func() 执行,setTimeout2 执行,回调被放入宏任务队列;Promise 的 Executor 执行console.log('C'),输出 C;

    <执行栈>:

    C

    <微任务>:

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  4. obj.func() 执行完后,调用 then, 回调被放入微任务队列;

    <执行栈>:

    C

    <微任务>:

    obj.func().then 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  5. console.log('E') 执行,输出 E;

    <执行栈>:

    C

    E

    <微任务>:

    obj.func().then 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  6. 清空本轮所有微任务,obj.func().then 的回调执行console.log('D'),输出 D;

    <执行栈>:

    C

    E

    D

    <微任务>:

    obj.func().then 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  7. 没有 GUI渲染,取一个宏任务setTimeout1 的回调出来执行console.log('A'),输出 A;

    <执行栈>:

    C

    E

    D

    A

    <微任务>:

    obj.func().then 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  8. 下一轮,没有微任务,没有 GUI 渲染,取宏任务 setTimeout2 的回调执行 console.log('B'),输出 B;

    <执行栈>:

    C

    E

    D

    A

    B

    <微任务>:

    obj.func().then 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  9. 完事。

image-20220521154826952

例子5

js
console.log("script start"); // setTimeout1 setTimeout(function() { console.log("setTimeout---0"); }, 0); // setTimeout2 setTimeout(function() { console.log("setTimeout---200"); // setTimeout3 setTimeout(function() { console.log("inner-setTimeout---0"); }); // Promise1.then Promise.resolve().then(function() { console.log("promise5"); }); }, 0); // Promise2 Promise.resolve() // Promise2.then1 .then(function() { console.log("promise1"); }) // Promise2.then2 .then(function() { console.log("promise2"); }); // Promise3.then Promise.resolve().then(function() { console.log("promise3"); }); console.log("script end");
  1. script 执行;

    <执行栈>:

    <微任务>:

    <GUI渲染>:

    <宏任务>:

  2. console.log("script start") 执行,输出 script start;

    <执行栈>:

    script start

    <微任务>:

    <GUI渲染>:

    <宏任务>:

  3. setTimeout1 执行,回调放入宏任务队列;

    <执行栈>:

    script start

    <微任务>:

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

  4. setTimeout2 执行,回调放入宏任务队列;

    <执行栈>:

    script start

    <微任务>:

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  5. Promise2 执行,Promise2.then1 回调放入微任务队列;

    <执行栈>:

    script start

    <微任务>:

    Promise2.then1 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  6. Promise3 执行,回调放入微任务队列;

    <执行栈>:

    script start

    <微任务>:

    Promise2.then1 的回调

    Promise3.then 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  7. console.log("script end") 执行,输出 script end;

    <执行栈>:

    script start

    script end

    <微任务>:

    Promise2.then1 的回调

    Promise3.then 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  8. 清空当前所有微任务:Promise2.then1 回调执行 console.log("promise1") ,输出 promise1,此时 Promise2.then1 执行完毕后返回一个新的 Promise,后面 then 执行,Promise2.then2 回调放入微任务队列;

    <执行栈>:

    script start

    script end

    promise1

    <微任务>:

    Promise2.then1 的回调

    Promise3.then 的回调

    Promise2.then2 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  9. Promise3.then 回调执行console.log("promise3") ,输出 promise3;Promise2.then2 回调执行 console.log("promise2") ,输出 promise2;

    <执行栈>:

    script start

    script end

    promise1

    promise3

    promise2

    <微任务>:

    Promise2.then1 的回调

    Promise3.then 的回调

    Promise2.then2 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  10. 没有 GUI渲染,取一个宏任务 setTimeout1 的回调执行 console.log("setTimeout---0"),输出 setTimeout—0;

    <执行栈>:

    script start

    script end

    promise1

    promise3

    promise2

    setTimeout—0

    <微任务>:

    Promise2.then1 的回调~~

    Promise3.then 的回调

    ~~Promise2.then2 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

  11. 下一轮,没有微任务,没有 GUI渲染,取 setTimeout2 的回调执行:console.log("setTimeout---200") ,输出 setTimeout—200;setTimeout3 执行,回调被放入宏任务队列;Promise1.then 执行,回调放入微任务队列;

    <执行栈>:

    script start

    script end

    promise1

    promise3

    promise2

    setTimeout—0

    setTimeout—200

    <微任务>:

    Promise2.then1 的回调~~

    Promise3.then 的回调

    ~~Promise2.then2 的回调

    Promise1.then 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

    setTimeout3 的回调

  12. 下一轮,清空微任务:Promise1.then 回调执行console.log("promise5"),输出 promise5;

    <执行栈>:

    script start

    script end

    promise1

    promise3

    promise2

    setTimeout—0

    setTimeout—200

    promise5

    <微任务>:

    Promise2.then1 的回调~~

    Promise3.then 的回调

    ~~Promise2.then2 的回调

    Promise1.then 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

    setTimeout3 的回调

  13. 没有 GUI 渲染,取一个宏任务 setTimeout3 的回调执行 console.log("inner-setTimeout---0"),输出 inner-setTimeout—0;

    <执行栈>:

    script start

    script end

    promise1

    promise3

    promise2

    setTimeout—0

    setTimeout—200

    promise5

    inner-setTimeout—0

    <微任务>:

    Promise2.then1 的回调~~

    Promise3.then 的回调

    ~~Promise2.then2 的回调

    Promise1.then 的回调

    <GUI渲染>:

    <宏任务>:

    setTimeout1 的回调

    setTimeout2 的回调

    setTimeout3 的回调

  14. 完事。

image-20220521160507129

这里可能会对8、9两个步骤有疑问,但这也并不难理解,我们前面说过,当且仅当 Promise 状态变更,且调用 then 的时候,回调才会被放入微任务队列

那么怎么理解呢?看下面的代码:

js
Promise.resolve() .then(() => { console.log(1) }) .then(() => { console.log(3) })

我们可以将前面这一块看成一个整体:

image-20220521200126005

then 返回一个新的 Promise,在 MDN 中有这样一段话:

The behavior of the handler function follows a specific set of rules. If a handler function:

  • returns a value, the promise returned by then gets resolved with the returned value as its value.
  • doesn’t return anything, the promise returned by then gets resolved with an undefined value.
  • throws an error, the promise returned by then gets rejected with the thrown error as its value.
  • returns an already fulfilled promise, the promise returned by then gets fulfilled with that promise’s value as its value.
  • returns an already rejected promise, the promise returned by then gets rejected with that promise’s value as its value.
  • returns another pending promise object, the resolution/rejection of the promise returned by then will be subsequent to the resolution/rejection of the promise returned by the handler. Also, the resolved value of the promise returned by then will be the same as the resolved value of the promise returned by the handler.

我们可以看到,返回的 Promise 的状态会根据 then 中的回调函数的执行结果不同而不同,也就是说,会在回调执行了之后才会变更状态。

所以上面的代码我们可以这样看:

js
new Promise((resolve, reject) => { Promise.resolve().then(() => { console.log(1) resolve() }) }).then(() => { console.log(3) })

我们分析一下上面的代码:

  1. Promise 的 Executor 执行,Promise.resolve().then 执行,回调被放入微任务队列;

    <执行栈>:

    <微任务>:

    Promise.resolve().then 的回调

    <GUI渲染>:

    <宏任务>:

  2. 清空本轮微任务,console.log(1) 被执行,输出1;之后 Promise 的状态变成 resolve, 所以 Promise.then 被放入微任务队列。

    <执行栈>:

    1

    <微任务>:

    Promise.resolve().then 的回调

    Promise.then 的回调

    <GUI渲染>:

    <宏任务>:

  3. 还是在清空本轮的微任务,console.log(3) 被执行,输出3。

    <执行栈>:

    1

    3

    <微任务>:

    Promise.resolve().then 的回调

    Promise.then 的回调

    <GUI渲染>:

    <宏任务>:

  4. 没有 GUI渲染,没有宏任务,完事。

我们再看下,他到底是不是在本轮执行后续的微任务?还是上面的例子,我们做一些变化,给它加一个宏任务:

js
new Promise((resolve, reject) => { Promise.resolve().then(() => { console.log(1) resolve() }) }).then(() => { console.log(3) }) setTimeout(() => { console.log('setTimeout') }, 0)

直接看执行结果:

image-20220521204945553

显然,是在本轮执行后续的微任务。

趁热打铁,我们再做一个小粟子。

例子5-1

js
console.log('start') // Promise1 Promise.resolve() // Promise1.then1 .then(() => { console.log(1) // setTimeout1 setTimeout(() => { console.log(2) }) }) // Promise1.then2 .then(() => { console.log(3) // setTimeout2 setTimeout(() => { console.log(4) }) }) // Promise1.then3 .then(() => { console.log(5) // setTimeout3 setTimeout(() => { console.log(6) }) }) // Promise2 Promise.resolve() // Promise2.then1 .then(() => { console.log(7) // setTimeout4 setTimeout(() => { console.log(8) }) }) // Promise2.then2 .then(() => { console.log(9) // setTimeout5 setTimeout(() => { console.log(10) }) }) // Promise2.then3 .then(() => { console.log(11) // setTimeout6 setTimeout(() => { console.log(12) }) }) console.log('end')

题看起来有点复杂,不过不要慌,我们一步一步地分析它(因为没有渲染操作,所以忽略它,不再分析):

  1. script 执行;

    <执行栈>:

    <微任务>:

    <宏任务>:

  2. console.log('start') 执行,输出 start;

    <执行栈>:

    start

    <微任务>:

    <宏任务>:

  3. Promise1 执行, Promise1.then1 被放入微任务队列;

    <执行栈>:

    start

    <微任务>:

    Promise1.then1 的回调

    <宏任务>:

  4. Promise2 执行,Promise2.then1 被放入微任务队列;

    <执行栈>:

    start

    <微任务>:

    Promise1.then1 的回调

    Promise2.then1 的回调

    <宏任务>:

  5. console.log('end') 执行,输出 end;

    <执行栈>:

    start

    end

    <微任务>:

    Promise1.then1 的回调

    Promise2.then1 的回调

    <宏任务>:

  6. 清空微任务,Promise1.then1 的回调执行console.log(1),输出 1;setTimeout1 执行,回调放入宏任务队列;之后,Promise1.then2 的回调被放入微任务队列;

    <执行栈>:

    start

    end

    1

    <微任务>:

    Promise1.then1 的回调

    Promise2.then1 的回调

    Promise1.then2 的回调

    <宏任务>:

    setTimeout1 的回调

  7. Promise2.then1 的回调执行console.log(7),输出 7;setTimeout 4 执行,回调放入宏任务队列;之后,Promise2.then2 的回调被放入微任务队列;

    <执行栈>:

    start

    end

    1

    7

    <微任务>:

    Promise1.then1 的回调

    Promise2.then1 的回调

    Promise1.then2 的回调

    Promise2.then2 的回调

    <宏任务>:

    setTimeout1 的回调

    setTimeout4 的回调

  8. 继续清空本轮微任务,Promise1.then2 的回调执行console.log(3) ,输出3; setTimeout2 执行,回调放入宏任务队列;之后,Promise1.then3 的回调被放入微任务队列;

    <执行栈>:

    start

    end

    1

    7

    3

    <微任务>:

    Promise1.then1 的回调

    Promise2.then1 的回调

    Promise1.then2 的回调

    Promise2.then2 的回调

    Promise1.then3 的回调

    <宏任务>:

    setTimeout1 的回调

    setTimeout4 的回调

    setTimeout2 的回调

  9. Promise2.then2 的回调执行console.log(9), 输出 9;setTimeout5 执行,回调被放入宏任务队列;之后 Promise2.then3 的回调被放入微任务队列;

    <执行栈>:

    start

    end

    1

    7

    3

    9

    <微任务>:

    Promise1.then1 的回调

    Promise2.then1 的回调

    Promise1.then2 的回调

    Promise2.then2 的回调

    Promise1.then3 的回调

    Promise2.then3 的回调

    <宏任务>:

    setTimeout1 的回调

    setTimeout4 的回调

    setTimeout2 的回调

    setTimeout5 的回调

  10. Promise1.then3 的回调执行console.log(5) ,输出 5;setTimeout3 执行,回调放入宏任务队列;

    <执行栈>:

    start

    end

    1

    7

    3

    9

    5

    <微任务>:

    Promise1.then1 的回调

    Promise2.then1 的回调

    Promise1.then2 的回调

    Promise2.then2 的回调

    Promise1.then3 的回调

    Promise2.then3 的回调

    <宏任务>:

    setTimeout1 的回调

    setTimeout4 的回调

    setTimeout2 的回调

    setTimeout5 的回调

    setTimeout3 的回调

  11. Promise2.then3 的回调执行console.log(11),输出 11;setTimeout6 执行,回调放入宏任务队列;

    <执行栈>:

    start

    end

    1

    7

    3

    9

    5

    11

    <微任务>:

    Promise1.then1 的回调

    Promise2.then1 的回调

    Promise1.then2 的回调

    Promise2.then2 的回调

    Promise1.then3 的回调

    Promise2.then3 的回调

    <宏任务>:

    setTimeout1 的回调

    setTimeout4 的回调

    setTimeout2 的回调

    setTimeout5 的回调

    setTimeout3 的回调

    setTimeout6 的回调

  12. 至此,本轮所有微任务被清空;GUI 渲染没有,取一个宏任务执行(从代码上看,已经不会再有微任务和GUI渲染),所以之后是依次取宏任务执行,这里就不一一分析了;

    <执行栈>:

    start

    end

    1

    7

    3

    9

    5

    11

    2

    8

    4

    10

    6

    12

    <微任务>:

    Promise1.then1 的回调

    Promise2.then1 的回调

    Promise1.then2 的回调

    Promise2.then2 的回调

    Promise1.then3 的回调

    Promise2.then3 的回调

    <宏任务>:

    setTimeout1 的回调

    setTimeout4 的回调

    setTimeout2 的回调

    setTimeout5 的回调

    setTimeout3 的回调

    setTimeout6 的回调

  13. 完事。

image-20220521212024585

例子6

js
// setTimeout1 setTimeout(function () { console.log('timeout1'); }, 1000); console.log('start'); // Promise1.then Promise.resolve().then(function () { console.log('promise1'); // Promise2.then Promise.resolve().then(function () { console.log('promise2'); }); // setTimeout2 setTimeout(function () { // Promise3.then Promise.resolve().then(function () { console.log('promise3'); }); console.log('timeout2') }, 0); }); console.log('done');
  1. script 执行;

    <执行栈>:

    <微任务>:

    <宏任务>:

  2. setTimeout1 执行,回调放入宏任务队列;

    <执行栈>:

    <微任务>:

    <宏任务>:

    setTimeout1 的回调(延时1000)

  3. console.log('start') 输出 start;

    <执行栈>:

    start

    <微任务>:

    <宏任务>:

    setTimeout1 的回调(延时1000)

  4. Promise1.then 执行,回调放入微任务队列;

    <执行栈>:

    start

    <微任务>:

    Promise1.then 的回调

    <宏任务>:

    setTimeout1 的回调(延时1000)

  5. console.log('done') 输出 done;

    <执行栈>:

    start

    done

    <微任务>:

    Promise1.then 的回调

    <宏任务>:

    setTimeout1 的回调(延时1000)

  6. 清空微任务,Promise1.then 的回调执行console.log('promise1') ,输出 promise1;Promise2.then 执行,回调放入微任务队列;setTimeout2 执行,回调放入宏任务队列;

    <执行栈>:

    start

    done

    promise1

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    <宏任务>:

    setTimeout1 的回调(延时1000)

    setTimeout2 的回调(延时0)

  7. 继续本轮微任务,Promise2.then 的回调执行 console.log('promise2'),输出 promise2;

    <执行栈>:

    start

    done

    promise1

    promise2

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    <宏任务>:

    setTimeout1 的回调(延时1000)

    setTimeout2 的回调(延时0)

  8. 取一个宏任务回调执行,注意是延时已到了的执行,所以是 setTimeout2 的回调执行,Promise3.then 执行,回调放入微任务队列;console.log('timeout2') 执行,输出 timeout2;

    <执行栈>:

    start

    done

    promise1

    promise2

    timeout2

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    Promise3.then 的回调

    <宏任务>:

    setTimeout1 的回调(延时1000)

    setTimeout2 的回调(延时0)

  9. 下一轮,清空微任务,Promise3.then 的回调执行console.log('promise3'),输出 promise3;

    <执行栈>:

    start

    done

    promise1

    promise2

    timeout2

    promise3

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    Promise3.then 的回调

    <宏任务>:

    setTimeout1 的回调(延时1000)

    setTimeout2 的回调(延时0)

  10. 等待 1000ms 延迟过后,取 setTimeout1 的回调执行console.log('timeout1'), 输出 timeout1;

    <执行栈>:

    start

    done

    promise1

    promise2

    timeout2

    promise3

    timeout1

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    Promise3.then 的回调

    <宏任务>:

    setTimeout1 的回调(延时1000)

    setTimeout2 的回调(延时0)

  11. 完事。

image-20220521220024225

例子7

据说是一道头条的面试题。

js
async function async1() { console.log('async1 start') await async2() console.log('async1 end') } async function async2() { console.log('async2') } console.log('script start') setTimeout(function () { console.log('setTimeout') }, 0) async1() new Promise((resolve) => { console.log('promise1') resolve() }).then(function () { console.log('promise2') }) console.log('script end')

我们在分析这道题之前,先了解一下 async/await 是怎么回事。

async/await

async/await 是一个 generator + co 的语法糖;一个函数,加上了 async 关键字,那么它的返回值必定是一个 Promise。await 后面的语句,它也必定是个 Promise,如果不是,将会被 Promise.resolve() 包裹。

js
async function test () { console.log(1) await 2 console.log(3) } // => 相当于 function test () { console.log(1) Promise.resolve(2) .then(() => { console.log(3) }) } async function test1 () { console.log(1) await test2() console.log(2) } async function test2 () { console.log(3) } // => 相当于 function test1 () { console.log(1) new Promise((resolve, reject) => { console.log(3) resolve() }) .then(() => { console.log(2) }) }

有了上面的知识,我们继续分析一下这道题,我们先把 async 函数作一些调整:

js
function async1 () { console.log('async1 start') // Promise1 new Promise((resolve, reject) => { async2() resolve() }) // Promise1.then .then(() => { console.log('async1 end') }) } function async2 () { console.log('async2') } console.log('script start') setTimeout(function () { console.log('setTimeout') }, 0) async1() // Promise2 new Promise((resolve) => { console.log('promise1') resolve() }) // Promise2.then .then(function () { console.log('promise2') }) console.log('script end')
  1. script 执行;

    <执行栈>:

    <微任务>:

    <宏任务>:

  2. async1async2 函数声明,console.log('script start') 执行,输出 script start;

    <执行栈>:

    script start

    <微任务>:

    <宏任务>:

  3. setTimeout 执行,回调放入宏任务队列;

    <执行栈>:

    script start

    <微任务>:

    <宏任务>:

    setTimeout 的回调

  4. async1() 执行,console.log('async1 start') 执行,输出 async1 start;Promise1 的 Executor 内部代码执行,async2() 执行console.log('async2') ,输出 async2;Promise1.then 的回调放入微任务队列;

    <执行栈>:

    script start

    async1 start

    async2

    <微任务>:

    Promise1.then 的回调

    <宏任务>:

    setTimeout 的回调

  5. Promise2 的 Executor 内部代码执行console.log('promise1') ,输出 promise1 ;Promise2.then 的回调放入微任务队列;

    <执行栈>:

    script start

    async1 start

    async2

    promise1

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    <宏任务>:

    setTimeout 的回调

  6. console.log('script end') 执行,输出 script end;

    <执行栈>:

    script start

    async1 start

    async2

    promise1

    script end

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    <宏任务>:

    setTimeout 的回调

  7. 清空微任务;Promise1.then 的回调执行console.log('async1 end') ,输出 async1 end;Promise2.then 的回调执行console.log('promise2') ,输出 promise2;

    <执行栈>:

    script start

    async1 start

    async2

    promise1

    script end

    async1 end

    promise2

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    <宏任务>:

    setTimeout 的回调

  8. 取一个宏任务setTimeout1 的回调执行,输出 setTimeout;

    <执行栈>:

    script start

    async1 start

    async2

    promise1

    script end

    async1 end

    promise2

    setTimeout

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    <宏任务>:

    setTimeout 的回调

  9. 完事。

image-20220521222548687

例子8

js
console.log('script start'); setTimeout(() => { console.log('setTimeout'); }, 2000); Promise.resolve() .then(function() { console.log('promise1'); }) .then(function() { console.log('promise2'); }); async function foo() { await bar() console.log('async1 end') } foo() async function errorFunc () { try { await Promise.reject('error!!!') } catch(e) { console.log(e) } console.log('async1'); return Promise.resolve('async1 success') } errorFunc().then(res => console.log(res)) function bar() { console.log('async2 end') } console.log('script end');

在分析这个例子之前,我们先看看 try...catch 是怎么回事:

js
console.log(a) // 报错:Uncaught ReferenceError: a is not defined

如果我们使用 try...catch 包裹上面的代码:

js
try { console.log(a) } catch (e) { console.log(e) // 控制台输出:'Syntax Error: `a` is not defined' }

我们看下 Promise 的情况:

js
async function test () { await Promise.reject('Error') // 报错:Uncaught (in promise) Error } test()

使用 try...catch 包裹 Promise 的时候,如果 Promise 的状态变成 Rejected,那么会执行 catch 里面的代码:

js
async function test () { try { await Promise.reject('Error') } catch (e) { console.log(e) // 控制台输出:'Error' } } test()

如果 Promise 的状态变成 Fulfilled,那么 catch 里面的代码不会被执行:

js
async function test () { try { await Promise.resolve(1) } catch (e) { console.log(e) } } test()

注意:try...catch 并不会捕获 Promise.prototype.reject 出来的错误:

js
try { Promise.reject('Error') // 控制台报错:Uncaught (in promise) Error } catch (e) { console.log(e) }

但是加上了 await 之后,后续的代码执行完毕,会由 Generator.prototype.throw 来抛出一个错误,从而被 catch 捕捉到。

并且,try...catch 不会阻止后面的代码执行。

有了这些前置知识,我们可以将上面的代码转换成这样来分析一下:

js
console.log('script start'); setTimeout(() => { console.log('setTimeout'); }, 2000); // Promise1 Promise.resolve() // Promise1.then1 .then(function() { console.log('promise1'); }) // Promise1.then2 .then(function() { console.log('promise2'); }); function foo() { // Promise2 new Promise((resolve, reject) => { bar() resolve() }) // Promise2.then .then(() => { console.log('async1 end') }) } foo() function errorFunc () { // Promise3 return new Promise((_, reject) => { reject('error!!!') }) // Promise3.then .then( () => { console.log('async1') return Promise.resolve('async1 success') }, e => { console.log(e) console.log('async1') return Promise.resolve('async1 success') }) } errorFunc().then(res => console.log(res)) function bar() { console.log('async2 end') } console.log('script end');
  1. script 执行;

    <执行栈>:

    <微任务>:

    <宏任务>:

  2. console.log('script start') 执行,输出 script start;

    <执行栈>:

    script start

    <微任务>:

    <宏任务>:

  3. setTimeout 执行,回调放入宏任务队列;

    <执行栈>:

    script start

    <微任务>:

    <宏任务>:

    setTimeout 的回调(延时2000)

  4. Promise1 执行,Promise1.then1 的回调放入微任务队列;

    <执行栈>:

    script start

    <微任务>:

    Promise1.then1 的回调

    <宏任务>:

    setTimeout 的回调(延时2000)

  5. foo() 执行,Promise2 的 Executor 执行,bar() 执行console.log('async2 end') ,输出 async2 end;Promise2.then 的回调放入微任务队列;

<执行栈>:

script start

async2 end

<微任务>:

Promise1.then1 的回调

Promise2.then 的回调

<宏任务>:

setTimeout 的回调(延时2000)

  1. errorFunc() 执行,Promise3 的 Executor 执行,Promise3.then 的回调放入微任务队列;

    <执行栈>:

    script start

    async2 end

    <微任务>:

    Promise1.then1 的回调

    Promise2.then 的回调

    Promise3.then 的回调

    <宏任务>:

    setTimeout 的回调(延时2000)

  2. console.log('script end') 执行,输出 script end;

    <执行栈>:

    script start

    async2 end

    script end

    <微任务>:

    Promise1.then1 的回调

    Promise2.then 的回调

    Promise3.then 的回调

    <宏任务>:

    setTimeout 的回调(延时2000)

  3. 清空本轮微任务,Promise1.then1 的回调执行,输出 promise1;Promise1.then2 的回调放入微任务队列;

    <执行栈>:

    script start

    async2 end

    script end

    promise1

    <微任务>:

    Promise1.then1 的回调

    Promise2.then 的回调

    Promise3.then 的回调

    Promise1.then2 的回调

    <宏任务>:

    setTimeout 的回调(延时2000)

  4. Promise2.then 的回调执行,输出 async1 end;

    <执行栈>:

    script start

    async2 end

    script end

    promise1

    async1 end

    <微任务>:

    Promise1.then1 的回调

    Promise2.then 的回调

    Promise3.then 的回调

    Promise1.then2 的回调

    <宏任务>:

    setTimeout 的回调(延时2000)

  5. Promise3.then 的回调执行,因为它是一个 Rejected 的状态,所以会执行 then 中的第二个参数回调,输出 error!!!;console.log('async1') 执行,输出 async1; 同时返回一个 Fulfilled 状态的新 Promise;errorFunc().then 放入微任务队列;

    <执行栈>:

    script start

    async2 end

    script end

    promise1

    async1 end

    error!!!

    async1;

    <微任务>:

    Promise1.then1 的回调

    Promise2.then 的回调

    Promise3.then 的回调

    Promise1.then2 的回调

    errorFunc().then 的回调

    <宏任务>:

    setTimeout 的回调(延时2000)

  6. Promise1.then2 的回调执行,输出 promise2;

    <执行栈>:

    script start

    async2 end

    script end

    promise1

    async1 end

    error!!!

    async1;

    promise2

    <微任务>:

    Promise1.then1 的回调

    Promise2.then 的回调

    Promise3.then 的回调

    Promise1.then2 的回调

    errorFunc().then 的回调

    <宏任务>:

    setTimeout 的回调(延时2000)

  7. errorFunc().then 的回调被执行,输出 async1 success;至此,本轮微任务被清空完毕;

    <执行栈>:

    script start

    async2 end

    script end

    promise1

    async1 end

    error!!!

    async1;

    promise2

    async1 success

    <微任务>:

    Promise1.then1 的回调

    Promise2.then 的回调

    Promise3.then 的回调

    Promise1.then2 的回调

    errorFunc().then 的回调

    <宏任务>:

    setTimeout 的回调(延时2000)

  8. 延时 2000ms 之后,取 setTimeout 的回调执行,输出 setTimeout;

    <执行栈>:

    script start

    async2 end

    script end

    promise1

    async1 end

    error!!!

    async1;

    promise2

    async1 success

    setTimeout

    <微任务>:

    Promise1.then1 的回调

    Promise2.then 的回调

    Promise3.then 的回调

    Promise1.then2 的回调

    errorFunc().then 的回调

    <宏任务>:

    setTimeout 的回调(延时2000)

  9. 完事

image-20220523180058361

例子9

js
console.log('start') let i = 0 let t = setInterval(() => { if (i > 2) { clearInterval(t) t = null return } // Promise1.then Promise.resolve().then(() => { console.log('promise') }) setTimeout(() => { // Promise2.then Promise.resolve().then(() => { console.log('setTimeout Promise') }) console.log('setTimeout') }) console.log('setInterval') i++ }, 50) console.log('end')

这题是我自己瞎写的,在分析之前,我们先了解一下 setInterval 是怎么工作的:

js
let i = 1 let t = setInterval(() => { if (i > 3) { clearInterval(t) t = null } Promise.resolve().then(console.log('promise')) setTimeout(() => { console.log('setTimeout') }) console.log(i++) for (let j = 0; j < 1000000000; j++) { let a = 1 } }, 10)

我们运行上面这段代码可以发现,它每一次输出的结果都有可能不一样。因为 setInterval 当延时时间到了,就会把回调推到宏任务队列,受到 for 循环的影响,每次执行回调所需的时间都不一致,导致后续的某些 setInterval 的回调会比 setTimeout 的回调更早执行。

例子上的代码,在 setInterval 的回调中的代码执行用时不多,它应该比设置的延时时间要更短,所以我们可以认为它的下一次回调会在执行完这次回调之后再推入宏任务队列。

  1. script 执行;

    <执行栈>:

    <微任务>:

    <宏任务>:

  2. console.log('start') 执行,输出 start;

    <执行栈>:

    start

    <微任务>:

    <宏任务>:

  3. setInterval 执行,setInterval 的回调被放入宏任务队列;

    <执行栈>:

    start

    <微任务>:

    <宏任务>:

    setInterval 的回调

  4. console.log('end') 执行,输出 end;

    <执行栈>:

    start

    end

    <微任务>:

    <宏任务>:

    setInterval 的回调

  5. 本轮没有微任务,没有 GUI 渲染,取一个宏任务 setInterval 的回调执行;Promise1.then 的回调放入微任务队列;setTimeout 的回调放入宏任务队列;console.log('setInterval') 执行,输出 setIntervali++ 执行,此时 i 为 1;本次回调执行完毕,setInterval 的回调再次加入宏任务队列;

    <执行栈>:

    start

    end

    setInterval

    <微任务>:

    Promise1.then 的回调

    <宏任务>:

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

  6. 下一轮,清空所有微任务;Promise1.then 的回调执行,输出 promise;

    <执行栈>:

    start

    end

    setInterval

    promise

    <微任务>:

    Promise1.then 的回调

    <宏任务>:

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

  7. 取一个宏任务 setTimeout 的回调执行,Promise2.then 的回调被放入微任务队列;console.log('setTimeout') 执行,输出 setTimeout;

    <执行栈>:

    start

    end

    setInterval

    promise

    setTimeout

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    <宏任务>:

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

  8. 下一轮,清空微任务 Promise2.then 的回调执行,输出 setTimeout Promise;

    <执行栈>:

    start

    end

    setInterval

    promise

    setTimeout

    setTimeout Promise

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    <宏任务>:

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

  9. 取一个宏任务 setInterval 的回调执行;Promise1.then 的回调放入微任务队列;setTimeout 的回调放入宏任务队列;console.log('setInterval') 执行,输出 setIntervali++ 执行,此时 i 为 2;本次回调执行完毕,setInterval 的回调再次加入宏任务队列;

    <执行栈>:

    start

    end

    setInterval

    promise

    setTimeout

    setTimeout Promise

    setInterval

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    Promise1.then 的回调

    <宏任务>:

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

  10. 同 6;清空所有微任务;Promise1.then 的回调执行,输出 promise;

    <执行栈>:

    start

    end

    setInterval

    promise

    setTimeout

    setTimeout Promise

    setInterval

    promise

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    Promise1.then 的回调

    <宏任务>:

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

  11. 同 7;取一个宏任务 setTimeout 的回调执行,Promise2.then 的回调被放入微任务队列;console.log('setTimeout') 执行,输出 setTimeout;

    <执行栈>:

    start

    end

    setInterval

    promise

    setTimeout

    setTimeout Promise

    setInterval

    promise

    setTimeout

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    Promise1.then 的回调

    Promise2.then 的回调

    <宏任务>:

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

  12. 同 8;清空微任务 Promise2.then 的回调执行,输出 setTimeout Promise;

    <执行栈>:

    start

    end

    setInterval

    promise

    setTimeout

    setTimeout Promise

    setInterval

    promise

    setTimeout

    setTimeout Promise

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    Promise1.then 的回调

    Promise2.then 的回调

    <宏任务>:

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

  13. 同 9;取一个宏任务 setInterval 的回调执行;Promise1.then 的回调放入微任务队列;setTimeout 的回调放入宏任务队列;console.log('setInterval') 执行,输出 setIntervali++ 执行,此时 i 为 3;本次回调执行完毕,setInterval 的回调再次加入宏任务队列;

  14. 同10

  15. 同11

  16. 同12

    <执行栈>:

    start

    end

    setInterval

    promise

    setTimeout

    setTimeout Promise

    setInterval

    promise

    setTimeout

    setTimeout Promise

    setInterval

    promise

    setTimeout

    setTimeout Promise

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    Promise1.then 的回调

    Promise2.then 的回调~~

    Promise1.then 的回调

    Promise2.then 的回调

    <宏任务>:

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

    setTimeout 的回调~~

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

  17. 取一个宏任务 setInterval 的回调执行;此时 t 为 3,满足 i > 2 的条件,clearInterval(t) 执行,return 中止后续代码执行;

    <执行栈>:

    start

    end

    setInterval

    promise

    setTimeout

    setTimeout Promise

    setInterval

    promise

    setTimeout

    setTimeout Promise

    setInterval

    promise

    setTimeout

    setTimeout Promise

    <微任务>:

    Promise1.then 的回调

    Promise2.then 的回调

    Promise1.then 的回调

    Promise2.then 的回调~~

    Promise1.then 的回调

    Promise2.then 的回调

    <宏任务>:

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

    setTimeout 的回调~~

    setInterval 的回调

    setTimeout 的回调

    setInterval 的回调

  18. 完事

image-20220526174335904

例子10

我们看下这个例子,DOM 中有一个按钮

html
<button id="J_button">Button</button>

然后在 js 中对该按钮加两个点击事件处理函数

js
const oBtn = document.querySelector('#J_button') function handler1 () { // setTimeout1 setTimeout(() => { console.log('setTimeout1') }, 0) // promise1.then Promise.resolve().then(() => { console.log('promise1') }) console.log('handler1') } function handler2 () { // setTimeout2 setTimeout(() => { console.log('setTimeout2') }, 0) // promise2.then Promise.resolve().then(() => { console.log('promise2') }) console.log('handler2') } oBtn.addEventListener('click', handler1, false) oBtn.addEventListener('click', handler2, false) // oBtn.click()

在我们通过手动点击按钮的时候,结果如下:

image-20220530094048867

如果我们把 oBtn.click() 的注释解开,也就是说让程序自动执行点击操作,重新刷新页面后,结果如下:

image-20220530094207884

为什么两种结果不一样呢?

oBtn.click()

我们先从程序执行点击操作来分析,在 oBtn.click() 执行的时候,实际上是直接执行了 handler1handler2 两个事件处理函数,相当于以下代码:

js
function handler1 () { // setTimeout1 setTimeout(() => { console.log('setTimeout1') }, 0) // promise1.then Promise.resolve().then(() => { console.log('promise1') }) console.log('handler1') } function handler2 () { // setTimeout2 setTimeout(() => { console.log('setTimeout2') }, 0) // promise2.then Promise.resolve().then(() => { console.log('promise2') }) console.log('handler2') } handler1() handler2()

这样子看,我们就很轻易地分析出如图上的结果。

用户点击触发

首先,我们需要知道 addEventListener 是一个宏任务,当它被用户触发时,会把事件处理函数放入宏任务队列。只有这样,我们才能分析在用户点击时,代码是怎样运行的。

  1. script 执行;获取DOM;函数声明;事件注册;

    <执行栈>:

    <微任务>:

    <宏任务>:

  2. 当用户点击时,handler1hanlder2 依次放入宏任务队列;

    <执行栈>:

    <微任务>:

    <宏任务>:

    handler1

    handler2

  3. 取一个宏任务 handler1 执行;setTimeout1 的回调被放入宏任务队列;promise1.then 的回调被放入微任务队列;console.log('handler1') 执行,输出 handler1;

    <执行栈>:

    hanlder1

    <微任务>:

    promise1.then 的回调

    <宏任务>:

    handler1

    handler2

    setTimeout1 的回调

  4. 下一轮,清空微任务 promise1.then 的回调执行,输出 promise1

    <执行栈>:

    hanlder1

    promise1

    <微任务>:

    promise1.then 的回调

    <宏任务>:

    handler1

    handler2

    setTimeout1 的回调

  5. 取一个宏任务 handle2 执行;setTimeout2 的回调被放入宏任务队列;promise2.then 的回调被放入微任务队列;console.log('handler2') 执行,输出 handler2;

    <执行栈>:

    hanlder1

    promise1

    handler2

    <微任务>:

    promise1.then 的回调

    promise2.then 的回调

    <宏任务>:

    handler1

    handler2

    setTimeout1 的回调

    setTimeout2 的回调

  6. 下一轮,清空微任务 promise2.then 的回调执行,输出 promise2

    <执行栈>:

    hanlder1

    promise1

    handler2

    promise2

    <微任务>:

    promise1.then 的回调

    promise2.then 的回调

    <宏任务>:

    handler1

    handler2

    setTimeout1 的回调

    setTimeout2 的回调

  7. 取一个宏任务 setTimeout1 的回调执行,输出 setTimeout1;

    <执行栈>:

    hanlder1

    promise1

    handler2

    promise2

    setTimeout1

    <微任务>:

    promise1.then 的回调

    promise2.then 的回调

    <宏任务>:

    handler1

    handler2

    setTimeout1 的回调

    setTimeout2 的回调

  8. 下一轮,没有微任务,没有 GUI 渲染,取一个宏任务 setTimeout2 的回调执行,输出 setTimeout2;

    <执行栈>:

    hanlder1

    promise1

    handler2

    promise2

    setTimeout1

    setTimtoue2

    <微任务>:

    promise1.then 的回调

    promise2.then 的回调

    <宏任务>:

    handler1

    handler2

    setTimeout1 的回调

    setTimeout2 的回调

  9. 完事。

宏任务

宏任务(Macro Task)是宿主提供的异步方法和任务,如 scriptsetTimeoutsetIntervalsetImmediatemessageChannelrequestAnimationFrame、用户交互事件、ajax

setTimeout

setTimeout 用于设定一个定时器,该定时器到期后,执行一个函数,或一段代码;

plain-text
setTimeout(callback[, delay, arg1, arg2, ...]) setTimeout(callback[, delay]) setTimeout(code[, delay])

参数说明

  • callback:到期后想要执行的回调函数;
  • code:到期后想要执行的代码,字符串形式,不推荐这种写法;
  • delay:延时,单位为毫秒;如果省略该参数,默认值为 0;延时不代表它的回调会在到期时立即执行,实际的执行时间会比期待的 delay 长。
  • arg1 ... argN:附加参数,它们作为参数传递给 callback

返回值

返回 timeoutID,它是一个正整数,表示定时器的编号,用于取消该定时器。

示例

js
const t1 = setTimeout(() => { console.log(1) }, 1000) const t2 = setTimeout(() => { console.log(2) clearTimeout(t1) }, 500)

setInterval

setInterval 用于重复执行一个函数或一段代码,每次调用都有相同的时间间隔;具体参数和用法和 setTimeout 一致。

setImmediate

setImmediate 用于把一些需要长时间运行的操作放在一个回调函数里,在浏览器完成其它语句后,就立即执行这个回调函数。

注意:这个方法只有最新版本的 IE/Edge 和 Node.js 0.10+ 实现了。

plain-text
setImmediate(callback[, arg1, arg2, ...]) setImmediate(callback)

参数说明

  • callback:回调函数
  • arg1 ... argN,传入回调的参数

返回值

类似 setTimeout

我们可以通过 setTimeout(callback, 0) 来模拟 setImmediate

MessageChannel

MessageChannel 接口允许我们创建一个新的消息通道,并通过两个 MessagePort 属性发送数据。

示例

html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>MessageChannel</title> <link rel="stylesheet" href="message-channel.css" /> </head> <body> <div class="container"> <!-- PORT1 --> <section class="port port1"> <header> <h2>与 PORT2 的对话</h2> </header> <div class="content-wrapper" id="J_port1-content"></div> <div class="input-wrapper"> <textarea type="text" id="J_port1-input"></textarea> <button type="button" id="J_port1-button">SEND</button> </div> </section> <!-- PORT2 --> <section class="port port2"> <header> <h2>与 PORT1 的对话</h2> </header> <div class="content-wrapper" id="J_port2-content"></div> <div class="input-wrapper"> <textarea type="text" id="J_port2-input"></textarea> <button type="button" id="J_port2-button">SEND</button> </div> </section> </div> <script type="tpl" id="J_tpl"> <div class="msg msg-from-{{from}}"> <div class="header"> <div class="name">{{ from }}</div> <div class="date">{{ date }}</div> </div> <div class="message">{{ message }}</div> </div> </script> <script> const zeroFill = input => ('' + input).padStart(2, '0') function formatDate (date, format = 'YYYY-MM-DD HH:mm:ss') { const Y = date.getFullYear() const M = date.getMonth() + 1 const D = date.getDate() const H = date.getHours() const m = date.getMinutes() const s = date.getSeconds() return format.replace(/YYYY/, Y) .replace(/MM/, zeroFill(M)) .replace(/DD/, zeroFill(D)) .replace(/HH/, zeroFill(H)) .replace(/mm/, zeroFill(m)) .replace(/ss/, zeroFill(s)) } </script> <script type="text/javascript"> const { port1, port2 } = new MessageChannel() const oPort1Input = document.querySelector('#J_port1-input') const oPort1Button = document.querySelector('#J_port1-button') const oPort1Content = document.querySelector('#J_port1-content') const oPort2Input = document.querySelector('#J_port2-input') const oPort2Button = document.querySelector('#J_port2-button') const oPort2Content = document.querySelector('#J_port2-content') const tpl = document.querySelector('#J_tpl').innerHTML const reg = /{{(.+?)}}/g; const data = new Proxy([], { set (arr, index, value) { Reflect.set(arr, index, value) render() return true } }) function render () { let innerHTML = '' data.forEach((item) => { innerHTML += tpl.replace(reg, ($, $1) => { return item[$1.trim()] }) }) oPort1Content.innerHTML = innerHTML oPort2Content.innerHTML = innerHTML scrollToBottom() } function scrollToBottom () { oPort1Content.scrollBy(0, 100000000) oPort2Content.scrollBy(0, 100000000) } function handleSendMessage (port) { let value = '' switch (port) { case 'port1': value = oPort1Input.value if (!value) { return } port1.postMessage(value.replace(/[\r\n]/g, '<br />')) oPort1Input.value = '' oPort1Input.focus() break case 'port2': value = oPort2Input.value if (!value) { return } port2.postMessage(value.replace(/[\r\n]/g, '<br />')) oPort2Input.value = '' oPort2Input.focus() break default: break } } port1.onmessage = (e) => { data.push({ from: 'port2', to: 'port1', message: e.data, date: formatDate(new Date()) }) } port2.onmessage = (e) => { data.push({ from: 'port1', to: 'port2', message: e.data, date: formatDate(new Date()) }) } oPort1Input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { if ((e.metaKey || e.ctrlKey)) { e.target.value += '\n' return } e.preventDefault() handleSendMessage('port1') } }) oPort2Input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { if ((e.metaKey || e.ctrlKey)) { e.target.value += '\n' return } e.preventDefault() handleSendMessage('port2') } }) oPort1Button.addEventListener('click', () => { handleSendMessage('port1') }) oPort2Button.addEventListener('click', () => { handleSendMessage('port2') }) </script> </body> </html>

message-channel.css

css
* { margin: 0; padding: 0; box-sizing: border-box; } body { color: #333; font-size: 16px; } .container { display: flex; width: 1000px; margin: 50px auto; justify-content: space-between; } .port { width: 49%; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; } .port header { padding: 8px 16px; border-bottom: 1px solid #ddd; background-color: deepskyblue; } .port h2 { color: #fff; font-size: 20px; line-height: 24px; } .content-wrapper { padding: 16px; height: 320px; overflow-y: auto; background-color: #f4f4f8; } .content-wrapper .msg::after { content: ''; display: table; clear: both; } .content-wrapper .header { display: flex; align-items: center; } .content-wrapper .msg:not(:first-child) { margin-top: 24px; } .content-wrapper .message { position: relative; max-width: 80%; margin-top: 10px; padding: 6px 16px; border-radius: 4px; word-break: break-all; } .content-wrapper .header .name { font-size: 18px; font-weight: 700; } .content-wrapper .header .date { margin: 0 8px; font-size: 14px; color: #8c8c8c; } .input-wrapper { border-top: 1px solid #ddd; background-color: #f4f4f8; } .input-wrapper::after { content: ''; display: table; clear: both; } .input-wrapper > textarea { display: block; width: 100%; height: 80px; padding: 16px; border: 0; outline: 0; line-height: 24px; font-size: 16px; background-color: #fff; resize: none; } .input-wrapper > button { float: right; padding: 8px 16px; outline: 0; color: #fff; border: 1px solid deepskyblue; background-color: deepskyblue; } :is( .port1 .content-wrapper .msg-from-port1, .port2 .content-wrapper .msg-from-port2 ) .header { justify-content: flex-end; } :is( .port1 .content-wrapper .msg-from-port1, .port2 .content-wrapper .msg-from-port2 ) .header .name { order: 2; } :is( .port1 .content-wrapper .msg-from-port1, .port2 .content-wrapper .msg-from-port2 ) .header .date { order: 1; } :is( .port1 .content-wrapper .msg-from-port1, .port2 .content-wrapper .msg-from-port2 ) .message { float: right; color: #fff; background-color: deepskyblue; } :is( .port1 .content-wrapper .msg-from-port1, .port2 .content-wrapper .msg-from-port2 ) .message::before { content: ''; position: absolute; right: 10px; top: -5px; width: 0; height: 0; border-bottom: 5px solid deepskyblue; border-left: 5px solid transparent; border-right: 5px solid transparent; } :is( .port1 .content-wrapper .msg-from-port2, .port2 .content-wrapper .msg-from-port1 ) .message { float: left; color: #333; background-color: #fff; } :is( .port1 .content-wrapper .msg-from-port2, .port2 .content-wrapper .msg-from-port1 ) .message::before { content: ''; position: absolute; left: 10px; top: -5px; width: 0; height: 0; border-bottom: 5px solid #fff; border-left: 5px solid transparent; border-right: 5px solid transparent; } :is( .port1 .content-wrapper .msg-from-port2, .port2 .content-wrapper .msg-from-port1 ) .message { float: left; color: #333; background-color: #fff; }

message-channel

requestAnimationFrame

requestAnimationFrame()方法需要传递一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

回调函数通常每秒执行60次,但在大多数浏览器中,回调函数的执行次数与屏幕的刷新率相匹配。

回调函数会被传入一个 DOMHighResTimeStamp 参数,指示当前回调函数被触发的时间。

示例

js
const element = document.getElementById('some-element-you-want-to-animate'); let start; function step(timestamp) { if (start === undefined) start = timestamp; const elapsed = timestamp - start; //这里使用`Math.min()`确保元素刚好停在200px的位置。 element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)'; if (elapsed < 2000) { // 在两秒后停止动画 window.requestAnimationFrame(step); } } window.requestAnimationFrame(step);

我们分析一下下面这段代码的执行顺序:

html
<div id="J_wrapper"></div> <script type="text/javascript"> const oWrapper = document.querySelector('#J_wrapper') function callback (mutationList, observer) { console.log(4) // setTimeout2 setTimeout(() => { console.log(5) }) // promise2.then Promise.resolve().then(() => { console.log(6) }) } const observer = new MutationObserver(callback) // childList、attributes 和 characterData 三个属性必须有一个为 true observer.observe(oWrapper, { childList: true, // 观察目标子节点的变化,是否有添加或者删除,无默认值 attributes: true, // 观察属性变动,无默认值 characterData: true // 观察指定目标节点或子节点树中节点所包含的字符数据的变化,无默认值 }) oWrapper.textContent = 'Hello world!' console.log(1) // setTimeout1 setTimeout(() => { console.log(2) }) // promise1.then Promise.resolve().then(() => { console.log(3) }) </script>
  1. script 执行;
  2. 代码依次执行,直接 observer.observe() 执行,MutationObserver 的回调放入宏任务

微任务

微任务(Micro Task)指的是语言标准(ECMA-262)提供的API:Promise.prototype.thenmutationObserverprocess.nextTick

MutationObserver

MutationObserver 提供了监听 DOM 树变化的能力。

它是一个构造函数,实例化之后返回一个新的观察器,它会在触发指定 DOM 事件时,调用指定的回调函数。

MutationObserver 对 DOM 的观察不会立即启动,必须先调用 observe() 方法来开启,要监听哪一部分 DOM 以及要响应哪些更改。

参数

callback:一个回调函数,当指定的节点或子树以及配置项有 DOM 变化时调用。

回调函数有两个参数:一个是描述所有被触发改动的 MutationRecrod 对象数组;另一个是调用该函数的 MutationObserver 对象。

示例

js
const oWrapper = document.querySelector('#J_wrapper') function callback (mutationList, observer) { console.log(mutationList, observer) } const observer = new MutationObserver(callback) // childList、attributes 和 characterData 三个属性必须有一个为 true observer.observe(oWrapper, { childList: true, // 观察目标子节点的变化,是否有添加或者删除,无默认值 attributes: true, // 观察属性变动,无默认值 characterData: true // 观察指定目标节点或子节点树中节点所包含的字符数据的变化,无默认值 }) oWrapper.textContent = 'Hello world!'

process.nextTick

这个将放在 Node.js 的 Event Loop 里面说明