速通EventLoop事件循环机制

100 阅读6分钟

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

进程和线程

  • 进程:OS(操作系统)进行资源分配和调度的最小单位,有独立的内存空间
    • 运行程序时相当开启了一个进程
    • 浏览器中打开的一个标签页就是一个进程
  • 线程:OS 进行运算调度的最小单位,共享进程内存空间
  • 一个线程在同一时刻只能执行一个任务
  • 如果只有一个线程,当前任务未执行完,下一任务无法执行,这就叫做同步
  • 如果有多个线程,同一时刻可以执行多个任务,下一任务可以交由别的线程执行,这就叫做异步
  • 浏览器是多线程的
    • GUI 渲染线程:渲染和解析页面
    • JS 引擎线程:渲染和解析 JS【浏览器分配了一个线程去解析 JS,所以 JS 是单线程的】
    • 定时器监听线程
    • 事件监听线程
    • HTTP 网络请求线程【同源下,浏览器最多同时分配 5~7 个 HTTP 线程】
    • WebWorker
    • ...
  • JS 是单线程运行的,所以其中大部分代码都是“同步”的(例如:循环)
  • JS 中也有部分异步操作的代码
    • 异步微任务
      • requestAnimationFrame
      • Promise.then/catch/finally
      • async/await
      • queueMicrotask 手动创建一个异步微任务
      • MutationObserver 监听当前 DOM 元素的改变
      • IntersectionObserver 监听当前 DOM 元素和可视区域交叉信息改变
      • process.nextTick Node 特有,在异步微任务中优先级最高
    • 异步宏任务
      • setTimeout/setInterval 定时器
      • 事件绑定【也叫事件队列】
      • 网络请求【XMLHttpRequest/Fetch】
      • MessageChannel 消息队列
      • setImmediate Node 特有的定时器,在异步宏任务中优先级最低
  • JS 中的异步操作是借用浏览器的多线程机制,再基于EventLoop 事件循环机制实现单线程异步效果

定时器

我们给定时器的延时时间设为 0,也不会立即执行定时器的回调函数,而是要等待 4ms 以上【浏览器的最快处理时间】

setTimeout(() => {
    console.log(1);
}, 0);

EventLoop

浏览器加载页面,会开辟堆栈内存,还会创建两个队列

  • WebAPI 任务队列:监听异步的任务是否可以执行
  • EventQueue 事件/任务队列:所有可以执行的异步任务,需要在这里排队等待执行
    • EventQueue 分为异步微任务队列和异步宏任务队列

EventLoop 事件循环机制:

  • 主线程自上而下执行代码的过程中,如果遇到“异步代码”,会把异步任务放到 WebAPI 中进行监听
    • 浏览器会开辟新的线程去监听异步任务是否可以执行
    • 就不会阻塞主线程的代码解析,继续向下执行代码
  • 当异步任务被监听为可执行时,不会立即执行,而是将其放到 EventQueue 中排队等待执行
    • 根据是异步微任务还是异步宏任务,放到不同的队列中
    • 队列是先入先出
    • PS:对于定时器来说,设置一个等待时间,到时间后并不一定会立即执行
  • 当同步代码(同步宏任务)都执行完毕,主线程空闲时,会去 EventQueue 中正在的排队的异步任务,按照顺序取出放到主线程执行 - 异步微任务的优先级高于异步宏任务,所以永远先执行异步微任务队列中任务 - 同样级别的任务(异步微任务和异步微任务,异步宏任务和异步宏任务),谁先进入到任务队列中,谁先执行 - 代码都是在栈中由主线程执行的,所以只要取出来的异步任务没有执行完毕,就不会再拿其他的任务来执行

event-loop

Promise 的 EventLoop

new Promise 里的函数是同步的,会立即执行,resolve 和 reject 调用是异步的

let p = new Promise((resolve, reject) => {
    console.log(1);
    resolve(100);
});
p.then((res) => {
    console.log(2);
});
console.log(3);

