JavaScript 事件循环及异步原理(完全指北)

引言

JS单线程
  1. JS 是单线程的,只有一个主线程
  2. 函数内的代码从上到下顺序执行,遇到被调用的函数先进入被调用函数执行,待完成后继续执行
  3. 遇到异步事件,浏览器另开一个线程,主线程继续执行,待结果返回后,执行回调函数
JS浏览器环境nodeJs环境
NodeNode.jslibuvNodelibuv
微任务宏任务JS
为什么 是单线程的
JavaScriptJavaScriptDOM

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

HTML5CPUJavaScriptDOMJavaScript
函数调用栈与任务队列

函数调用栈

JavaScriptcall stack

这类似于一个乒乓球桶,第一个放进去的乒乓球会最后一个拿出来。

举个栗子:

function a() {
console.log(“I‘m a!”);
}; function b() {
a();
console.log(“I’m b!”);
}; b();

执行过程如下所示:

main.jsmain.jsb()b()b()main.jsb()a()a()a()b()main.jsa()I‘m a!a()b()main.jsb()I’m b!b()main.jsmain.jsb()

这就是一个简单的调用栈,在调用栈中,前一个函数在执行的时候,下面的函数全部需要等待前一个任务执行完毕,才能执行。

JavaScript任务队列

任务队列

同步任务(synchronous)异步任务(asynchronous)

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

“任务队列”(task queue)“任务队列” 
setTimeoutsetTimeout

当然,一般不同的异步任务的回调函数会放入不同的任务队列之中。等到调用栈中所有任务执行完毕之后,接着去执行任务队列之中的回调函数。

用一张图来表示就是:

ChromewebcorewebcoreDOM Bindingnetworktimer

