Event loop

162 阅读7分钟

问题描述如下:

    setTimeout(() => console.log(1));

    setImmediate(() => console.log(2));

    process.nextTick(() => console.log(3));

    Promise.resolve().then(() => console.log(4));

    (() => console.log(5))();
输出结果为:
5

3

4

1

2

如果只有宏任务,在事务循环的机制下,我们并不能准确的获取宏任务的时机,宏任务触发时机会受到任务队列是否阻塞,而微任务的出现的解决了宏任务执行时机不可控的问题

原理与解释: Event loop可以简单理解为:

  • 所有任务都在主线程上执行,形成一个执行栈(execution context stack)。

  • 主线程之外,还存在一个"任务队列"(task queue)。系统把异步任务放到"任务队列"之中,然后主线程继续执行后续的任务。

  • 一旦"执行栈"中的所有任务执行完毕,系统就会读取"任务队列"。如果这个时候,异步任务已经结束了等待状态,就会从"任务队列"进入执行栈,恢复执行。

  • 主线程不断重复上面的第三步。

  • eventLoop中的任务队列 有 macrotask(宏任务) 和 microtask(微任务)两个概念, 这表示异步任务的两种分类。 在挂起任务时, JS引擎会将所有任务按照分类分到这两个队列中,首先在macrotask的队列中取出第一个任务,执行完毕后取出microtask队列中的所有任务顺序执行,之后再取macrotask任务,周而复始,直至两个队列的任务都取完。

  • macro-task(宏任务): script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering;

  • micro-task(微任务): process.nextTick, Promises(这里指浏览器实现的原生 Promise),Object.observe, MutationObserver

首先注意: 全部代码(script)算是一个macrotask

Event loop有六个阶段,这些阶段回一次执行 分别是:

timers
I/O callbacks
idle, prepare
poll
check
close callbacks

每个阶段都有一个先进先出的回调函数队列,只有一个阶段的回调函数队列清空了,该执行的会点函数都执行了,事件循环才会进入下一个阶段。

Event Loop图如下:

每个阶段的含义如下:

(1)timers 这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。

(2)I/O callbacks 除了以下操作的回调函数,其他的回调函数都在这个阶段执行。

setTimeout()和setInterval()的回调函数
setImmediate()的回调函数
用于关闭请求的回调函数,比如socket.on('close', ...)

(3)idle, prepare

该阶段只供 libuv 内部调用,这里可以忽略。

(4)Poll

这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。

这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。

(5)check

该阶段执行setImmediate()的回调函数。

(6)close callbacks

该阶段执行关闭请求的回调函数,比如socket.on('close', ...)。

题目发散与追问

(1)setTimeout与同步执行的区别?setTime为0为何还在当前执行上下文的后面?

  • setTimeout的运行机制是, 将自定的代码移出本次执行,放到任务队列中,等到下一轮Event Loop时,再检查是否到了指定时间,如果不到就等到下一轮Event Loop时重新判断。 这意味着,setTimeout指定的代码必须等到本次执行的所有代码都执行完,才会执行。 即:setTimeout的真正作用是,在“任务队列”的现有事件的后面再添加一个事件,规定在指定时间执行某段代码。所以即使为0 也会在当前执行上下文的后面执行。

同步任务 是在执行栈中执行,先于异步任务的“任务队列”。

(2)resolve的原理是怎么实现的?在没有异步操作时,为何会在同步语句之后却在setTimeout之前?自己尝试写一个Promise对象,里面也有resolve方法,该怎么实现?

  • resolve的实现原理: promise里面的then函数仅仅是注册了后续需要执行的代码,真正的执行实在resolve方法里执行的。

Promises/A+ 规范明确要求回调 需要通过异步的方式执行,用以保证一致可靠的执行顺序。 所以resolve执行前,then方法已经注册完所有的回调

Promise 有三种状态:

pending: 初始状态, 非 fulfilled 或 rejected. fulfilled: 成功的操作. rejected: 失败的操作. 使用 new Promise(function(resolve, reject){...} )实例化 Promise 时,去改变promise的状态,是执行 resolve() 或 reject()方法,那么,resolver的两个参数分别是成功的操作函数和失败的操作函数。

执行完这段代码将不会有任何输出,但是promise的状态发生了改变,也就是this.status由 pending变成了 resolved(fulfilled)

在resolved执行更改状态后,继续执行then方法里注册的代码。

resolve的实现代码如下:

// 用于执行 new Promise(function(resolve, reject){}) 中的resove或reject方法
function executeResolver(resolver){
    //[标准 2.3.3.3.3] 如果resove()方法多次调用,只响应第一次,后面的忽略
    var called = false, _this = this;
    function onError(value) {
        if (called) { return; } 
        called = true;
        //[标准 2.3.3.3.2] 如果是错误 使用reject方法
        executeCallback.bind(_this)('reject', value);
    }
    function onSuccess(value) {
        if (called) { return; }
        called = true;
        //[标准 2.3.3.3.1] 如果是成功 使用resolve方法
        executeCallback.bind(_this)('resolve', value);
    }
    // 使用try...catch执行
    //[标准 2.3.3.3.4] 如果调用resolve()或reject()时发生错误,则将状态改成rejected,并将错误reject出去
    try{
        resolver(onSuccess, onError);
    }catch(e){
        onError(e);
    }
}

在execcuteCallback中执行 then注册在callbackQueue 中的方法。

在没有异步操作时,为何会在同步语句之后却在setTimeout之前?

  • eventLoop中的任务队列 有 macrotask(宏任务) 和 microtask(微任务)两个概念, 这表示异步任务的两种分类。 在挂起任务时, JS引擎会将所有任务按照分类分到这两个队列中,首先在macrotask的队列中取出第一个任务,执行完毕后取出microtask队列中的所有任务顺序执行,之后再取macrotask任务,周而复始,直至两个队列的任务都取完。

  • macro-task(宏任务): script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering;

  • micro-task(微任务): process.nextTick, Promises(这里指浏览器实现的原生 Promise),Object.observe, MutationObserver 首先注意: 全部代码(script)算是一个macrotask

  • 第一步: 执行全部代码这个macrotask, 执行过程中,创造了新的macrotask(setTimeout(function() {console.log(2)}, 0)); 还创建了一个microtask(Promise.resolve(3).then(function(val){console.log(val)}));中的console.log(val);这两个任务被挂起

  • 第二步: 执行microtask,将microtask queue 中的所有任务取出,

  • 第三步:重复,浏览器再执行一个macrotask,从macrotask queue 下一个任务取出执行

最终所有队列被清空,代码执行完毕。

所以promise会在setTimeout之前,promise.resolve()中的代码是同步的

(3)关于timeout与immediate的区别,根据名称和定义immediate是立即应该在前,timeout在后,但有时在前有时在后。这里的机制究竟是怎样的?

  • timeout和immediate,根据eventloop的定义,event执行顺序是timer -> IO callback -> poll阶段 -> check阶段 -> close阶段,执行调用时依次循环执行,每个阶段都有执行队列,且有一个最大执行队列的阈值(这个地方能保证不会程序不会永久的停留在某个阶段), timeout属于timer阶段,setimmediate阶段属于check阶段,程序运行之后,系统初始化event loop队列,执行到poll阶段会处于等待状态,直至有新的timer或IO回调或immediate加入,所以看见的是大部分情况是immediate会优先于timeout执行,但是如果event loop在check流程之后,这时按照流程执行顺序,会先执行timer阶段再执行check阶段,因此也会出现timeout执行在前

github.com/lgwebdream/…

juejin.cn/post/720065…