事件循环机制_js事件循环机制 - CSDN
精华内容
参与话题
  • 答题大纲先说基本知识点,宏任务、微任务有哪些说事件循环机制过程,边说边画图出来说async/await执行顺序注意,可以把 chrome 的优化,做法其实是违反了规范的,V8 团队的PR...

    答题大纲

    1. 先说基本知识点,宏任务、微任务有哪些

    2. 说事件循环机制过程,边说边画图出来

    3. 说async/await执行顺序注意,可以把 chrome 的优化,做法其实是违反了规范的,V8 团队的PR这些自信点说出来,显得你很好学,理解得很详细,很透彻。

    4. 把node的事件循环也说一下,重复1、2、3点,node中的第3点要说的是node11前后的事件循环变动点。

    下面就跟着这个大纲走,每个点来说一下吧~

    浏览器中的事件循环

    JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们称为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。

    macro-task大概包括:

    • script(整体代码)

    • setTimeout

    • setInterval

    • setImmediate

    • I/O

    • UI render

    micro-task大概包括:

    • process.nextTick

    • Promise

    • Async/Await(实际就是promise)

    • MutationObserver(html5新特性)

    整体执行,我画了一个流程图:

    GitHub

    总的结论就是,执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。举个例子:

    结合流程图理解,答案输出为:async2 end => Promise => async1 end => promise1 => promise2 => setTimeout 但是,对于async/await ,我们有个细节还要处理一下。如下:

    async/await执行顺序

    我们知道async隐式返回 Promise 作为结果的函数,那么可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。但是我们要注意这个微任务产生的时机,它是执行完await之后,直接跳出async函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到async函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中。我们来看个例子:

    console.log('script start')
    
    async function async1() {
    await async2()
    console.log('async1 end')
    }
    async function async2() {
    console.log('async2 end')
    }
    async1()
    
    setTimeout(function() {
    console.log('setTimeout')
    }, 0)
    
    new Promise(resolve => {
    console.log('Promise')
    resolve()
    })
    .then(function() {
    console.log('promise1')
    })
    .then(function() {
    console.log('promise2')
    })
    
    console.log('script end')
     // 旧版输出如下,但是请继续看完本文下面的注意那里,新版有改动
    // script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
    

    分析这段代码:

    • 执行代码,输出script start

    • 执行async1(),会调用async2(),然后输出async2 end,此时将会保留async1函数的上下文,然后跳出async1函数。

    • 遇到setTimeout,产生一个宏任务

    • 执行Promise,输出Promise。遇到then,产生第一个微任务

    • 继续执行代码,输出script end

    • 代码逻辑执行完毕(当前宏任务执行完毕),开始执行当前宏任务产生的微任务队列,输出promise1,该微任务遇到then,产生一个新的微任务

    • 执行产生的微任务,输出promise2,当前微任务队列执行完毕。执行权回到async1

    • 执行await,实际上会产生一个promise返回,即

    let promise_ = new Promise((resolve,reject){ resolve(undefined)})
    

    执行完成,执行await后面的语句,输出async1 end

    • 最后,执行下一个宏任务,即执行setTimeout,输出setTimeout

    注意

    新版的chrome浏览器中不是如上打印的,因为chrome优化了,await变得更快了,输出为:

    // script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
    

    但是这种做法其实是违法了规范的,当然规范也是可以更改的,这是 V8 团队的一个 PR ,目前新版打印已经修改。知乎上也有相关讨论,可以看看 https://www.zhihu.com/question/268007969

    我们可以分2种情况来理解:

    1. 如果await 后面直接跟的为一个变量,比如:await 1;这种情况的话相当于直接把await后面的代码注册为一个微任务,可以简单理解为promise.then(await下面的代码)。然后跳出async1函数,执行其他代码,当遇到promise函数的时候,会注册promise.then()函数到微任务队列,注意此时微任务队列里面已经存在await后面的微任务。所以这种情况会先执行await后面的代码(async1 end),再执行async1函数后面注册的微任务代码(promise1,promise2)。

    2. 如果await后面跟的是一个异步函数的调用,比如上面的代码,将代码改成这样:

    console.log('script start')
    
    async function async1() {
        await async2()
        console.log('async1 end')
    }
    async function async2() {
        console.log('async2 end')
        return Promise.resolve().then(()=>{
            console.log('async2 end1')
        })
    }
    async1()
    
    setTimeout(function() {
        console.log('setTimeout')
    }, 0)
    
    new Promise(resolve => {
        console.log('Promise')
        resolve()
    })
    .then(function() {
        console.log('promise1')
    })
    .then(function() {
        console.log('promise2')
    })
    
    console.log('script end')
    

    输出为:

    // script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout
    

    此时执行完awit并不先把await后面的代码注册到微任务队列中去,而是执行完await之后,直接跳出async1函数,执行其他代码。然后遇到promise的时候,把promise.then注册为微任务。其他代码执行完毕后,需要回到async1函数去执行剩下的代码,然后把await后面的代码注册到微任务队列当中,注意此时微任务队列中是有之前注册的微任务的。所以这种情况会先执行async1函数之外的微任务(promise1,promise2),然后才执行async1内注册的微任务(async1 end). 可以理解为,这种情况下,await 后面的代码会在本轮循环的最后被执行. 浏览器中有事件循环,node 中也有,事件循环是 node 处理非阻塞 I/O 操作的机制,node中事件循环的实现是依靠的libuv引擎。由于 node 11 之后,事件循环的一些原理发生了变化,这里就以新的标准去讲,最后再列上变化点让大家了解前因后果。

    node 中的事件循环

    浏览器中有事件循环,node 中也有,事件循环是 node 处理非阻塞 I/O 操作的机制,node中事件循环的实现是依靠的libuv引擎。由于 node 11 之后,事件循环的一些原理发生了变化,这里就以新的标准去讲,最后再列上变化点让大家了解前因后果。

    宏任务和微任务

    node 中也有宏任务和微任务,与浏览器中的事件循环类似,其中,

    macro-task 大概包括:

    • setTimeout

    • setInterval

    • setImmediate

    • script(整体代码)

    • I/O 操作等。

    micro-task 大概包括:

    • process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)

    • new Promise().then(回调)等。

    node事件循环整体理解

    先看一张官网的 node 事件循环简化图:

    图中的每个框被称为事件循环机制的一个阶段,每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。

    因此,从上面这个简化图中,我们可以分析出 node 的事件循环的阶段顺序为:

    输入数据阶段(incoming data)->轮询阶段(poll)->检查阶段(check)->关闭事件回调阶段(close callback)->定时器检测阶段(timers)->I/O事件回调阶段(I/O callbacks)->闲置阶段(idle, prepare)->轮询阶段...

    阶段概述

    • 定时器检测阶段(timers):本阶段执行 timer 的回调,即 setTimeout、setInterval 里面的回调函数。

    • I/O事件回调阶段(I/O callbacks):执行延迟到下一个循环迭代的 I/O 回调,即上一轮循环中未被执行的一些I/O回调。

    • 闲置阶段(idle, prepare):仅系统内部使用。

    • 轮询阶段(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。

    • 检查阶段(check):setImmediate() 回调函数在这里执行

    • 关闭事件回调阶段(close callback):一些关闭的回调函数,如:socket.on('close', ...)。

    三大重点阶段

    日常开发中的绝大部分异步任务都是在 poll、check、timers 这3个阶段处理的,所以我们来重点看看。

    timers

    timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。同样,在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行。

    poll

    poll 是一个至关重要的阶段,poll 阶段的执行逻辑流程图如下:

    如果当前已经存在定时器,而且有定时器到时间了,拿出来执行,eventLoop 将回到 timers 阶段。

    如果没有定时器, 会去看回调函数队列。

    • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制

    • 如果 poll 队列为空时,会有两件事发生

      • 如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调

      • 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去,一段时间后自动进入 check 阶段。

    check

    check 阶段。这是一个比较简单的阶段,直接执行 setImmdiate 的回调。

    process.nextTick

    process.nextTick 是一个独立于 eventLoop 的任务队列。

    在每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。

    看一个例子:

    setImmediate(() => {
        console.log('timeout1')
        Promise.resolve().then(() => console.log('promise resolve'))
        process.nextTick(() => console.log('next tick1'))
    });
    setImmediate(() => {
        console.log('timeout2')
        process.nextTick(() => console.log('next tick2'))
    });
    setImmediate(() => console.log('timeout3'));
    setImmediate(() => console.log('timeout4'));
    
    • 在 node11 之前,因为每一个 eventLoop 阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行,因此上述代码是先进入 check 阶段,执行所有 setImmediate,完成之后执行 nextTick 队列,最后执行微任务队列,因此输出为timeout1=>timeout2=>timeout3=>timeout4=>next tick1=>next tick2=>promise resolve

    • 在 node11 之后,process.nextTick 是微任务的一种,因此上述代码是先进入 check 阶段,执行一个 setImmediate 宏任务,然后执行其微任务队列,再执行下一个宏任务及其微任务,因此输出为timeout1=>next tick1=>promise resolve=>timeout2=>next tick2=>timeout3=>timeout4

    node 版本差异说明

    这里主要说明的是 node11 前后的差异,因为 node11 之后一些特性已经向浏览器看齐了,总的变化一句话来说就是,如果是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行对应的微任务队列,一起来看看吧~

    timers 阶段的执行时机变化

    setTimeout(()=>{
        console.log('timer1')
        Promise.resolve().then(function() {
            console.log('promise1')
        })
    }, 0)
    setTimeout(()=>{
        console.log('timer2')
        Promise.resolve().then(function() {
            console.log('promise2')
        })
    }, 0)
    
    • 如果是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列,这就跟浏览器端运行一致,最后的结果为timer1=>promise1=>timer2=>promise2

    • 如果是 node10 及其之前版本要看第一个定时器执行完,第二个定时器是否在完成队列中.

      • 如果是第二个定时器还未在完成队列中,最后的结果为timer1=>promise1=>timer2=>promise2

      • 如果是第二个定时器已经在完成队列中,则最后的结果为timer1=>timer2=>promise1=>promise2

    check 阶段的执行时机变化

    setImmediate(() => console.log('immediate1'));
    setImmediate(() => {
        console.log('immediate2')
        Promise.resolve().then(() => console.log('promise resolve'))
    });
    setImmediate(() => console.log('immediate3'));
    setImmediate(() => console.log('immediate4'));
    
    • 如果是 node11 后的版本,会输出immediate1=>immediate2=>promise resolve=>immediate3=>immediate4

    • 如果是 node11 前的版本,会输出immediate1=>immediate2=>immediate3=>immediate4=>promise resolve

    nextTick 队列的执行时机变化

    setImmediate(() => console.log('timeout1'));
    setImmediate(() => {
        console.log('timeout2')
        process.nextTick(() => console.log('next tick'))
    });
    setImmediate(() => console.log('timeout3'));
    setImmediate(() => console.log('timeout4'));
    
    • 如果是 node11 后的版本,会输出timeout1=>timeout2=>next tick=>timeout3=>timeout4

    • 如果是 node11 前的版本,会输出timeout1=>timeout2=>timeout3=>timeout4=>next tick

    以上几个例子,你应该就能清晰感受到它的变化了,反正记着一个结论,如果是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行对应的微任务队列。

    node 和 浏览器 eventLoop的主要区别

    两者最主要的区别在于浏览器中的微任务是在每个相应的宏任务中执行的,而nodejs中的微任务是在不同阶段之间执行的。

    思考拓展题

    node的事件循环中,首次进入事件循环时,在poll阶段,有可能会跳到check阶段执行回调,但是check阶段在poll阶段之后,那么poll阶段是如何知道check阶段有没有回调需要执行的呢?

    更多理解资料

    参考资料

    • 一道面试题引发的node事件循环深入思考

    • Node.js 事件循环,定时器和 process.nextTick()

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

    • New Changes to the Timers and Microtasks in Node v11.0.0 ( and above)


    最后

    • 欢迎加我微信(winty230),拉你进技术群,长期交流学习...

    • 欢迎关注「前端Q」,认真学前端,做个专业的技术人...

    原创不易,点个在看支持我吧

    展开全文
  • JS的事件循环机制

    2020-06-19 17:43:11
    JS的异步操作来源于浏览器提供的异步API,以及事件循环机制,回调函数队列(或者叫task queue,任务队列)。事件循环机制循环往复地监测JS调用栈和任务队列,一旦调用栈为空,且任务队列中有回调函数,就会将回调...

    事件循环的意义

    将耗时的任务分解为几部分来执行,这几个分解的任务所执行的时机由事件循环机制来确保。这样做是为了避免耗时操作阻塞浏览器UI界面的重绘。

    事件循环与异步操作

    JS的异步操作实质上是将一个任务分为几部分,以两部分为例,第二部分作为回调函数(这个函数根据API的写法来确定是异步调用还是同步调用,如果是异步调用才真正应该叫回调函数,同步的操作无所谓回调),等第一部分运行结束有了结果后,再通过调用回调函数(即第二部分)来继续运行任务。第一部分运行往往要么用时不长,要么是浏览器的其他线程执行操作(如远程请求等),回调函数在等待被调用期间,浏览器可以执行其他代码。因此,JS的异步是通过事件循环机制,将回调函数放入任务队列来进行的。

    解释异步操作流程

    在浏览器中,JS解释器用于执行JS代码,即JS函数调用栈。浏览器提供webAPI诸如setTimeout,Ajax等等异步操作,以及任务队列(task queue)。如下图
    浏览器内部执行原理图
    回调函数的写法是浏览器API提供的,那样的写法意味着回调函数会在将来被加入到任务队列里。比如一个Ajax请求$.ajax('url', foo),调用栈开始运行时,函数foo会一直等待ajax的运行结果。一旦运行有结果,foo会被加入到任务队列task queue中。此时,event loop机制开始发挥作用。事件循环机制就是一个监视器,一直监视着调用栈是否为空。一旦为空,就会把任务队列中的函数加入到调用栈内执行。之所以叫事件循环,就是这个监视器会一遍一遍循环监测任务队列中是否有任务。有任务的时候,就准备将任务加入调用栈(只有调用栈空了才加入)。
    此外,还有一个叫job queue的队列,一般称之为微任务队列,而task queue则称为宏任务队列。事件循环监测一圈时,先加入宏任务,宏任务执行完后,再加入微任务。等微任务执行完后,再继续循环。所以,循环一圈,最后加入的一定是微任务,除非没有微任务。不过关于微任务,node环境跟浏览器环境的执行不一样。

    事件循环每一轮的标志

    事件循环的每一轮都以调用栈清空为标志,每次清空完调用栈,就代表着一轮事件循环的结束。

    事件循环与浏览器重绘

    JS的调用栈不为空的时候,浏览器是没法执行重绘的,这样设置也很正常,因为JS代码可能会更改DOM结构,因此只有调用栈为空时,浏览器才会执行重绘。为了避免出现UI界面阻塞,调用栈中的函数代码不应该是耗时操作,如果有任务是耗时的,那应该将任务分割成几部分,采用异步操作来执行任务,从而得到一个漂亮顺畅的UI界面。

    展开全文
  • JavaScript 是浏览器的脚本语言。其主要作用是与客户端进行交互。最开始设计之初它就是单线程。因为它主要操作 DOM。如果设计成多线程,假设一个线程在操作某个 DOM 节点,一个线程又在删除这个 DOM 节点。...

    单线程的 JavaScript

    JavaScript 是浏览器的脚本语言。其主要作用是与客户端进行交互。最开始设计之初它就是单线程。因为它主要操作 DOM。如果设计成多线程,假设一个线程在操作某个 DOM 节点,一个线程又在删除这个 DOM 节点。这时候浏览器就无法判断以哪个线程为主,这种复杂的场景大大增加了语言的难度。所以 JavaScript 从一诞生就是单线程。也是它最重要的特性之一。

    Web Worker

    在 HTML5 中提出了 Web Worker。其主要作用是为 JavaScript 提供多线程环境。主线程可以创建多个 Worker 子线程。但是 Web Worker 不能操作 DOM ,且不能影响主线程。所以也并没有改变 JavaScript 单线程的本质。只是为了有效利用多核 CPU,充分发挥计算机的计算能力。

    任务队列

    因为 JavaScript 是单线程,所以所有的任务只能一个个等着被执行。如果一个任务是被 CPU 一直占着倒也罢。但是有时候是 IO (输入输出设备)占用的时间比较长,而 CPU 处在空闲状态。那么为了有效利用 CPU。可以将 IO 设备耗时较长的任务挂起,先执行后面的任务。等到 IO 设备返回了结果,再回过头来执行挂起的任务。任务队列是一个先进先出的结构,排在前面的事件,优先进入主线程被执行。

    运行机制

    1. 所有的任务都在主线程执行,形成执行栈。
    2. 主线程之外,还有一个任务队列。系统将异步任务放在任务队列中。
    3. 等到主线程中的任务执行完成,系统就会读取任务队列中的任务。
    4. 异步任务结束了等待状态,就会从任务队列进入执行栈,恢复执行。
    5. 主线程重复以上步骤。

    事件回调

    除了常见的 IO 设备之外,用户交互(点击,滑动,触摸等)都会被添加到任务队列,等待主线程读取。IO 设备完成一项任务,就是在 “任务队列”中添加一个事件。表示相关的任务可以进入执行栈了。主线程读取任务队列,就是读取里面有哪些回调事件。
    回调函数就是被任务队列挂起的代码。异步任务必须指定回调函数。当异步任务从任务队列中回到执行栈,就是回调函数被执行的时候。

    事件循环(Event Loop)

    主线程运行的时候,会产生堆和栈。栈中的代码会调用各种外部 API。他们在任务队列中加入各种事件。只要栈中的代码执行完毕,主线程就会去读取“任务列队”,依次执行哪些事件所对应的回调函数。主线程从“任务队列”中读取事件,这个过程是循环不断的。所以叫事件循环机制。

    展开全文
  • 前言我们都知道,...而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一

    前言

    我们都知道,javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互。

    单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务。

    而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。

    单线程是必要的,也是javascript这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的dom操作。试想一下 如果javascript是多线程的,那么当两个线程同时对dom进行一项操作,例如一个向其添加事件,而另一个删除了这个dom,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,javascript选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。

    当然,现如今人们也意识到,单线程在保证了执行顺序的同时也限制了javascript的效率,因此开发出了web worker技术。这项技术号称让javascript成为一门多线程语言。

    然而,使用web worker技术开的多线程有着诸多限制,例如:所有新线程都受主线程的完全控制,不能独立执行。这意味着这些“线程” 实际上应属于主线程的子线程。另外,这些子线程并没有执行I/O操作的权限,只能为主线程分担一些诸如计算等任务。所以严格来讲这些线程并没有完整的功能,也因此这项技术并非改变了javascript语言的单线程本质。

    可以预见,未来的javascript也会一直是一门单线程的语言。

    话说回来,前面提到javascript的另一个特点是“非阻塞”,那么javascript引擎到底是如何实现的这一点呢?答案就是今天这篇文章的主角——event loop(事件循环)。

    注:虽然nodejs中的也存在与传统浏览器环境下的相似的事件循环。然而两者间却有着诸多不同,故把两者分开,单独解释。

    正文

    浏览器环境下js引擎的事件循环机制

    1.执行栈与事件队列

    当javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。 但是我们这里说的执行栈和上面这个栈的意义却有些不同。

    我们知道,当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。

    当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并吧这个执行环境销毁。接着继续执行队列里的下一段代码。

    下面这个图片非常直观的展示了这个过程,其中的global就是初次运行脚本时向执行栈中加入的代码:




    从图片可知,一个方法执行会向执行栈中加入这个方法的执行环境,在这个执行环境中还可以调用其他方法,甚至是自己,其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的,除非发生了栈溢出,即超过了所能使用内存的最大值。

    以上的过程说的都是同步代码的执行。那么当一个异步代码(如发送ajax请求数据)执行后会如何呢?前文提过,js的另一大特点是非阻塞,实现这一点的关键在于下面要说的这项机制——事件队列(Task Queue)。

    js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。

    这里还有一张图来展示这个过程:



    图中的stack表示我们所说的执行栈,web apis则是代表一些异步事件,而callback queue即事件队列。

    2.macro task与micro task

    以上的事件循环过程是一个宏观的表述,实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)。

    以下事件属于宏任务:

    • setInterval()
    • setTimeout()

    以下事件属于微任务

    • new Promise()
    • new MutaionObserver()

    前面我们介绍过,在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈...如此反复,进入循环。

    我们只需记住当当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行

    这样就能解释下面这段代码的结果:

    setTimeout(function () {
        console.log(1);
    });
    
    new Promise(function(resolve,reject){
        console.log(2)
        resolve(3)
    }).then(function(val){
        console.log(val);
    })
    

    结果为:

    2
    3
    1
     
    

    node环境下的事件循环机制

    1.与浏览器环境有何不同?

    在node中,事件循环表现出的状态与浏览器中大致相同。不同的是node中有一套自己的模型。node中事件循环的实现是依靠的libuv引擎。我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api,而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。 因此实际上node中的事件循环存在于libuv引擎中。

    2.事件循环模型

    下面是一个libuv引擎中的事件循环的模型:

       ┌───────────────────────┐
    ┌─>│        timers         │
    │  └──────────┬────────────┘
    │  ┌──────────┴────────────┐
    │  │     I/O callbacks     │
    │  └──────────┬────────────┘
    │  ┌──────────┴────────────┐
    │  │     idle, prepare     │
    │  └──────────┬────────────┘      ┌───────────────┐
    │  ┌──────────┴────────────┐      │   incoming:   │
    │  │         poll          │<──connections───     │
    │  └──────────┬────────────┘      │   data, etc.  │
    │  ┌──────────┴────────────┐      └───────────────┘
    │  │        check          │
    │  └──────────┬────────────┘
    │  ┌──────────┴────────────┐
    └──┤    close callbacks    │
       └───────────────────────┘
    

    注:模型中的每一个方块代表事件循环的一个阶段

    这个模型是node官网上的一篇文章中给出的,我下面的解释也都来源于这篇文章。我会在文末把文章地址贴出来,有兴趣的朋友可以亲自与看看原文。

    3.事件循环各阶段详解

    从上面这个模型中,我们可以大致分析出node中的事件循环的顺序:

    外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段...

    以上各阶段的名称是根据我个人理解的翻译,为了避免错误和歧义,下面解释的时候会用英文来表示这些阶段。

    这些阶段大致的功能如下:

    • timers: 这个阶段执行定时器队列中的回调如 setTimeout() 和 setInterval()
    • I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。
    • idle, prepare: 这个阶段仅在内部使用,可以不必理会。
    • poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
    • check: setImmediate()的回调会在这个阶段执行。
    • close callbacks: 例如socket.on('close', ...)这种close事件的回调。

    下面我们来按照代码第一次进入libuv引擎后的顺序来详细解说这些阶段:

    poll阶段

    当个v8引擎将js代码解析后传入libuv引擎后,循环首先进入poll阶段。poll阶段的执行逻辑如下: 先查看poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调。 当queue为空时,会检查是否有setImmediate()的callback,如果有就进入check阶段执行这些callback。但同时也会检查是否有到期的timer,如果有,就把这些到期的timer的callback按照调用顺序放到timer queue中,之后循环会进入timer阶段执行queue中的 callback。 这两者的顺序是不固定的,收到代码运行的环境的影响。如果两者的queue都是空的,那么loop会在poll阶段停留,直到有一个i/o事件返回,循环会进入i/o callback阶段并立即执行这个事件的callback。

    值得注意的是,poll阶段在执行poll queue中的回调时实际上不会无限的执行下去。有两种情况poll阶段会终止执行poll queue中的下一个回调:1.所有回调执行完毕。2.执行数超过了node的限制。

    check阶段

    check阶段专门用来执行setImmediate()方法的回调,当poll阶段进入空闲状态,并且setImmediate queue中有callback时,事件循环进入这个阶段。

    close阶段

    当一个socket连接或者一个handle被突然关闭时(例如调用了socket.destroy()方法),close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()方法发送出去。

    timer阶段

    这个阶段以先进先出的方式执行所有到期的timer加入timer队列里的callback,一个timer callback指得是一个通过setTimeout或者setInterval函数设置的回调函数。

    I/O callback阶段

    如上文所言,这个阶段主要执行大部分I/O事件的回调,包括一些为操作系统执行的回调。例如一个TCP连接生错误时,系统需要执行回调来获得这个错误的报告。

    4.process.nextTick,setTimeout与setImmediate的区别与使用场景

    在node中有三个常用的用来推迟任务执行的方法:process.nextTick,setTimeout(setInterval与之相同)与setImmediate

    这三者间存在着一些非常不同的区别:

    process.nextTick()

    尽管没有提及,但是实际上node中存在着一个特殊的队列,即nextTick queue。这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先清空这个队列。与执行poll queue中的任务不同的是,这个操作在队列清空前是不会停止的。这也就意味着,错误的使用process.nextTick()方法会导致node进入一个死循环。。直到内存泄漏。

    那么合适使用这个方法比较合适呢?下面有一个例子:

    const server = net.createServer(() => {}).listen(8080);
    
    server.on('listening', () => {});
    

    这个例子中当,当listen方法被调用时,除非端口被占用,否则会立刻绑定在对应的端口上。这意味着此时这个端口可以立刻触发listening事件并执行其回调。然而,这时候on('listening)还没有将callback设置好,自然没有callback可以执行。为了避免出现这种情况,node会在listen事件中使用process.nextTick()方法,确保事件在回调函数绑定后被触发。

    setTimeout()和setImmediate()

    在三个方法中,这两个方法最容易被弄混。实际上,某些情况下这两个方法的表现也非常相似。然而实际上,这两个方法的意义却大为不同。

    setTimeout()方法是定义一个回调,并且希望这个回调在我们所指定的时间间隔后第一时间去执行。注意这个“第一时间执行”,这意味着,受到操作系统和当前执行任务的诸多影响,该回调并不会在我们预期的时间间隔后精准的执行。执行的时间存在一定的延迟和误差,这是不可避免的。node会在可以执行timer回调的第一时间去执行你所设定的任务。

    setImmediate()方法从意义上将是立刻执行的意思,但是实际上它却是在一个固定的阶段才会执行回调,即poll阶段之后。有趣的是,这个名字的意义和之前提到过的process.nextTick()方法才是最匹配的。node的开发者们也清楚这两个方法的命名上存在一定的混淆,他们表示不会把这两个方法的名字调换过来---因为有大量的ndoe程序使用着这两个方法,调换命名所带来的好处与它的影响相比不值一提。

    setTimeout()和不设置时间间隔的setImmediate()表现上及其相似。猜猜下面这段代码的结果是什么?

    setTimeout(() => {
        console.log('timeout');
    }, 0);
    
    setImmediate(() => {
        console.log('immediate');
    });
    

    实际上,答案是不一定。没错,就连node的开发者都无法准确的判断这两者的顺序谁前谁后。这取决于这段代码的运行环境。运行环境中的各种复杂的情况会导致在同步队列里两个方法的顺序随机决定。但是,在一种情况下可以准确判断两个方法回调的执行顺序,那就是在一个I/O事件的回调中。下面这段代码的顺序永远是固定的:

    const fs = require('fs');
    
    fs.readFile(__filename, () => {
        setTimeout(() => {
            console.log('timeout');
        }, 0);
        setImmediate(() => {
            console.log('immediate');
        });
    });
    

    答案永远是:

    immediate
    timeout
    

    因为在I/O事件的回调中,setImmediate方法的回调永远在timer的回调前执行。

    尾声

    javascrit的事件循环是这门语言中非常重要且基础的概念。清楚的了解了事件循环的执行顺序和每一个阶段的特点,可以使我们对一段异步代码的执行顺序有一个清晰的认识,从而减少代码运行的不确定性。合理的使用各种延迟事件的方法,有助于代码更好的按照其优先级去执行。这篇文章期望用最易理解的方式和语言准确描述事件循环这个复杂过程,但由于作者自己水平有限,文章中难免出现疏漏。如果您发现了文章中的一些问题,欢迎在留言中提出,我会尽量回复这些评论,把错误更正。

    引用

    JavaScript中执行环境和栈 - 陪伴是最长情的告白 - 博客园www.cnblogs.com图标Macrotask 与 Microtask 核心概念js.walfud.com图标The Node.js Event Loop, Timers, and process.nextTick() | Node.jsnodejs.org图标
    展开全文
  • js事件循环机制

    千次阅读 2018-08-14 17:39:13
     从上一篇说明vue nextTick的文章中,多次出现“事件循环”这个名词,简单说明了事件循环的步骤,以便理解nextTick的运行时机,这篇文章将更为详细的分析下事件循环。在此之前需要了解JS单线程,及由此产生的同步...
  • 事件循环机制

    2020-10-13 14:42:09
    事件循环机制 一.任务 单线程就意味着,所有的任务都需要排队,前面一个任务结束,才会执行后一个任务.如果前一个任务耗时很长,后一个任务就不得不一直等着 任务分为2种: 1.同步任务(synchronous) 同步任务是指:在主线...
  • 事件循环机制小结

    2020-04-18 10:37:37
    事件循环机制 谈谈我自己理解的JS事件循环机制,考虑到开发中遇到的场景,我将其分为两部分来理解。 执行栈和任务队列 首先要明白JS是单线程的,无法向其他语言那样用多线程来处理问题(WebWorker不考虑,且其并没有...
  • Event Loop事件循环机制

    千次阅读 2019-03-03 16:40:55
    转载自:阮一峰博客《JavaScript 运行机制详解:再谈Event Loop》 一、为什么JavaScript是单线程? JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个...
  • Qt的事件循环机制

    万次阅读 多人点赞 2020-04-07 17:45:37
    答:鼠标事件(QMouseEvent)、键盘事件(QKeyEvent)、绘制事件(QPaintEvent)、窗口尺寸改变(QResizeEvent)、滚动事件(QScrollEvent)、控件显示(QShowEvent)、控件隐藏(QHideEvent)、定时器事件(QTimerEvent)等等。...
  • Js中for循环的阻塞机制

    万次阅读 2016-10-24 20:48:11
    什么是阻塞机制 Js阻塞机制,跟Js引擎的单线程处理方式有关,每个window一个JS线程。所谓单线程,在某个特定的时刻只有特定的代码能够被执行,并阻塞其它的代码。 由于浏览器是事件驱动的(Event driven),因此...
  • JS:事件循环机制、调用栈以及任务队列

    万次阅读 多人点赞 2017-05-01 15:40:07
    写在前面js里的事件循环机制十分有趣。从很多面试题也可以看出来,考察简单的setTimeout也就是考察这个机制的。 在之前,我只是简单地认为由于函数执行很快,setTimeout执行时间即便为0也不会马上输出,而是等待...
  • Node.js机制及原理理解初步

    万次阅读 多人点赞 2014-12-16 20:07:23
    node.js是单线程。...node使用异步IO和事件驱动(回调函数)来解决这个问题。一般来说,高并发解决方案会提供多线程模型,为每个业务逻辑配备一个线程,通过系统线程切换来来弥补同步I/O调用的时间开销。而node.js
  • QT源码解析 文章索引

    万次阅读 2010-04-26 10:01:00
    QT源码解析(一) QT创建窗口程序、消息循环和WinMain函数 QT源码解析(二)深入剖析QT元对象系统和信号槽机制 QT源码解析(三)深入剖析QT元对象系统和信号槽机制(续) QT源码解析(四)剖析Qt的事件机制原理 QT...
  • 深入了解nodejs的事件循环机制

    千次阅读 2018-09-29 17:36:59
    一直以来,我写的的大部分JS代码都是在浏览器环境下运行,因此也了解过浏览器的事件循环机制,知道有macrotask和microtask的区别。但最近写node时,发现node的事件循环机制和浏览器端有很大的不同,特此深入地学习了...
  • C#中消息处理机制(事件与委托)

    千次阅读 2016-05-10 14:30:06
    编写过Windows桌面应用程序的人都知道,微软的Windows操作系统与应用程序之间的通信绝大部分是基于消息循环机制的。在VC++中,程序使用GetMessage,TranslateMessage,DispatchMessage语句从消息队列中获取消息,...
  • 1.回顾 通过前两篇总结Android input处理机制(一)InputReader , Android input处理机制(二)改键机制,我们...InputReader通过使用EventHub循环获取事件,当没有事件发生时InputReaderThread会休眠。 2.InputD
  • libevent源码深度剖析七

    万次阅读 2010-03-10 11:09:00
    现在我们已经初步了解了libevent的Reactor组件——event_base和事件管理框架,接下来就是libevent事件处理的中心部分——事件主循环,根据系统提供的事件多路分发机制执行事件循环,对已注册的就绪事件,调用注册...
  • 网络库libevent、libev、libuv对比

    万次阅读 2017-05-05 17:03:44
    Libevent、libev、libuv三个网络库,都是c语言实现的异步事件库Asynchronousevent library)。 异步事件库本质上是提供异步事件通知...事件(Event):事件是异步事件通知机制的核心,比如fd事件、超时事件、信
  • 一、引言前边已经提到过,线程的同步机制包括互斥锁同步和事件同步。互斥锁同步包括atomic、critical、mutex函数,其机制与普通多线程同步的机制类似。而事件同步则通过nowait、sections、single、master等预处理器...
  • Node.js异步机制和基本原理

    千次阅读 2017-02-19 21:39:23
    一、Node.js优缺点 node.js是单线程。 好处就是 1)简单 2)高性能,避免了频繁的线程切换开销 ...3)占用资源小,因为是单线程,在大负荷情况下,对内存占用仍然很低 ...node使用异步IO和事件驱动(回
1 2 3 4 5 ... 20
收藏数 171,333
精华内容 68,533
关键字:

事件循环机制