JavaScript之Event Loop


一、事件循环机制

  1. JS引擎逐行扫描JS代码,遇到同步任务会将其添加到JS引擎线程的执行栈中。
  2. 遇到异步任务,交由浏览器渲染进程中的相应线程(比如setTimeout交给定时器线程http请求交给异步网络请求线程)管理,当异步任务有了运行结果,事件触发线程就会将异步任务的事件回调依次放进任务队列的末尾。该线程维护一个微任务队列和一个宏任务队列
  3. 执行栈中的同步任务执行完之后,JS引擎会从宏任务队列队首取一个宏任务,和微任务队列中到执行栈里,直到微任务队列为空。
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 微任务执行完成,GUI线程开始渲染工作
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务,依次循环

一旦主线程执行栈中的所有同步任务执行完毕,主线程就会读取任务队列中的任务添加到执行栈中 ,继续开始执行,执行完毕后,就会再去任务队列中读取,这种循环往复的执行机制就是事件循环

示例:

let setTimeoutCallBack = function() {
  console.log('定时器回调');
};
let httpCallback = function() {
  console.log('http请求回调');
}

// 同步任务
console.log('同步任务1');

// 异步定时任务
setTimeout(setTimeoutCallBack, 1000);

// 异步http请求任务
ajax.get('/info', httpCallback);

// 同步任务
console.log('同步任务2');

代码执行过程:

  1. 执行console.log('同步任务1')
  2. 执行setTimeout(setTimeoutCallBack, 1000);,会将该异步任务移交给定时器线程,定时器线程在1s 后将setTimeoutCallBack 这个回调交给事件触发线程处理,事件触发线程收到回调后把它加入到事件触发线程所管理的任务队列中等待执行。
  3. 执行ajax.get('/info', httpCallback);,会将该异步任务移交给异步http请求线程,http请求线程在请求结束后将httpCallback这个回调交由事件触发线程处理,事件触发线程收到 回调后把它加入到事件触发线程所管理的事件队列中等待执行。
  4. 执行console.log('同步任务2')
  5. 至此执行栈中的任务执行完毕,JS引擎线程已经空闲,开始向事件触发线程发起询问,询问事件触发线程的任务队列中是否有需要执行的回调函数,如果有将任务队列中的回调加入执行栈中,开始执行回调;如果任务队列中没有回调,JS引擎线程会一直发起询问,直到有为止。

二、宏任务 & 微任务

1.宏任务(macrotask|task)

本质: 宿主(Node或浏览器)发起的任务。
主要包含:

  • 整体代码script
  • setTimeout
  • setInterval
  • I/O
  • UI交互事件
  • postMessage
  • MessageChannel(Web Worker)
  • setImmediate(setTimeout(fn, 0))

2.微任务(microtask)

本质:Javascript 引擎发起的任务。
主要包含:

  • process.nextTick(Node.js)
  • Promise.then
  • catchfinally
  • MutationObserver(DOM变化观察器)

任务队列中分为宏任务队列微任务队列,每执行一次任务都可能注册新的宏任务或微任务到相应的任务队列中,遵循每执行一个宏任务,就会清空一次任务队列中的所有微任务这一循环规则。
大致过程如下:

  • 一开始整个脚本作为一个宏任务执行
  • 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 微任务执行完成,GUI线程开始渲染工作
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务,依次循环

三、示例代码

示例1

setTimeout(function(){
    console.log('setTimeout')
});

new Promise(function(resolve){
    console.log('pro');
    for(var i = 0; i < 10000; i++){
        i == 99 && resolve();
    }
}).then(function(){
    console.log('then')
});

console.log('end');

// 打印结果
pro
end
then 
setTimeout

代码执行过程:

  1. 整段代码作为宏任务执行,遇到setTimeout()分配到宏任务队列中;
  2. 遇到new Promise,其中的代码为同步代码,立即执行。打印pro
  3. then的回调分配到微任务队列中;
  4. 遇到同步代码console.log,打印end
  5. 至此主代码宏任务执行结束;
  6. 将微任务队列中的代码提取到主线程开始执行,打印then
  7. 微任务队列中执行完毕后,读取宏任务队列中的下一个宏任务开始执行,打印setTimeout

示例2

微任务和宏任务嵌套创建:

console.log('1');

// 记作 set1
setTimeout(function () {
    console.log('2');
    // set4
    setTimeout(function() {
        console.log('3');
    });
    // pro2
    new Promise(function (resolve) {
        console.log('4');
        resolve();
    }).then(function () {
        console.log('5')
    })
})

// 记作 pro1
new Promise(function (resolve) {
    console.log('6');
    resolve();
}).then(function () {
    console.log('7');
    // set3
    setTimeout(function() {
        console.log('8');
    });
})

// 记作 set2
setTimeout(function () {
    console.log('9');
    // 记作 pro3
    new Promise(function (resolve) {
        console.log('10');
        resolve();
    }).then(function () {
        console.log('11');
    })
})

// 代码打印结果
1
6
7
2
4
5
9
10
11
8
3

