最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 定时器,Immediates 和 Process.nextTick —— Node 事件循环 Part 2

    正文概述 掘金(众成翻译)   2021-01-31   687

    译者:焉逢

    原文链接

    定时器,Immediates 和 Process.nextTick —— Node 事件循环 Part 2

    欢迎回到⎣事件循环⎤文章系列!在第一篇文章中,我整体介绍了 Node 的事件循环。在这篇文章中,我准备用一些代码片段作为例子,来详细探讨我们前文提及的三个重要的事件队列 —— 定时器队列immediates 队列以及 nextTick 队列

    本系列文章指引

    • 事件循环总体概览

    • 定时器, Immediates 和 Process.nextTick (本文)

    • Resolved Promises 和 Process.nextTick — 未完成

    • I/O 的处理 — 未完成

    • 处理事件循环的最佳实践 — 未完成

    • 编写异步组件(async Add-ons) — 未完成

    nextTick 队列

    先让我们再次看看前文出现过的图示。

    定时器,Immediates 和 Process.nextTick —— Node 事件循环 Part 2

    图中,nextTick 队列被区分开来,因为它是由 Node 实现而非 libuv 原生提供

    对于事件循环的每个阶段(定时器队列,I/O 事件队列,immediates 队列,close 处理队列是 4 个主要阶段),事件循环在移动到某一阶段之前,Node 会检查 nextTick 队列中是否有可处理的事件。如果有,在前往下一个主要阶段之前,事件循环会先直接处理这个队列直至它为空(当然还有其他 microtask 队列)。

    这带来了一个新的问题。递归、无限的调用 process.nextTicknextTick 队列添加事件将导致 I/O 和其他队列永远处于饥饿(starve)状态。我们可以用下面的代码模拟这种情况。

    const fs = require('fs');
    
    function addNextTickRecurs(count) {
        let self = this;
        if (self.id === undefined) {
            self.id = 0;
        }
    
        if (self.id === count) return;
    
        process.nextTick(() => {
            console.log(`process.nextTick call ${++self.id}`);
            addNextTickRecurs.call(self, count);
        });
    }
    
    addNextTickRecurs(Infinity);
    setTimeout(console.log.bind(console, 'omg! setTimeout was called'), 10);
    setImmediate(console.log.bind(console, 'omg! setImmediate also was called'));
    fs.readFile(__filename, () => {
        console.log('omg! file read complete callback was called!');
    });
    
    console.log('started');
    

    你可以看到输出是一个无限循环的 nextTick 回调调用,而 setTimeoutsetImmediatefs.readFile 回调从未被调用,因为任何 ""omg!..."" 都没有打印到控制台中。

    started
    process.nextTick call 1
    process.nextTick call 2
    process.nextTick call 3
    process.nextTick call 4
    process.nextTick call 5
    process.nextTick call 6
    process.nextTick call 7
    process.nextTick call 8
    process.nextTick call 9
    process.nextTick call 10
    process.nextTick call 11
    process.nextTick call 12
    ....
    

    你可以尝试设置一个有限的值作为 addNextTickRecurs 调用的参数,你将看到 setTimeoutsetImmediatefs.readFile 回调会在一系列 process.nextTick 回调后面被调用。

    定时器队列

    每当你使用 setTimeoutsetInterval 添加定时器回调时,Node 将添加定时器到定时器堆(timer heap)中,这是一个由 libuv 访问的数据结构。在事件循环的定时器阶段,Node 会检查定时器堆中已过期的 timer/interval ,并调用它们各自的回调。如果有多个已过期的定时器(比如设置了相同的过期时间),那它们会按照它们被设置的顺序执行。

    当一个 timer/interval 设置了明确的到期时间时,并不能确保在到期后能准时地执行回调。定时器回调的调用,依赖于系统的性能(Node 在执行回调前需要检查时间,显然这需要一些 CPU 时间)以及当前事件循环中的执行情况。事实上,到期时间确保的是,定时器回调至少在该时间之前,不会被调用。我们可以模拟一下:

    const start = process.hrtime();
    
    setTimeout(() => {
        const end = process.hrtime(start);
        console.log(`timeout callback executed after ${end[0]}s and ${end[1]/Math.pow(10,9)}ms`);
    }, 1000);
    

    上面的代码设置了一个过期时间为 1000ms 的定时器,它会打印出执行回调时消耗的时间。如果你运行该代码多次,你会注意到它每次的结果都不尽相同,并且永远不会出现 timeout callback executed after 1s and 0ms 这样的结果。你看到的结果类似这样:

    timeout callback executed after 1s and 0.006058353ms
    timeout callback executed after 1s and 0.004489878ms
    timeout callback executed after 1s and 0.004307132ms
    ...
    

    setTimeoutsetImmediate 一起使用时,定时器的这种特性可能会导致意外和不可预测的结果,接下来你就会看到了。

    Immediates 队列

    尽管在行为上,immediates 队列和定时器队列有点相似,但它也有自己的一些独特特性。不像定时器,即使过期时间为0,我们也不能确保它的回调何时会执行,但 immediates 队列被确保会在事件循环的 I/O 阶段之后被立即执行。可以通过 setImmediate 向此队列添加一个事件(回调):

    setImmediate(() => {
       console.log('Hi, this is an immediate');
    });
    

    setTimeout vs setImmediate ?

    现在,我们回过头来看看文章开头的那个图示,可以看到当事件循环开始执行的时候,Node 首先会处理定时器队列。接着,在处理完 I/O 之后,会来到 immediates 队列。通过图示,我们很容易推断出以下代码的输出结果。

    setTimeout(function() {
        console.log('setTimeout')
    }, 0);
    setImmediate(function() {
        console.log('setImmediate')
    });
    

    你可能会猜,以上代码永远会在 setImmediate 之前打印 setTimeout,因为定时器队列的处理优先于 immediates 队列。然而事实是,以上代码的运行结果是永远不确定的!如果你试着运行多次,你会得到不一样的输出结果。

    这是因为设定一个 0 秒过期的定时器,永远不能保证在 0 秒后回调会被准时调用。正因如此,当事件循环启动时,它可能还没有看到已过期的定时器。这时候事件循环将前往 I/O 阶段,接着来到 immediates 队列,发现这队列中有一个待处理的事件,于是执行它。

    但对于下面这段代码,我们可以确保 immediate 回调永远会比定时器回调先执行。

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

    我们来看看它的执行流程。

    • 一开始,程序通过 fs.readFile 异步读取当前文件,并提供了一个回调

    • 事件循环启动

    • 一旦文件读取完成,一个事件(待执行的回调)会被添加进事件循环的 I/O 队列中

    • 由于没有其他待执行的事件,Node 开始等待后续的 I/O 事件。当它看到文件读取完成的事件时,执行它

    • 在这个回调的执行过程中,一个定时器被添加到定时器堆中,一个 immediate 事件被添加到 immediates 队列中

    • 我们知道当前事件循环处于 I/O 阶段,并且此时没有其他 I/O 事件可处理,事件循环于是来到 immediates 阶段,它发现了在执行文件读取回调的过程中被添加的 immediate 回调,于是便直接执行它

    • 在事件循环的下一轮中,它发现了已过期的定时器,于是执行相应的回调

    总结

    让我们一起看看这些不同的阶段、队列,在整个事件循环中是如何工作的。以下是代码示例。

    setImmediate(() => console.log('this is set immediate 1'));
    setImmediate(() => console.log('this is set immediate 2'));
    setImmediate(() => console.log('this is set immediate 3'));
    
    setTimeout(() => console.log('this is set timeout 1'), 0);
    setTimeout(() => {
        console.log('this is set timeout 2');
        process.nextTick(() => console.log('this is process.nextTick added inside setTimeout'));
    }, 0);
    setTimeout(() => console.log('this is set timeout 3'), 0);
    setTimeout(() => console.log('this is set timeout 4'), 0);
    setTimeout(() => console.log('this is set timeout 5'), 0);
    
    process.nextTick(() => console.log('this is process.nextTick 1'));
    process.nextTick(() => {
        process.nextTick(console.log.bind(console, 'this is the inner next tick inside next tick'));
    });
    process.nextTick(() => console.log('this is process.nextTick 2'));
    process.nextTick(() => console.log('this is process.nextTick 3'));
    process.nextTick(() => console.log('this is process.nextTick 4'));
    

    以上代码执行完之后,以下这些事件会被添加到相应的事件队列中。

    • 3 个 immediate 事件
    • 5 个定时器事件
    • 5 个 nextTick 事件

    我们看看执行流程:

    1. 事件循环启动时,它注意到 nextTick 队列非空,于是开始处理这个队列。在执行第二个 nextTick 回调的过程中,一个新的 nextTick 被添加到该队列的末尾,它将在当前 nextTick 队列结束后执行

    2. 开始执行定时器回调。在执行第二个定时器回调的过程中,一个事件被添加至 nextTick 队列

    3. 一旦定时器队列处理完,事件循环看到 nextTick 队列中有一个事件(在执行第二个定时器回调的过程中被添加的),于是处理 nextTick 队列

    4. 由于没有任何 I/O 事件可处理,事件循环来到了 immediates 阶段,并处理相应队列

    很好,如果你运行以上代码,你将看到下面的输出结果:

    this is process.nextTick 1
    this is process.nextTick 2
    this is process.nextTick 3
    this is process.nextTick 4
    this is the inner next tick inside next tick
    this is set timeout 1
    this is set timeout 2
    this is set timeout 3
    this is set timeout 4
    this is set timeout 5
    this is process.nextTick added inside setTimeout
    this is set immediate 1
    this is set immediate 2
    this is set immediate 3
    

    下篇文章我们将讨论更多关于 nextTick 回调和 promise 的内容。如果你发现了有什么需要更正或添加的内容,请随时回复。

    参考链接:

    • NodeJS API Docs nodejs.org/api

    • NodeJS Github github.com/nodejs/node…

    • Libuv Official Documentation docs.libuv.org/

    • NodeJS Design Patterns www.packtpub.com/mapt/book/w…

    • Everything You Need to Know About Node.js Event Loop — Bert Belder, IBM www.youtube.com/watch?v=PNa…

    • Node’s Event Loop From the Inside Out by Sam Roberts, IBM www.youtube.com/watch?v=P9c…

    • asynchronous disk I/O blog.libtorrent.org/2012/10/asy…

    • Event loop in JavaScript acemood.github.io/2016/02/01/…


    起源地下载网 » 定时器,Immediates 和 Process.nextTick —— Node 事件循环 Part 2

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元