// 输出:1  3  2
  1. 情况 1:p.then(onfulfilled, onrejected),已知 Promise 实例 p 的状态和值,但是不会立即执行 onfulfilled / onrejected,而是先进入 WebAPI 中,监听到状态变为成功,则 onfulfilled 可以被执行了,将其放入到 EventQueue 中(异步微任务队列)

    let p = new Promise((resolve) => {
        resolve(1);
    });
    p.then((value) => {
        console.log("成功", value);
    });
    console.log(2);
    // 输出 2  成功1
    
  2. 情况 2:如果 Promise 实例 p 的状态是未知的,则把 onfulfilled/onrejected 存起来【可以简单的理解为放到 WebAPI 进行监听,只有知道实例的状态,才可以执行】,resolve/reject 执行会立即改变实例的状态和值,也决定了 WebAPI 中监听的 onfulfilled/onrejected 最终哪一个去执行(放到 EventQueue 中,异步微任务队列),等到主线程的同步代码执行完毕再取出来执行

    let p = new Promise((resolve) => {
        setTimeout(() => {
            resolve(10);
            console.log(2);
        }, 1000);
    });
    p.then((value) => {
        console.log("成功", value);
    });
    console.log(1);
    // 输出 1  2  成功10
    
  3. 情况 3:第一个.then 返回一个新的 Promise 实例 p2,代码执行到第一个 then 时先放入 WebAPI 监听到状态成功,放入到 EventQueue 中(异步微任务队列)排队等待执行,主线程同步代码继续往下执行,执行到第二个 then 时,将他放入 WebAPI 监听状态的改变,但是 p2 的状态是由第一个 then 决定的,所以他先不进入 EventQueue,主线程同步代码执行完毕,取出第一个 then 执行,一执行则立即改变 p2 的状态,WebAPI 监听到 p2 状态的改变,把第二个 then 放入 EventQueue,主线程执行第一个 then 完毕后就取第二个 then 执行

    Promise.resolve(1)
        .then((res) => {
            console.log(res);
            return 2;
        })
        .then((res) => {
            console.log(res);
        });
    // 输出  1  2
    
  4. 情况 4:async/await

    • 当执行到 await 时,会立即执行后面的代码,看返回的 Promise 实例(如果不是 Promise 实例也会变成 Promise 实例)的状态否成功
    • 会把当前上下文,await 下面的代码当做异步微任务
      • 放入到 WebAPI 监听,只有实例的状态改变了,表示可以执行
      • 可以执行了则放入 EventQueue 中排队等待执行
    const fn = async () => {
        console.log(1);
        return 10;
    };
    
    (async function () {
        /**
         * 遇到await 后面的代码立即执行,fn()立即执行
         * 因为被await修饰了,fn执行完返回一个状态成功值是10的Promise实例p
         * 当前上下文,await下面的代码不会立即执行而是会创建一个异步微任务,
         * 放入到WebAPI监听实例p的状态改变
         * 状态是成功放入到EventQueue排队等待执行
         * 我们可以简单的理解为
         * p.then(() => {
         *    console.log(2, res);
         * })
         */
        let res = await fn();
        console.log(2, res);
    })();
    
    console.log(3);
    // 输出1  3  210
    

练习

练习一

setTimeout(() => {
    console.log(1);
}, 20);
console.log(2);
setTimeout(() => {
    console.log(3);
}, 10);
console.log(4);
for (let index = 0; index < 100000000; index++) {
    // 循环花费80ms以上
}
console.log(5);
setTimeout(() => {
    console.log(6);
}, 8);
console.log(7);
setTimeout(() => {
    console.log(8);
}, 15);
console.log(9);

//输出: 2  4  5  7  9  3  1  6  8

练习二

async function asyncFn1() {
    console.log("asyncFn1 start");
    await asyncFn2();
    console.log("asyncFn1 end");
}
async function asyncFn2() {
    console.log("asyncFn2");
    // 返回状态成功值为undefined的Promise实例
}
console.log("script start");
setTimeout(() => {
    console.log("setTimeout");
}, 0);

asyncFn1();
new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});
console.log("script end");

/**
 * script start
 * asyncFn1 start
 * asyncFn2
 * promise1
 * script end
 * asyncFn1 end
 * promise2
 * setTimeout
 */

练习三

let body = document.body;
body.addEventListener("click", function () {
    Promise.resolve().then(() => {
        console.log(1);
    });
    console.log(2);
});
body.addEventListener("click", function () {
    Promise.resolve().then(() => {
        console.log(3);
    });
    console.log(4);
});

// 2 1 4 3

nodejs 的 EventLoop

  1. 执行同步代码
  2. 执行微任务代码(process.nextTick 优先级最高)
  3. 按顺序执行 6 个类型的宏任务(每个宏任务开始前要检查微任务队列是否有新加入的任务,有则先执行微任务,执行完再执行宏任务。宏任务中 setImmediate 优先级最低)

推荐使用 setImmediate 代替 process.nextTick

扩展

Vue 的 nextTick 经历了微任务 -> 宏任务 -> 微任务和宏任务并行 -> 微任务