代码执行过程:

  1. 整体代码作为第一个宏任务进入执行栈中开始执行。打印1

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    整体js代码
  2. 遇到set1, 将set1回调分发到宏任务队列中。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    整体js代码 set1回调
  3. 遇到pro1,打印6,将pro1的then回调分发至微任务队列。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    整体js代码 set1回调 pro1的then回调
  4. 遇到set1, 将set2回调分发到宏任务队列中。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    整体js代码 set1回调 pro1的then回调
    set2回调
  5. 整体js代码宏任务至此执行完毕,js引擎读取微任务队列到执行栈中。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    pro1的then回调 set1回调
    set2回调
  6. pro1的then回调读取到执行栈中,打印7,将set3的回调分发至宏任务队列。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    pro1的then回调 set1回调
    set2回调
    set3回调
  7. pro1的then回调执行完毕,且微任务队列中已经无任务,读取宏任务队列,将set1回调宏任务添加到执行栈中。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    set1回调 set2回调
    set3回调
  8. 打印2,将set4回调分发至宏任务队列;遇到pro2,打印4,将pro2的then回调分发至微任务队列。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    set1回调 set2回调 pro2的then回调
    set3回调
    set4回调
  9. set1回调宏任务执行完毕,读取微任务队列到执行栈中。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    pro2的then回调 set2回调
    set3回调
    set4回调
  10. 执行pro2的then回调,打印5。执行完毕后,读取下一个宏任务set2回调到执行栈中。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    set2回调 set3回调
    set4回调
  11. 打印9,执行pro3,打印10,将pro3的then回调分发到微任务队列中。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    set2回调 set3回调 pro3的then回调
    set4回调
  12. set2回调执行完毕,将微任务队列中的pro3的then回调添加到执行栈中。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    pro3的then回调 set3回调
    set4回调
  13. 打印11pro3的then回调执行完毕,将宏任务队列中的set3回调添加到执行栈中。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    set3回调 set4回调
  14. 打印8set3回调执行完毕,将宏任务队列中的set4回调添加到执行栈中。

    主线程 Event Queue
    执行栈 宏任务队列 微任务队列
    set4回调
  15. 打印3

示例3

async function async1() {
  console.log(1);
  const result = await async2();
  console.log(3);
}

async function async2() {
  console.log(2);
}

// 记作pro
Promise.resolve().then(() => {
  console.log(4);
});

// 记作set
setTimeout(() => {
  console.log(5);
});

async1();
console.log(6);

// 打印结果
1
2
6
4
3
5

代码执行流程:

  1. 执行pro,将pro的then回调分发到微任务队列中;
  2. 执行set,将set回调添加到宏任务队列;
  3. 执行async1(),打印1
  4. 执行const result = await async2(),打印2;同时将console.log(3)分发到微任务队列;
  5. 继续执行主线程中的代码,打印6;
  6. 执行微任务队列中的回调,依次打印43;
  7. 执行宏任务队列中的回调,打印5

示例4

async function t1 () {
    console.log(1)
    //  记作pro2
    new Promise(function(resolve) {
        console.log('promise3')
        resolve();
    }).then( function () {
        console.log('promise4')
    })
    
    // 记作pro3
    await new Promise(function(resolve) {
        console.log('b')
        resolve();
    }).then( function() {
        console.log('t1p')
    })

    // 记作await pro3
    console.log(2)
    // 记作pro5
    new Promise(function(resolve) {
        console.log('promise5')
        resolve();
    }).then(function() {
        console.log('promise6')
    })
}

// 记作set
setTimeout(function() {
    console.log('setTimeout')
}, 0)

async function t2() {
    console.log(3);
    // 记作pro4
    await Promise.resolve().then(() => console.log('t2p'));
    
    // 记作await pro4
    console.log(4);
}

t1();

// 记作pro1
new Promise(function(resolve) {
    console.log('promise1')
    resolve();
}).then(function() {
    console.log('promise2')
})

t2();

console.log('end');

// 打印结果
1 
promise3 
b 
promise1 
3
end
promise4
t1p
promise2
t2p
2
primise5
4
promise6
setTimeout

代码执行流程:

  1. set回调进到宏任务队列;

  2. 执行t1(),打印1,打印promise3,将pro2的then回调放到微任务队列;

  3. 打印b,将pro3的then回调放到微任务队列,因为await,之后的代码在pro3的then回调执行后再执行;

  4. 打印promise1,将pro1的then回调放到微任务队列;

  5. 执行t2(),打印3,将pro4的then回调放到微任务队列,pro4之后的代码暂不执行;

  6. 打印end。此时的微任务队列为:

    微任务队列
    pro2的then回调
    pro3的then回调
    pro1的then回调
    pro4的then回调
  7. 接着开始依次执行微任务队列中的回调。执行pro2的then回调,打印promise4

  8. 执行pro3的then回调,打印t1p,将await pro3代码块放到微任务队列;

  9. 执行pro1的then回调,打印promise2

  10. 执行pro4的then回调,打印t2p,将await pro4代码放到微任务队列;

  11. 执行await pro3代码块,打印2,打印promise5,将pro5的then回调放到微任务队列;

  12. 执行await pro4,打印4

  13. 执行pro5的then回调,打印promise6

  14. 微任务队列代码执行完毕,读取宏任务队列,执行set回调,打印setTimeout

知识点总结
async/await本质上还是基于Promise的一些封装, 可以理解为,await 以前的代码,相当于与 new Promise 的同步代码,await 以后的代码相当于 Promise.then的回调。
await之后的代码必须等await语句执行完成后(包括微任务完成),才能执行后面的,也就是说,只有运行完await语句,才把await语句后面的全部代码加入到微任务队列。

四、参考文章


文章作者: Snail-Lu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Snail-Lu !
  目录