广西嘉华李家鸣 發表於 2019-9-29 10:18:00

【THE LAST TIME】彻底吃透 JavaScript 执行机制

<h2 id="前言">前言</h2>
<blockquote>
<p>The last time, I have learned</p>
</blockquote>
<p>【THE LAST TIME】一直是我想写的一个系列,旨在厚积薄发,重温前端。</p>
<p>也是给自己的查缺补漏和技术分享。</p>
<p>欢迎大家多多评论指点吐槽。</p>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101757729-1184276418.png"></p>
<blockquote>
<p>系列文章均首发于公众号【全栈前端精选】,笔者文章集合详见Nealyang/personalBlog。目录皆为暂定</p>
</blockquote>
<h2 id="执行--运行">执行 &amp; 运行</h2>
<p>首先我们需要声明下,<code>JavaScript</code> 的执行和运行是两个不同概念的,<strong>执行,一般依赖于环境</strong>,比如 <code>node</code>、浏览器、<code>Ringo</code> 等, <strong>JavaScript 在不同环境下的执行机制可能并不相同</strong>。而今天我们要讨论的 <code>Event Loop</code> 就是 <code>JavaScript</code> 的一种执行方式。所以下文我们还会梳理 <code>node</code> 的执行方式。<strong>而运行呢,是指JavaScript 的解析引擎。这是统一的。</strong></p>
<h2 id="关于-javascript">关于 JavaScript</h2>
<p>此篇文章中,这个小标题下,我们只需要牢记一句话: <strong>JavaScript 是单线程语言</strong> ,无论<code>HTML5</code> 里面 <code>Web-Worker</code> 还是 node 里面的<code>cluster</code>都是“纸老虎”,而且 <code>cluster</code> 还是进程管理相关。这里读者注意区分:进程和线程。</p>
<p>既然 <code>JavaScript</code> 是单线程语言,那么就会存在一个问题,所有的代码都得一句一句的来执行。就像我们在食堂排队打饭,必须一个一个排队点菜结账。那些没有排到的,就得等着~</p>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101758446-2119957864.jpg"></p>
<h2 id="概念梳理">概念梳理</h2>
<p>在详解执行机制之前,先梳理一下 <code>JavaScript</code> 的一些基本概念,方便后面我们说到的时候大伙儿心里有个印象和大概的轮廓。</p>
<h3 id="事件循环event-loop">事件循环(Event Loop)</h3>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101758670-1500206169.png"></p>
<p>什么是 Event Loop?</p>
<p>其实这个概念还是比较模糊的,因为他必须得结合着运行机制来解释。</p>
<p><code>JavaScript</code> 有一个主线程 <code>main thread</code>,和调用栈 <code>call-stack</code> 也称之为执行栈。所有的任务都会放到调用栈中等待主线程来执行。</p>
<p>暂且,我们先理解为上图的大圈圈就是 Event Loop 吧!并且,这个圈圈,一直在转圈圈~ 也就是说,<code>JavaScript</code> 的 <code>Event Loop</code> 是伴随着整个源码文件生命周期的,只要当前 <code>JavaScript</code> 在运行中,内部的这个循环就会不断地循环下去,去寻找 <code>queue</code> 里面能执行的 <code>task</code>。</p>
<h3 id="任务队列task-queue">任务队列(task queue)</h3>
<p><code>task</code>,就是任务的意思,我们这里理解为每一个语句就是一个任务</p>
<pre><code>console.log(1);
console.log(2);
</code></pre>
<p>如上语句,其实就是就可以理解为两个 <code>task</code>。</p>
<p>而 <code>queue</code> 呢,就是<code>FIFO</code>的队列!</p>
<p>所以 <code>Task Queue</code> 就是承载任务的队列。而 <code>JavaScript</code> 的 <code>Event Loop</code> 就是会不断地过来找这个 <code>queue</code>,问有没有 <code>task</code> 可以运行运行。</p>
<h3 id="同步任务synctask异步任务asynctask">同步任务(SyncTask)、异步任务(AsyncTask)</h3>
<p><strong>同步任务说白了就是主线程来执行的时候立即就能执行的代码</strong>,比如:</p>
<pre><code>console.log('this is THE LAST TIME');
console.log('Nealyang');
</code></pre>
<p>代码在执行到上述 <code>console</code> 的时候,就会立即在控制台上打印相应结果。</p>
<p>而所谓的异步任务就是主线程执行到这个 <code>task</code> 的时候,“<em>唉!你等会,我现在先不执行,等我 xxx 完了以后我再来等你执行</em>” 注意上述我说的是<strong>等你</strong>来执行。</p>
<p>说白了,<strong>异步任务就是你先去执行别的 task,等我这 xxx 完之后再往 Task Queue 里面塞一个 task 的同步任务来等待被执行</strong></p>
<pre><code>setTimeout(()=&gt;{
console.log(2)
});
console.log(1);
</code></pre>
<p>如上述代码,<code>setTimeout</code> 就是一个异步任务,主线程去执行的时候遇到 <code>setTimeout</code> 发现是一个异步任务,就先注册了一个异步的回调,然后接着执行下面的语句<code>console.log(1)</code>,等上面的异步任务等待的时间到了以后,在执行<code>console.log(2)</code>。具体的执行机制会在后面剖析。</p>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101759355-372121020.png"></p>
<ul>
<li>主线程自上而下执行所有代码</li>
<li>同步任务直接进入到主线程被执行,而异步任务则进入到 <code>Event Table</code> 并注册相对应的回调函数</li>
<li>异步任务完成后,<code>Event Table</code> 会将这个函数移入 <code>Event Queue</code></li>
<li>主线程任务执行完了以后,会从<code>Event Queue</code>中读取任务,进入到主线程去执行。</li>
<li>循环如上</li>
</ul>
<p>上述动作不断循环,就是我们所说的事件循环(<code>Event Loop</code>)。</p>
<h4 id="小试牛刀">小试牛刀</h4>
<pre><code>ajax({
    url:www.Nealyang.com,
    data:prams,
    success:() =&gt; {
      console.log('请求成功!');
    },
    error:()=&gt;{
      console.log('请求失败~');
    }
})
console.log('这是一个同步任务');
</code></pre>
<ul>
<li>ajax 请求首先进入到 <code>Event Table</code> ,分别注册了<code>onError</code>和<code>onSuccess</code>回调函数。</li>
<li>主线程执行同步任务:<code>console.log('这是一个同步任务');</code></li>
<li>主线程任务执行完毕,看<code>Event Queue</code>是否有待执行的 task,这里是不断地检查,只要主线程的<code>task queue</code>没有任务执行了,主线程就一直在这等着</li>
<li>ajax 执行完毕,将回调函数<code>push</code> 到<code>Event Queue</code>。(步骤 3、4 没有先后顺序而言)</li>
<li>主线程“终于”等到了<code>Event Queue</code>里有 <code>task</code>可以执行了,执行对应的回调任务。</li>
<li>如此往复。</li>
</ul>
<h3 id="宏任务macrotask微任务microtask">宏任务(MacroTask)、微任务(MicroTask)</h3>
<p><code>JavaScript</code> 的任务不仅仅分为同步任务和异步任务,同时从另一个维度,也分为了宏任务(<code>MacroTask</code>)和微任务(<code>MicroTask</code>)。</p>
<p>先说说 <code>MacroTask</code>,所有的同步任务代码都是<code>MacroTask</code>(这么说其实不是很严谨,下面解释),<code>setTimeout</code>、<code>setInterval</code>、<code>I/O</code>、<code>UI Rendering</code> 等都是宏任务。</p>
<p><code>MicroTask</code>,为什么说上述不严谨我却还是强调所有的同步任务都是 <code>MacroTask</code> 呢,因为我们仅仅需要记住几个 <code>MicroTask</code> 即可,排除法!别的都是 <code>MacroTask</code>。<code>MicroTask</code> 包括:<code>Process.nextTick</code>、<code>Promise.then catch finally</code>(注意我不是说 Promise)、<code>MutationObserver</code>。</p>
<h2 id="浏览器环境下的-event-loop">浏览器环境下的 Event Loop</h2>
<p>当我们梳理完哪些是 <code>MicroTask</code> ,除了那些别的都是 <code>MacroTask</code> 后,哪些是同步任务,哪些又是异步任务后,这里就应该彻底的梳理下JavaScript 的执行机制了。</p>
<p>如开篇说到的,执行和运行是不同的,执行要区分环境。所以这里我们将 <code>Event Loop</code> 的介绍分为浏览器和 Node 两个环境下。</p>
<p>先放图镇楼!如果你已经理解了这张图的意思,那么恭喜你,你完全可以直接阅读 Node 环境下的 <code>Event Loop</code> 章节了!</p>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101803115-472057592.gif"></p>
<h3 id="settimeoutsetinterval">setTimeout、setInterval</h3>
<h4 id="settimeout">setTimeout</h4>
<p><code>setTimeout</code> 就是等多长时间来执行这个回调函数。<code>setInterval</code> 就是每隔多长时间来执行这个回调。</p>
<pre><code>let startTime = new Date().getTime();

