JavaScript之事件循环

61 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第10天,点击查看活动详情

前言

JavaScript 是一门 轻量级解释型(单线程) 语言,单线程也就意味着 在相同的一个时刻下,只会执行一个任务。但是实际开发中会发现 JavaScript 执行时并不会因为网络请求、定时器等操作影响后续代码的执行。

JS 在浏览器中运行时,console.log('3') 语句并不会在 setTimeout 的回调函数执行完毕后才执行。

console.log('1');
console.log('2');
setTimeout(() => {
    console.log('4');
}, 1000)
console.log('3');

同步(阻塞)和异步(非阻塞)

同步(阻塞)JS 作为一门单线程语言,最大的特点就是:代码 从上到下逐行 解释和运行。也就是不会出现 先执行 行3 的代码,再执行 行1 的代码

:下面的代码执行时,console.log('end') 语句会被 while 循环语句阻塞,直到循环结束才会执行。

let i = 1000;
while(i >= 0) {
    console.log(i);
    i --;
}
console.log('end')

异步(非阻塞)异步 模型简单来说就是代码执行顺序并不一定按照 从上到下 的规则执行。

想一个问题:如果 JS 在浏览器中运行也是按照同步模型的规则运行,那么任何耗时操作 (如:定时器、网络请求) 都有可能影响页面的渲染,这对用户体验的影响是显而易见的。

好在浏览器是 多线程 的,它定义了 同步任务异步任务,当浏览器引擎解释 异步任务 时,会先将其 挂起,然后按照 特定的顺序 依次取出来执行。

浏览器线程

浏览器包含下列几个线程:

  • JavaScript 引擎线程
  • GUI 渲染线程
  • 事件触发线程
  • 定时器触发线程
  • 网络线程
  • 其它...

其中 JavaScript 引擎线程用来解释和执行 JS 代码,GUI 渲染线程主要负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。

注意: 两个线程 互斥,不可同时进行。因为 JS 中的代码可能会操作 DOM,影响 GUI 线程的渲染。

为了便于分析 JavaScript 的事件循环机制,将上列线程大致分为两个线程:

  1. JS 主线程:执行 JS 中的 同步任务
  2. 其它线程:主要用来处理 异步任务

JavaScript 中的事件循环(Event Loop)机制

注: 此处忽略 执行上下文、VO、scope chain、this 等概念,详见这篇文章

事件循环模型:

事件循环.png

栈:先进后出

队列:先进先出,先入队的任务先入栈执行

  • 主线程中会区分 同步代码异步代码
  • 对于 同步代码:加入 执行栈 中执行;
  • 异步代码 则交给其他线程挂起等待,当到达 异步代码 的执行时机,会将其加入 任务队列 中等待执行
  • 任务队列 中的任务会在主执行栈中的同步代码全部运行结束并出栈后依次进栈执行
  • ...

示例代码:


console.log(1); // 同步代码,执行,输出 1

setTimeout(() => { // 异步代码,挂起等待 0ms
    console.log(4);
}, 0)

Promise.resolve().then(() => { // 异步代码,挂起等待
    console.log(3) 
})

console.log(2); // 同步代码,执行,输出 2

// 最终输出结果:1 2 3 4

为什么 setTimeoutPromise 同样是 异步任务,并且例子中的 setTimeoutPromise 先挂起等待执行,但是 Promise 异步任务却先执行了?

请看下面对于 微任务宏任务 的相关介绍

微任务和宏任务

上述提到的 任务队列 其实要分为 微任务队列宏任务队列

简单来说,微任务宏任务 都是异步任务,区别在于 微任务 的优先级更高,每一个 宏任务 执行之前,都要先检查 微任务 队列是否为空,微任务 队列为空时才会执行下一个 宏任务

常见的宏任务和微任务:

1. 宏任务:setTimeoutsetIntervalrequestAnimationFrame、UI事件、postMessage

2. 微任务:PromiseMutationObserverrequestAnimationFrame

任务队列图示:

image.png

示例代码:

console.log(1); //  同步代码,执行,输出 1

setTimeout(() => { //  加入 宏任务 队列
    console.log(4)
    Promise.resolve().then(() => { //  加入 微任务 队列
        console.log(5)
    })
}, 0)

Promise.resolve().then(() => {  // 加入微任务队列
    console.log(3)
})

setTimeout(() => { //  加入 宏任务 队列
    console.log(6)
}, 0)

console.log(2) //  同步代码,执行,输出 2
  • 同步代码执行结束后:输出 1、2宏任务队列 加入两个 setTimeout 定时任务;微任务队列 加入一个 Promise 任务
  • 执行 微任务队列 中的 Promise 任务,输出 3
  • 执行第一个 setTimeout 宏任务,输出 4,产生一个新的 Promise ,加入到 微任务队列
  • 重点:此时 微任务队列 中存在一个待执行任务,先执行 微任务队列 的任务,待 微任务队列 清空后才会执行下一个 宏任务。输出 5
  • 最后执行 宏任务队列 中的 setTimeout 任务,输出 6

总结

JS 事件循环机制重点 在于理解同步任务和异步任务的执行时机,以及异步任务中微任务和宏任务的执行优先级。

巩固一下

下列代码输出结果是什么?

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