Eventloop的秘密

3,011 阅读7分钟

写在前面
面过前端的小伙伴们都见过这么一道关于异步的小题:

for(var i = 0; i<10; i++){
    setTimeout(function(){
        console.log(i)
    },1000)
}

稍微了解异步的同学都会对答案呼之欲出。BUT! 如果问题升级为:

setTimeout(function(){
    console.log(1)
},0)
new Promise(function executor(resolve) {
  console.log(2)
  for(var j = 0;j<100;j++){
    j=99&&resolve()
  }
  console.log(3)
}).then(function(){
    console.log(4)
})
console.log(5)

是不是稍微的有那么点小蒙圈? 别着急,本篇内容结束后以上问题都不再是事儿。解决以上问题的要点,首先需要清楚Javascript异步处理模块,事件队列,以及事件环-Eventloop.

基础概念

进程 (process)与线程 (thread)

进程是操作系统分配资源和调度任务的基本单位,线程是建立在进程上的一次程序运行单位,一个进程上可以有多个线程。
进程和线程属基础概念,不再赘述。有个最生动易懂的解释,详情请移步参考:进程与线程的一个简单解释

Javascript 单线程

对Javascript而言,从诞生之日起,它就是单线程的。为什么呢?举个小栗子:如果可以多线程,a线程要添加某DOM节点,b线程要删除它,浏览器怎么办?难道要精分? 所以说,单线程减少了很多情境的复杂性。
既然js是单线程的,它又以什么样的规则来处理并发的任务呢?千军万马要过独木桥的时候,不能靠力气来抢路。单线程,任务多,就得有个规矩来安排大家。于是秘密终于被我们发现——事件环(Event Loop)就要出马了。 对于首次听说这个概念的同学,有必要铺垫下基础知识:

堆(heap)

对象被分配在一个堆中,即用以表示一个大部分非结构化的内存区域。

栈(stack)

函数调用形成一个栈帧;

栈的特点:先进后出(First in, last out,具体是怎样让那些函数先入后出的?看下图会恍然大明白,图中的帅哥是Philip Roberts,看解释,别光看脸!

gif图有点大,如果图裂了,请直接看Philip Roberts的演讲:Help, I'm stuck in an event-loop

任务队列(queue)—— 特点:先进先出(FIFO)

一个 JavaScript 运行时包含了一个待处理的消息队列。 每一个消息都有一个为了处理这个消息相关联的函数

说到了任务队列,就到了重点部分:事件环(Eventloop)了

Defined by webappapis:

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section.

任务队列以事件环来协调事件,用户交互,脚本,渲染,网络等。

事件环 Eventloop

  • Explained by Jake Archibald

    Each 'thread' gets its own event loop, so each web worker gets its own, so it can execute independently

  • Or explained by some other guys:

    This is a constantly running process that checks if the call stack is empty. Imagine it like a clock and every time it ticks it looks at the Call Stack and if it is empty it looks into the Event Queue.

简单来说,每个线程都有他自己的事件环,浏览器也拥有自己的事件环;事件环是一种运行时机制,它像个钟表一样,每滴答一下,就去看看stack里有没有事需要处理,没有的话就去事件队列(Event Queue)看看有没有事做。
此处大家需要明白,事件环并不是定死的某个规矩,需要根据不同的运行时进行自己的一套规则。

There are two kinds of event loops: those for browsing contexts, and those for workers.--from webapis.

node下的事件环浏览器环境下的事件环就不是相同的规则。一定要记清楚哦!首先讨论浏览器事件环。

浏览器事件环


依据webapis里声明的事件环(event loop)存在时的执行步骤,概括如下:

  1. 首先执行script,script被称为全局任务,也属于macrotask;
  2. 当macrotask执行完以下,执行所有的微任务;
  3. 微任务全部执行完,再取任务队列中的一个宏任务执行。
  • 宏任务包括:script, setTimeout, setInterval, setImmediate, I/O
  • 微任务包括:process.nextTick(node api), 原生Promise(有些实现的promise将then方法放到了宏任务中),Object.observe(已废弃), MutationObserver

一图顶千言,作图解释下:

看似按照这个规则,我们文章开头的问题就可以有答案了。 回顾一下:

setTimeout(function(){
    console.log(1)
},0)
new Promise(function executor(resolve) {
  console.log(2)
  for(var j = 0;j<100;j++){
    j=99&&resolve()
  }
  console.log(3)
}).then(function(){
    console.log(4)
})
console.log(5)

代码执行后,按照执行顺序:

  1. script执行:2,3,5(均为同步任务,new Promise会在同步代码中执行)
  2. 处理微任务队列中的所有任务:then 4
  3. 接着执行下一个宏任务: setTimeout 1 所以执行结果为: 2,3,5,4,1

微任务

Explained by webappapis

Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue. There are two kinds of microtasks: solitary callback microtasks, and compound microtasks.

翻译:每个eventloop都有一个微任务队列,微任务最初是被放到微任务队列而不是任务队列

这里大家请把这句话放在心里,这是解决宏任务的一把钥匙所在。

宏任务到底是什么

虽然知道任务队列分为宏任务,微任务,但是一直未找到宏任务的定义,直到看到stackoverflow上的解释。

One go-around of the event loop will have exactly one task being processed from the macrotask queue (this queue is simply called the task queue in the WHATWG specification).

每个事件环必须有一个来自宏任务队列的任务正在执行。 这里将宏任务解释为whatwg上定义的任务队列.
是不是有点迷惑,这不对啊? 虽然把任务队列分成宏任务,微任务理解比较容易,但我认为这后面更大的秘密才是我苦苦找寻的真相。 如果只执行宏任务队列,那微任务队列怎么执行呢?
这正好与上文中提到微任务队列是单独的队列,跟任务队列不是一回事符合。 整个任务可以理解为,所有的宏任务放在一个宏任务队列(即任务队列),处理完一个宏任务(从sccript开始),将微任务队列(包含当时所有的微任务)压入任务队列(宏任务队列)并执行,之后再取下一个任务队列(宏任务)中的宏任务。

于是,我们的题目可以翻译成以下解决思路:

  1. 宏任务队列中script执行:2,3,5
  2. 微任务队列压入宏任务队列(任务队列)并执行:4
  3. 取下一个宏任务执行:1

多说一句

事件环eventloop中为什么必须在所有的微任务microtask都执行结束后再取新的宏任务macrotask呢?

这涉及microtask执行机制

step2中做了明确的解释,当微任务队列 的标记被写为true之后,只要microtask的队列不为空,eventloop中的当前任务就会按顺序执行microtask队列中的所有任务。
这里可以看到两者的一点区别,微任务microtask队列是独立的一个队列,在eventloop执行过程中才进入到任务队列task queue一次执行。

写在最后

没有尽兴的朋友推荐以下几篇好文,这些是我个人认为讲解事件环,异步事件队列等最为具体清晰的文章:
Best reference:

  1. Tasks, microtasks, queues and schedules by Jake Archibald
  2. How does Javascript event work? or
    Help, I'm stuck in an event-loop by Philip Roberts
  3. stackoverflow.com/questions/2…

其他参考

  1. microtasks-macrotasks-more-on-the-event-loop
  2. event-loop-timers-and-nexttick
  3. node process_nextTick_api
  4. microtask-queue
  5. webapis:task-queue
  6. webapis:event-loops

希望大家看文本文能有收货。欢迎批评指正。

Author: Yanni Jia
Nickname: 非常兔
Email: 385067638@qq.com