setTimeout(()=&gt;{
console.log(new Date().getTime()-startTime);
},1000);
</code></pre>
<p>如上代码,顾名思义,就是等 1s 后再去执行 <code>console</code>。放到浏览器下去执行,OK,如你所愿就是如此。</p>
<p>但是这次我们在探讨 JavaScript 的执行机制,所以这里我们得探讨下如下代码:</p>
<pre><code>let startTime = new Date().getTime();

console.log({startTime})

setTimeout(()=&gt;{
console.log(`开始执行回调的相隔时差:${new Date().getTime()-startTime}`);
},1000);

for(let i = 0;i&lt;40000;i++){
console.log(1)
}
</code></pre>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101803351-1405436773.png"></p>
<p>如上运行,<code>setTimeout</code> 的回调函数等到 4.7s 以后才执行!而这时候,我们把 <code>setTimeout</code> 的 1s 延迟给删了:</p>
<pre><code>let startTime = new Date().getTime();

console.log({startTime})

setTimeout(()=&gt;{
console.log(`开始执行回调的相隔时差:${new Date().getTime()-startTime}`);
},0);

for(let i = 0;i&lt;40000;i++){
console.log(1)
}
</code></pre>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101804173-454473561.png"></p>
<p>结果依然是等到 4.7s 后才执行setTimeout 的回调。貌似 setTimeout 后面的延迟并没有产生任何效果!</p>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101804629-1929886595.png"></p>
<p>其实这么说,又应该回到上面的那张 JavaScript 执行的流程图了。</p>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101804804-1115217525.png"></p>
<p><code>setTimeout</code>这里就是简单的异步,我们通过上面的图来分析上述代码的一步一步执行情况</p>
<ul>
<li>首先 <code>JavaScript</code> 自上而下执行代码</li>
<li>遇到遇到赋值语句、以及第一个 <code>console.log({startTime})</code> 分别作为一个 <code>task</code>,压入到立即执行栈中被执行。</li>
<li>遇到 <code>setTImeout</code> 是一个异步任务,则注册相应回调函数。(异步函数告诉你,js 你先别急,等 1s 后我再将回调函数:<code>console.log(xxx)</code>放到 <code>Task Queue</code> 中)</li>
<li>OK,这时候 JavaScript 则接着往下走,遇到了 40000 个 for 循环的 task,没办法,1s 后都还没执行完。其实这个时候上述的回调已经在<code>Task Queue</code> 中了。</li>
<li>等所有的立即执行栈中的 task 都执行完了,在回头看 <code>Task Queue</code> 中的任务,发现异步的回调 task 已经在里面了,所以接着执行。</li>
</ul>
<h3 id="打个比方">打个比方</h3>
<blockquote>
<p>其实上述的不仅仅是 timeout,而是任何异步,比如网络请求等。</p>
</blockquote>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101807252-373789928.png"></p>
<p>就好比,我六点钟下班了,可以安排下自己的活动了!</p>
<p>然后收拾电脑(同步任务)、收拾书包(同步任务)、<strong>给女朋友打电话说出来吃饭吧</strong>(必然是异步任务),然后女朋友说你等会,我先化个妆,等我画好了call你。</p>
<p>那我不能干等着呀,就接着做别的事情,比如那我就在改个 bug 吧,你好了通知我。结果等她一个小时后说我化好妆了,我们出去吃饭吧。不行!我 bug 还没有解决掉呢?你等会。。。。其实这个时候你的一小时化妆还是 5 分钟化妆都已经毫无意义了。。。因为哥哥这会没空~~</p>
<p>如果我 bug 在半个小时就解决完了,没别的任务需要执行了,那么就在这等着呀!必须等着!随时待命!。然后女朋友来电话了,我化完妆了,我们出去吃饭吧,那么刚好,我们在你的完成了请求或者 timeout 时间到了后我刚好闲着,那么我必须立即执行了。</p>
<h3 id="setinterval">setInterval</h3>
<p>说完了 <code>setTimeout</code>,当然不能错过他的孪生兄弟:<code>setInterval</code>。对于执行顺序来说,<code>setInterval</code>会每隔指定的时间将注册的函数置入 <code>Task Queue</code>,如果前面的任务耗时太久,那么同样需要等待。</p>
<p>这里需要说的是,对于 <code>setInterval(fn,ms)</code> 来说,我们制定没 <code>xx ms</code>执行一次 <code>fn</code>,其实是没 <code>xx ms</code>,会有一个<code>fn</code> 进入到 <code>Task Queue</code> 中。<strong>一旦 setInterval 的回调函数<code>fn</code>执行时间超过了xx ms,那么就完全看不出来有时间间隔了。</strong> 仔细回味回味,是不是那么回事?</p>
<h3 id="promise">Promise</h3>
<p>关于 <code>Promise</code> 的用法,这里就不过过多介绍了,后面会在写《【THE LAST TIME】彻底吃透 JavaScript 异步》 一文的时候详细介绍。这里我们只说 JavaScript 的执行机制。</p>
<p>如上所说,<code>promise.then</code> 、<code>catch</code> 和 <code>finally</code> 是属于 <code>MicroTask</code>。这里主要是异步的区分。展开说明之前,我们结合上述说的,再来“扭曲”梳理一下。</p>
<p>为了避免初学者这时候脑子有点混乱,我们暂时<strong>忘掉 JavaScript 异步任务!</strong> 我们暂且称之为待会再执行的同步任务。</p>
<p>有了如上约束后,我们可以说,JavaScript 从一开始就自上而下的执行每一个语句(<code>Task</code>),这时候只能遇到立马就要执行的任务和待会再执行的任务。对于那待会再执行的任务等到能执行了,也不会立即执行,你得等js 执行完这一趟才行</p>
<h4 id="再打个比方">再打个比方</h4>
<p>就像做公交车一样,公交车不等人呀,公交车路线上有人就会停(农村公交!么得站牌),但是等公交车来,你跟司机说,我肚子疼要拉x~这时候公交不会等你。你只能拉完以后等公交下一趟再来(大山里!一个路线就一趟车)。</p>
<p>OK!你拉完了。。。等公交,公交也很快到了!但是,你不能立马上车,因为这时候前面<strong>有个孕妇!有个老人!还有熊孩子</strong>,你必须得让他们先上车,然后你才能上车!</p>
<p>而这些 孕妇、老人、熊孩子所组成的就是传说中的 <code>MicroTask Queue</code>,而且,就在你和你的同事、朋友就必须在他们后面上车。</p>
<p>这里我们没有异步的概念,只有同样的一次循环回来,有了两种队伍,一种优先上车的队伍叫做<code>MicroTask Queue</code>,而你和你的同事这帮壮汉组成的队伍就是宏队伍(<code>MacroTask Queue</code>)。</p>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101807466-597730231.png"></p>
<p>一句话理解:一次事件循环回来后,开始去执行 <code>Task Queue</code> 中的 <code>task</code>,但是这里的 <code>task</code> 有<strong>优先级</strong>。所以优先执行 <code>MicroTask Queue</code> 中的 task<br>
,执行完后在执行<code>MacroTask Queue</code> 中的 task</p>
<h4 id="小试牛刀-1">小试牛刀</h4>
<p>理论都扯完了,也不知道你懂没懂。来,期中考试了!</p>
<pre><code>console.log('script start');

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

Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
</code></pre>
<blockquote>
<p>没必要搞个 setTimeout 有加个 Promise,Promise 里面再整个 setTimeout 的例子。因为只要上面代码你懂了,无非就是公交再来一趟而已!</p>
</blockquote>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101803115-472057592.gif"></p>
<p>如果说了这么多,还是没能理解上图,那么公众号内回复【1】,手摸手指导!</p>
<h2 id="node-环境下的-event-loop">Node 环境下的 Event Loop</h2>
<p>Node中的<code>Event Loop</code>是基于<code>libuv</code>实现的,而<code>libuv</code>是 Node 的新跨平台抽象层,<code>libuv</code>使用异步,事件驱动的编程方式,核心是提供<code>i/o</code>的事件循环和异步回调。<code>libuv</code>的<code>API</code>包含有时间,非阻塞的网络,异步文件操作,子进程等等。</p>
<p>Event Loop就是在<code>libuv</code>中实现的。所以关于 Node 的 <code>Event Loop</code>学习,有两个官方途径可以学习:</p>
<ul>
<li>libuv 文档</li>
<li>官网的What is the Event Loop?.</li>
</ul>
<p>在学习 Node 环境下的 <code>Event Loop</code> 之前呢,我们首先要明确执行环境,Node 和浏览器的Event Loop是两个有明确区分的事物,不能混为一谈。nodejs的event是基于libuv,而浏览器的event loop则在html5的规范中明确定义。</p>
<pre><code>   ┌───────────────────────────┐
┌─&gt;│         timers          │
│└─────────────┬─────────────┘
│┌─────────────┴─────────────┐
││   pending callbacks   │
│└─────────────┬─────────────┘
│┌─────────────┴─────────────┐
││       idle, prepare       │
│└─────────────┬─────────────┘      ┌───────────────┐
│┌─────────────┴─────────────┐      │   incoming:   │
││         poll            │&lt;─────┤connections, │
│└─────────────┬─────────────┘      │   data, etc.│
│┌─────────────┴─────────────┐      └───────────────┘
││         check         │
│└─────────────┬─────────────┘
│┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘
</code></pre>
<p>Node 的 Event Loop 分为 6 个阶段:</p>
<ul>
<li>timers:执行<code>setTimeout()</code> 和 <code>setInterval()</code>中到期的callback。</li>
<li>pending callback: 上一轮循环中有少数的<code>I/O</code> callback会被延迟到这一轮的这一阶段执行</li>
<li>idle, prepare:仅内部使用</li>
<li>poll: 最为重要的阶段,执行<code>I/O</code> callback,在<strong>适当的条件下</strong>会阻塞在这个阶段</li>
<li>check: 执行<code>setImmediate</code>的callback</li>
<li>close callbacks: 执行<code>close</code>事件的callback,例如<code>socket.on('close'[,fn])</code>、<code>http.server.on('close, fn)</code></li>
</ul>
<blockquote>
<p>上面六个阶段都不包括 process.nextTick()(下文会介绍)</p>
</blockquote>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101813208-75146895.png"></p>
<p>整体的执行机制如上图所示,下面我们具体展开每一个阶段的说明</p>
<h3 id="timers-阶段">timers 阶段</h3>
<p>timers 阶段会执行 <code>setTimeout</code> 和 <code>setInterval</code> 回调,并且是由 poll 阶段控制的。</p>
<p>在 timers 阶段其实使用一个最小堆而不是队列来保存所有的元素,其实也可以理解,因为timeout的callback是按照超时时间的顺序来调用的,并不是先进先出的队列逻辑)。而为什么 timer 阶段在第一个执行阶梯上其实也不难理解。在 Node 中定时器指定的时间也是不准确的,而这样,就能尽可能的准确了,让其回调函数尽快执行。</p>
<p>以下是官网给出的例子:</p>
<pre><code>const fs = require('fs');

