事件循环

ⅰ亾dé卋堺 提交于 2019-12-04 16:44:58

说在前头:事件循环和js语言本身没有关系,和js引擎也没有关系。

先看一个题目:

setTimeout(()=>{
  console.log(1);
},10)

for (let i=0; i<100000000; i++) {}

console.log(2);

setTimeout(()=>{
  console.log(3);
},0)

Promise.resolve().then(function(){
   console.log(4);
})

console.log(5);

答案是 2 5 4 1 3 或 2 5 4 3 1。 这取决于这个for循环执行耗时。

先思考一个问题,我们的javascript代码是如何在浏览器或者是Node环境跑起来的。真正执行javascript代码的是谁?
保证我们代码有序执行的都有哪些参与者?

执行javascript代码的是javascript引擎(Engine),比如chrome里是V8,safari里是javascriptCore。要想执行javascript代码只有引擎是不行的,还得有宿主环境,也叫javascript运行时环境(Runtime),比如浏览器或者Node。宿主给引擎一段js代码,引擎才开始执行js代码。

在浏览器里js代码的执行是多个角色参与,看图:

  • Call stack (js引擎内)
  • Web API (浏览器提供)
  • Callback queue (事件队列对应的回调队列)

事件循环

所有的宿主环境都有一个共同点就是都实现了一个叫 Event Loop(事件循环)的内置机制。它来通过多次调用javascript引擎来调度程序中多个模块的执行顺序。

Event Loop有一个简单的工作机制——就是去监视Call Stack和Callback Queue。 如果调用栈为空,它将从Callback队列中取出第一个事件回调,并将其推送到调用栈,调用栈开始顺序执行代码。
而Callback Queue里的回调是宿主环境在一定的时机推入的。

现在我们看开头那断代码,这里我们先不考虑Promise:

// 标号1
setTimeout(()=>{
  console.log(1);
},10)
//标号5
for (let i=0; i<100000000; i++) {}
//标号2
console.log(2);
//标号3
setTimeout(()=>{
  console.log(3);
},0)
//标号4
console.log(5);

当宿主环境将这段代码给到js引擎时,引擎创建一个常驻内存的调用栈开始顺序执行代码。
这段代码可能是一段script内的代码也可能是一个模块代码,我们暂且将其用一个名为main函数指代。
最初调用栈的栈里只有一个main。随着代码顺序执行:

  1. 标号1的setTimeout函数入栈,执行完出栈
  2. 标号2的console.log函数入栈,执行完出栈
  3. 标号3的setTimeout函数入栈,执行完出栈
  4. 标号4console.log函数入栈,执行完出栈
  5. 此时main也执行完了,出栈,调用栈空了

注:这里说的函数入栈即所得函数上下文入栈。

接下来关键Event Loop该登场了,Event Loop检测到调用栈空了,说明当前迭代已经执行完毕,就会去Callback Queue 取队首的事件回调,并将其推入调用栈,下一轮迭代就开始了。

到这里我们好像忽略了一件事情,Callback Queue 里有东西吗?有的话是什么时候有的呢?
我们再看第一次迭代中的步骤1,setTimeout函数是浏览器提供的api, js引擎告诉浏览器10ms后给我做点事(传入的回调),此时浏览器就开启了一个定时器,并保存下这个回调函数,定时器时间到了后,将这个回调推入Callback Queue。

所以如果说标号5(示例代码中, 下同)的for循环执行耗时,会影响标号1和标号3两个setTimeout的回调执行顺序。

如果在执行标号3的setTimeout时,标号1的setTimeout时间已到,则标号1的回调会先于标号3的回调被推入Callback Queue。所以在第二次迭代时,从Callback Queue队首取到的是
标号1的回调,接下来的执行过程:

  1. console.log(1) 入栈,执行完出栈
  2. 调用栈为空,Even 大专栏  事件循环t Loop从Callback Queue取出此时队首的事件回调并推入调用栈
  3. console.log(3) 入栈,执行完出栈
  4. 此时调用栈为空,Callback Queue也为空,js引擎就可以先休息会了。

我在mbp的chrome 运行上面代码打印顺序是:

2 5 1 3

我们可以把for循环的次数调小至100,再次查看打印顺序,来验证上面的论证。此时的打印顺序是:

2 5 3 1

宏任务和微任务

在上面例子中的setTimeout是由浏览器这个宿主提供的api,这个异步任务也是有其发起的。

宿主环境发起的任务叫宏任务。

在ES5及更早版本,javascript本身是没有异步执行代码的能力的。宿主环境把一段代码给js引擎,引擎就把这段代码顺次执行了,而这个任务就是宿主发起的任务。
ES6引入了Promise,这使得不用依赖宿主环境,javascript引擎自己也可以发起任务了。

javascript引擎发起的任务叫微任务(对应ES规范里的job)

微任务通常来说就是需要在当前宏任务执行结束后立即执行的任务。所有的微任务会在下一个宏任务执行之前执行完毕。

我们再看开头的带有promise的例子,这里我们简化一下:

 // 标号1
setTimeout(()=>{
    console.log(3);
},0)
// 标号2
Promise.resolve().then(function(){
   console.log(4);
})
// 标号3
console.log(5);
  1. 标号1的timeout入栈,执行完毕出栈;同时浏览器开一个定时器,立即将该回调函数推入Callback Queue
  2. 标号2的promise入栈,执行完毕出栈;同时javascript引擎发起了一个微任务,promise的then回调会被推入本轮迭代的队尾。
  3. 标号3的console.log(5)入栈,执行完毕出栈
  4. 关键的一步来了,正常是本轮迭代已经执行完毕,栈也为空了,但是本轮迭代还有微任务的回调队列即标号2的promise的then回调,上面我们说了微任务永远会在本轮迭代完成,故先执行console.log(4);
  5. 此时本轮迭代的调用栈真的为空了,Event Loop从Callback Queue队首去取回调:console.log(3);

故打印顺序:

5 4 3

可以看到一个宏观任务可能会包含一个或多个微观任务,而我们的任务队列可以看做是一个二维数组:第一纬是宏观任务,第二维则是每一个宏观任务包含的微观任务。

参考资料

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!