我们先来看一个有意思的现象,我运行一段代码,大家觉得输出的顺序是什么:

  setTimeout(() => {

console.log(&#39;setTimeout&#39;)<br/>

}, 22)
for (let i = 0; i++ &lt; 2;) {

i === 1 &amp;&amp; console.log(&#39;1&#39;)<br/>

}
setTimeout(() =&gt; {

console.log(&#39;set2&#39;)<br/>

}, 20)
for (let i = 0; i++ &lt; 100000000;) {

i === 99999999 &amp;&amp; console.log(&#39;2&#39;)<br/>

}

没错!结果很量子化:

那么这实际上是一个什么过程呢?那我就拿上面的一个过程解析一下:

setTimeoutsetTimeoutWebapisAPItimer

setTimeout异步APIapp.js
setTimeout
  1. 调用栈顺序调用任务
  2. 当调用栈发现异步任务时,将异步任务交给其他模块处理,自己继续进行下面的调用
  3. 异步执行完毕,异步模块将任务推入任务队列,并通知调用栈
  4. 调用栈在执行完当前任务后,将执行任务队列里的任务
  5. 调用栈执行完任务队列里的任务之后,继续执行其他任务
事件循环(Event Loop)

那么,了解了这么多,小伙伴们能从事件循环上面来解析下面代码的输出吗?

  for (var i = 0; i &lt; 10; i++) {

setTimeout(() =&gt; {<br/>
  console.log(i)<br/>
}, 1000)<br/>

}
console.log(i)

解析:

varisetTimeoutconsole.log(i)ii1010i1010

用下图示意:

现在小伙伴们是否已经恍然大悟,从底层了解了为什么这个代码会输出这个内容吧:

那么问题又来了,我们看下面的代码:

  setTimeout(() =&gt; {

console.log(4)<br/>

}, 0);
new Promise((resolve) =&gt;{

console.log(1);<br/>
for (var i = 0; i &lt; 10000000; i++) {<br/>
  i === 9999999 &amp;&amp; resolve();<br/>
}<br/>
console.log(2);<br/>

}).then(() =&gt; {

console.log(5);<br/>

});
console.log(3);

大家觉得这个输出是多少呢?

promise123PromisesetTimeoutsetTimeoutPromise

1,2,3,5,4
宏任务和微任务

什么是宏任务和微任务

macro-task(宏任务)micro-task(微任务)taskjobs
macro-task(宏任务)script(整体代码)setTimeoutsetIntervalsetImmediate(NodeJs)I/OUI renderingmicro-task(微任务)process.nextTick(NodeJs)PromiseObject.observe(已废弃)MutationObserver(html5新特性)setTimeoutsetInterval
micro-task(微任务)micro-task(微任务)macro-task(宏任务)macro-task(宏任务)setTimeoutmicro-task(微任务)

解析

现在我就开始解析上面的代码。

scriptsetTimeoutPromisePromisePromisenew.thenmicro-taskPromiseapp.js 3app.jsmacro-task(宏任务)scriptmicro-task(微任务)micro-task(微任务)macro-task(宏任务)setTimeoutmacro-task(宏任务)setTimeout1,2,3,5,4

那么上面这个例子的输出结果就显而易见。大家可以自行尝试体会。

总结

macro-taskmicro-taskmicro-taskmacro-taskmacro-task(宏任务)script(整体代码)setTimeoutsetIntervalsetImmediate(NodeJs)I/OUI renderingmicro-task(微任务)process.nextTick(NodeJs)PromiseObject.observe(已废弃)MutationObserver(html5新特性)
进阶举例

那么,我再来一些有意思一点的代码:

&lt;script&gt;
setTimeout(() =&gt; {

console.log(4)<br/>

}, 0);
new Promise((resolve) =&gt; {

console.log(1);<br/>
for (var i = 0; i &lt; 10000000; i++) {<br/>
  i === 9999999 &amp;&amp; resolve();<br/>
}<br/>
console.log(2);<br/>

}).then(() =&gt; {

console.log(5);<br/>

});
console.log(3);
&lt;/script&gt;
&lt;script&gt;
console.log(6)
new Promise((resolve) =&gt; {

resolve()<br/>

}).then(() =&gt; {

console.log(7);<br/>

});
&lt;/script&gt;

这一段代码输出的顺序是什么呢?

其实,看明白上面流程的同学应该知道整个流程,为了防止一些同学不明白,我再简单分析一下:

script1scriptscript1script2script1script1script2
1,2,3,5,6,7,4

了解了上面的内容,我觉得再复杂一点异步调用关系你也能搞定:

setImmediate(() =&gt; {

console.log(1);<br/>

},0);
setTimeout(() =&gt; {

console.log(2);<br/>

},0);
new Promise((resolve) =&gt; {

console.log(3);<br/>
resolve();<br/>
console.log(4);<br/>

}).then(() =&gt; {

console.log(5);<br/>

});
console.log(6);
process.nextTick(()=&gt; {

console.log(7);<br/>

});
console.log(8);
//输出结果是3 4 6 8 7 5 2 1

终极测试
setTimeout(() =&gt; {

console.log(&#39;to1&#39;);<br/>
process.nextTick(() =&gt; {<br/>
    console.log(&#39;to1_nT&#39;);<br/>
})<br/>
new Promise((resolve) =&gt; {<br/>
    console.log(&#39;to1_p&#39;);<br/>
    setTimeout(() =&gt; {<br/>
      console.log(&#39;to1_p_to&#39;)<br/>
    })<br/>
    resolve();<br/>
}).then(() =&gt; {<br/>
    console.log(&#39;to1_then&#39;)<br/>
})<br/>

}) setImmediate(() =&gt; {

console.log(&#39;imm1&#39;);<br/>
process.nextTick(() =&gt; {<br/>
    console.log(&#39;imm1_nT&#39;);<br/>
})<br/>
new Promise((resolve) =&gt; {<br/>
    console.log(&#39;imm1_p&#39;);<br/>
    resolve();<br/>
}).then(() =&gt; {<br/>
    console.log(&#39;imm1_then&#39;)<br/>
})<br/>

}) process.nextTick(() =&gt; {

console.log(&#39;nT1&#39;);<br/>

})
new Promise((resolve) =&gt; {

console.log(&#39;p1&#39;);<br/>
resolve();<br/>

}).then(() =&gt; {

console.log(&#39;then1&#39;)<br/>

}) setTimeout(() =&gt; {

console.log(&#39;to2&#39;);<br/>
process.nextTick(() =&gt; {<br/>
    console.log(&#39;to2_nT&#39;);<br/>
})<br/>
new Promise((resolve) =&gt; {<br/>
    console.log(&#39;to2_p&#39;);<br/>
    resolve();<br/>
}).then(() =&gt; {<br/>
    console.log(&#39;to2_then&#39;)<br/>
})<br/>

}) process.nextTick(() =&gt; {

console.log(&#39;nT2&#39;);<br/>

}) new Promise((resolve) =&gt; {

console.log(&#39;p2&#39;);<br/>
resolve();<br/>

}).then(() =&gt; {

console.log(&#39;then2&#39;)<br/>

}) setImmediate(() =&gt; {

console.log(&#39;imm2&#39;);<br/>
process.nextTick(() =&gt; {<br/>
    console.log(&#39;imm2_nT&#39;);<br/>
})<br/>
new Promise((resolve) =&gt; {<br/>
    console.log(&#39;imm2_p&#39;);<br/>
    resolve();<br/>
}).then(() =&gt; {<br/>
    console.log(&#39;imm2_then&#39;)<br/>
})<br/>

})
// 输出结果是:?

大家可以在评论里留言结果哟~