function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}

const timeoutScheduled = Date.now();

setTimeout(() =&gt; {
const delay = Date.now() - timeoutScheduled;

console.log(`${delay}ms have passed since I was scheduled`);
}, 100);

// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() =&gt; {
const startCallback = Date.now();

// do something that will take 10ms...
while (Date.now() - startCallback &lt; 10) {
    // do nothing
}
});
</code></pre>
<p>当进入事件循环时,它有一个空队列(<code>fs.readFile()</code>尚未完成),因此定时器将等待剩余毫秒数,当到达95ms时,<code>fs.readFile()</code>完成读取文件并且其完成需要10毫秒的回调被添加到轮询队列并执行。</p>
<p>当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的阈值,然后回到timers阶段以执行定时器的回调。<br>
在此示例中,您将看到正在调度的计时器与正在执行的回调之间的总延迟将为105毫秒。</p>
<h3 id="pending-callbacks-阶段">pending callbacks 阶段</h3>
<p>pending callbacks 阶段其实是 <code>I/O</code> 的 callbacks 阶段。比如一些 TCP 的 error 回调等。</p>
<p>举个栗子:如果<code>TCP socket ECONNREFUSED</code>在尝试<code>connect</code>时<code>receives</code>,则某些* nix系统希望等待报告错误。 这将在pending callbacks阶段执行。</p>
<h3 id="poll-阶段">poll 阶段</h3>
<p>poll 阶段主要有两个功能:</p>
<ul>
<li>执行 <code>I/O</code> 回调</li>
<li>处理 poll 队列(poll queue)中的事件</li>
</ul>
<p>当时Event Loop 进入到 poll 阶段并且 timers 阶段没有任何可执行的 task 的时候(也就是没有定时器回调),将会有以下两种情况</p>
<ul>
<li>如果 poll queue 非空,则 Event Loop就会执行他们,知道为空或者达到system-dependent(系统相关限制)</li>
<li>如果 poll queue 为空,则会发生以下一种情况
<ul>
<li>如果setImmediate()有回调需要执行,则会立即进入到 check 阶段</li>
<li>相反,如果没有setImmediate()需要执行,则 poll 阶段将等待 callback 被添加到队列中再立即执行,这也是为什么我们说 poll 阶段可能会阻塞的原因。</li>
</ul>
</li>
</ul>
<p>一旦 poll queue 为空,Event Loop就回去检查timer 阶段的任务。如果有的话,则会回到 timer 阶段执行回调。</p>
<h3 id="check-阶段">check 阶段</h3>
<p>check 阶段在 poll 阶段之后,<code>setImmediate()</code>的回调会被加入check队列中,他是一个使用<code>libuv API</code> 的特殊的计数器。</p>
<p>通常在代码执行的时候,Event Loop 最终会到达 poll 阶段,然后等待传入的链接或者请求等,但是如果已经指定了setImmediate()并且这时候 poll 阶段已经空闲的时候,则 poll 阶段将会被中止然后开始 check 阶段的执行。</p>
<h3 id="close-callbacks-阶段">close callbacks 阶段</h3>
<p>如果一个 socket 或者事件处理函数突然关闭/中断(比如:<code>socket.destroy()</code>),则这个阶段就会发生 <code>close</code> 的回调执行。否则他会通过 <code>process.nextTick()</code> 发出。</p>
<h3 id="setimmediate-vs-settimeout">setImmediate() vs setTimeout()</h3>
<p><code>setImmediate()</code> 和 <code>setTimeout()</code>非常的相似,区别取决于谁调用了它。</p>
<ul>
<li><code>setImmediate</code>在 poll 阶段后执行,即check 阶段</li>
<li><code>setTimeout</code> 在 poll 空闲时且设定时间到达的时候执行,在 timer 阶段</li>
</ul>
<p>计时器的执行顺序将根据调用它们的上下文而有所不同。 如果两者都是从主模块中调用的,则时序将受到进程性能的限制。</p>
<p>例如,如果我们运行以下不在<code>I / O</code>周期(即主模块)内的脚本,则两个计时器的执行顺序是不确定的,因为它受进程性能的约束:</p>
<pre><code>// timeout_vs_immediate.js
setTimeout(() =&gt; {
console.log('timeout');
}, 0);

