zoukankan      html  css  js  c++  java
  • 解读setTimeout, promise.then, process.nextTick, setImmediate的执行顺序

    最近在看《Node.js调试指南》的时候遇到有意思的几道题,是关于setTimeout, promise.then, process.nextTick, setImmediate的执行顺序。今天抽空记录下这道题的分析过程及背后的原理与知识点。
    题目如下:

    // 题目一:
    setTimeout(()=>{
        console.log('setTimeout')
    },0)
    setImmediate(()=>{
        console.log('setImmediate')
    })
    // 题目二:
    const promise = Promise.resolve()
    promise.then(()=>{
        console.log('promise')
    })
    process.nextTick(()=>{
        console.log('nextTick')
    })
    // 题目三:
    setTimeout (() => { 
      console.log(1)
    },0)
    new Promise((resolve,reject) => { 
      console.log(2)
      for(let i = 0; i <10000; i++) {
          i === 9999 && resolve()
      }         
      console.log(3) 
    }).then(() => { 
      console.log(4)
    })
    console.log(5)
    // 题目四
    setInterval(()=>{
        console.log('setInterval')
    },100)
    process.nextTick(function tick(){
        process.nextTick(tick)
    })
    

    在分析这几道题之前先有必要了解下node.js的事件循环

    事件循环 Event Loop

    我们可以简单理解Event Loop如下:

    1. 所有任务都在主线程上执行,形成一个执行栈(Execution Context Stack)
    2. 在主线程之外还存在一个任务队列(Task Queen),系统把异步任务放到任务队列中,然后主线程继续执行后续的任务
    3. 一旦执行栈中所有的任务执行完毕,系统就会读取任务队列。如果这时异步任务已结束等待状态,就会从任务队列进入执行栈,恢复执行
    4. 主线程不断重复上面的第三步
      上面第三步中的读取任务队列包括以下6个阶段
    • timers:执行setTimeout()和setInterval()中到期的callback
    • I/O callbacks:上一轮循环中有少数的I/O callback会被延迟到这一轮的这一阶段
    • idle,prepare:仅内部调用
    • poll:最重要的阶段,执行I/O callback,在某些条件下node会阻塞在这个阶段
    • check:执行setImmediate()的callback
    • close callbacks:执行close事件的callback,例如socket.on('close',func)

    每个阶段都有一个FIFO的回调队列,当Event Loop执行到这个阶段时,就会从当前阶段的队列里拿出一个任务放到执行栈中执行,在队列任务清空或者执行的回调数量达到上限后,Event Loop就会进入下一个阶段

    poll阶段

    poll阶段主要有两个功能,如下所述:

    1. 当timers的定时器到期后,执行定时器(setTimeout和setInterval)的callback
    2. 执行poll队列里面的I/O callback
      如果Event Loop进入了poll阶段,且代码未设定timer,则可能发生以下的情况:
    • 如果poll queue不为空,则Event Loop将同步执行queue里的callback,直至queue为空,或者执行的callback达到系统上限
    • 如果poll queue为空,则可能发生以下情况:
      • 如果代码中使用了setImmediate(),则Event Loop将结束poll阶段并进入check阶段,执行check阶段的代码
      • 如果代码中没有使用setImmediate(),则Event Loop将阻塞在该阶段,等待callback加入poll queue,如果有callback进来则立即执行

    一旦poll queue为空,则Event Loop将检查timers,如果有timer的时间到期,则Event Loop将回到timers阶段,然后执行timer queue

    事件循环原理

    1. node 的初始化
      1. 初始化 node 环境。
      2. 执行输入代码。
      3. 执行 process.nextTick 回调。
      4. 执行 microtasks。
    2. 进入 event-loop
      1. 进入 timers 阶段

        • 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行。
        • 检查是否有 process.nextTick 任务,如果有,全部执行。
        • 检查是否有microtask,如果有,全部执行。
        • 退出该阶段。
      2. 进入IO callbacks阶段。

        • 检查是否有 pending 的 I/O 回调。如果有,执行回调。如果没有,退出该阶段。
        • 检查是否有 process.nextTick 任务,如果有,全部执行。
        • 检查是否有microtask,如果有,全部执行。
        • 退出该阶段。
      3. 进入 idle,prepare 阶段:

        • 这两个阶段与我们编程关系不大,暂且按下不表。
      4. 进入 poll 阶段

        • 首先检查是否存在尚未完成的回调,如果存在,那么分两种情况。
          • 第一种情况:
            • 如果有可用回调(可用回调包含到期的定时器还有一些IO事件等),执行所有可用回调。
            • 检查是否有 process.nextTick 回调,如果有,全部执行。
            • 检查是否有 microtaks,如果有,全部执行。
            • 退出该阶段。
          • 第二种情况:
            • 如果没有可用回调。
            • 检查是否有 immediate 回调,如果有,退出 poll 阶段。如果没有,阻塞在此阶段,等待新的事件通知。
        • 如果不存在尚未完成的回调,退出poll阶段。
      5. 进入 check 阶段。

        • 如果有immediate回调,则执行所有immediate回调。
        • 检查是否有 process.nextTick 回调,如果有,全部执行。
        • 检查是否有 microtaks,如果有,全部执行。
        • 退出 check 阶段
      6. 进入 closing 阶段。

        • 如果有immediate回调,则执行所有immediate回调。
        • 检查是否有 process.nextTick 回调,如果有,全部执行。
        • 检查是否有 microtaks,如果有,全部执行。
        • 退出 closing 阶段
      7. 检查是否有活跃的 handles(定时器、IO等事件句柄)。

        • 如果有,继续下一轮循环。
        • 如果没有,结束事件循环,退出程序。

    通过上面的事件循环的介绍我们已经知道setTimeout setImmediate的执行机制,但是并没有介绍process.nextTick()和promise.then()。这里我们还需要知道宏任务与微任务的概念

    宏任务 Macrotask

    宏任务是指Event Loop在每个阶段执行的任务
    宏任务包括 script (整体代码),setTimeout, setInterval, setImmediate, I/O, UI renderin

    微任务 Microtask

    微任务是指Event Loop在每个阶段之间执行的任务
    微任务包括 process.nextTick, Promise.then,Object.observe,MutationObserver

    宏任务与微任务执行顺序图

    图中绿色小块表示Event Loop的各个阶段,执行的是宏任务,粉色箭头表示执行的是微任务

    了解到这里我们再来分析上面的几道题
    题目一的执行结果是:

    setTimeout
    setImmediate
    //或者
    setImmediate
    setTimeout
    

    为什么结果不确定呢?我们知道setTimeout的回调函数在timer阶段执行,setImmediate的回调函数在check阶段执行。但是从事件循环开始到timer阶段会消耗一定的时间,所以会出现两种情况:

    1. 若timer前的准备时间超过1ms,则执行timer阶段(setTimeout)的回调函数
    2. 若timer前的准备时间少于1ms,则执行check阶段(setImmediate)的回调函数,下次event loop循环在执行timer阶段的函数

    题目二的执行结果是

    nextTick
    promise
    

    这里虽然和process.nextTick一样,promise.then也将回调函数注册到microtask,但process.nextTick的microtask queue总是优先于promise的microtask queue执行的
    题目三的执行结果是

    2
    3
    5
    4
    1
    

    Promise构造函数是同步执行的,所以先打印2,3,在打印5,接下来事件循环执行微任务执行promise.then的回调,打印4,然后进入下一个事件循环执行timer阶段的回调打印1

    题目四的执行结果是
    永远不会打印setInterval

    process.nextTick会无限循环,将event loop阻塞在microtask阶段,导致event loop上其他macrotask阶段的回调函数没有机会执行
    解决方法通常是用setImmediate代替process.nextTick.
    在setImmediate内执行setImmedaite时会将immediate注册到下一次event loop的check阶段,这样其他macrotask就有机会执行

    至此终于将node.js事件循环宏任务与微任务分析清楚了

  • 相关阅读:
    命令行程序如何获取HINSTANCE?
    解决C++项目使用sqlite中文乱码问题
    第三章 CLR如何解析引用类型
    第二章 生成、打包、部署和管理应用程序及类型
    第一章 CLR执行模型
    如何快速提升自己硬实力
    前端优化
    Eureka的工作原理以及它与ZooKeeper的区别
    单链表反转
    链表中head->next = p;和p=head->next;之间的区别
  • 原文地址:https://www.cnblogs.com/jesse131/p/11708233.html
Copyright © 2011-2022 走看看