setImmediate(() =&gt; {
console.log('immediate');
});
</code></pre>
<pre><code>$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout
</code></pre>
<p>如果在一个<code>I/O</code> 周期内移动这两个调用,则始终首先执行立即回调:</p>
<pre><code>// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () =&gt; {
setTimeout(() =&gt; {
    console.log('timeout');
}, 0);
setImmediate(() =&gt; {
    console.log('immediate');
});
});
</code></pre>
<pre><code>$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout
</code></pre>
<p>所以与<code>setTimeout()</code>相比,使用<code>setImmediate()</code>的主要优点是,如果在<code>I / O</code>周期内安排了任何计时器,则<code>setImmediate()</code>将始终在任何计时器之前执行,而与存在多少计时器无关。</p>
<h3 id="nexttick-queue">nextTick queue</h3>
<p>可能你已经注意到<code>process.nextTick()</code>并未显示在图中,即使它是异步API的一部分。 所以他拥有一个自己的队列:<code>nextTickQueue</code>。</p>
<p>这是因为<code>process.nextTick()</code>从技术上讲不是Event Loop的一部分。 相反,无论当前事件循环的当前阶段如何,都将在当前操作完成之后处理<code>nextTickQueue</code>。</p>
<p>如果存在 <code>nextTickQueue</code>,就会清空队列中的所有回调函数,并且优先于其他 <code>microtask</code> 执行。</p>
<pre><code>setTimeout(() =&gt; {
console.log('timer1')
Promise.resolve().then(function() {
   console.log('promise1')
})
}, 0)
process.nextTick(() =&gt; {
console.log('nextTick')
process.nextTick(() =&gt; {
   console.log('nextTick')
   process.nextTick(() =&gt; {
   console.log('nextTick')
   process.nextTick(() =&gt; {
       console.log('nextTick')
   })
   })
})
})
// nextTick=&gt;nextTick=&gt;nextTick=&gt;nextTick=&gt;timer1=&gt;promise1
</code></pre>
<h3 id="processnexttick-vs-setimmediate">process.nextTick() vs setImmediate()</h3>
<p>从使用者角度而言,这两个名称非常的容易让人感觉到困惑。</p>
<ul>
<li><code>process.nextTick()</code>在同一阶段立即触发</li>
<li><code>setImmediate()</code>在事件循环的以下迭代或“tick”中触发</li>
</ul>
<p>貌似这两个名称应该呼唤下!的确~官方也这么认为。但是他们说这是历史包袱,已经不会更改了。</p>
<blockquote>
<p>这里还是建议大家尽可能使用setImmediate。因为更加的让程序可控容易推理。</p>
</blockquote>
<p>至于为什么还是需要 <code>process.nextTick</code>,存在即合理。这里建议大家阅读官方文档:why-use-process-nexttick。</p>
<h2 id="node与浏览器的-event-loop-差异">Node与浏览器的 Event Loop 差异</h2>
<p>一句话总结其中:<strong>浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。</strong></p>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101813614-1351187797.png"></p>
<blockquote>
<p>上图来自浪里行舟</p>
</blockquote>
<h2 id="最后">最后</h2>
<p>来~期末考试了</p>
<pre><code>console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
      console.log('3');
    })
    new Promise(function(resolve) {
      console.log('4');
      resolve();
    }).then(function() {
      console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
      console.log('10');
    })
    new Promise(function(resolve) {
      console.log('11');
      resolve();
    }).then(function() {
      console.log('12')
    })
})
</code></pre>
<p>评论区留下你的答案吧~~老铁!</p>
<h2 id="参考文献">参考文献</h2>
<ul>
<li>Tasks, microtasks, queues and schedules</li>
<li>libuv 文档</li>
<li>The Node.js Event Loop, Timers, and process.nextTick()</li>
<li>node 官网</li>
<li>async/await 在chrome 环境和 node 环境的 执行结果不一致,求解?</li>
<li>更快的异步函数和 Promise</li>
<li>一次弄懂Event Loop(彻底解决此类面试问题)</li>
<li>这一次,彻底弄懂 JavaScript 执行机制</li>
<li>不要混淆nodejs和浏览器中的event loop</li>
</ul>
<h3 id="学习交流">学习交流</h3>
<p>关注公众号: 【全栈前端精选】 每日获取好文推荐。</p>
<p>公众号内回复 【1】,加入全栈前端学习群,一起交流。</p>
<p><img src="https://img2018.cnblogs.com/blog/1814386/201909/1814386-20190929101815437-1868148426.png"></p><br><br>
来源:https://www.cnblogs.com/Nealyang/p/11606260.html
頁: [1]
查看完整版本: 【THE LAST TIME】彻底吃透 JavaScript 执行机制