简介
Javascript是单线程的。而在主线程之外,为了协调事件、用户交互、脚本、渲染、网络等,用户代理(user agents,一般指浏览器)必须使用一种机制来保证程序的正确运行。这种机制就是事件循环(event loops)。当主线程(栈)执行完毕后,就开始事件循环。事件循环有两种形式,分别是browsing contexts、workers(Web Worker)
Browsing contexts是一个浏览器展示Document的环境(一般来说是一个浏览器标签页,但也有可能是一个页面内的window或者frame)。
Web Works是H5的一个API,用来开辟一个新的线程。在Worker内部不能访问主程序的任何资源。与主程序的交互通过postMessage
和message
事件。但是Worker内部仍可以执行网络操作(Ajax、WebSocket)以及定时器等。所以这里也有独立的事件循环。
关键点
有几个概念需要知道。
- 一个事件循环有一个或者多个task queues。
- 事件循环中的每一个task和Document相关联。如果这个任务在一个元素的上下文排队,那么这个任务是该元素的node document。
- 每一个任务来自于某个特定的任务源(task source)。
- DOM操作事件源(添加删除dom元素)
- 用户交互事件源
- 网络事件源(响应网络活动)
- history事件源(history.back()等)
- 当用户代理添加一个任务的时候,根据任务源不同,会被添加到不同的task queues。
- 每个task queues保证任务是先进先出的。
- 对于不同的task queues,用户代理可能分配的时间不同。比如对于鼠标和键盘事件所在的task queues,用户代理可能分配3/4的时间,同时也不会让其他的task queues得不到执行。
- 每一个事件循环有一个currently running task(当前运行的任务)。开始,它是null。每当执行一个任务的时候,currently running task即这个正在执行的任务。执行完成后,被重置为null。
Jobs and Job Queues and promise
在讲macro-task和micro-task之前,这部分内容起到承上启下的作用。该部分内容在es6(ecma-262)规范。
Job只能在没有running execution context和execution context stack为空的时候执行(我的猜测是当没有正在执行的任务并且主线程栈没有任务的时候)。一个PendingJob是一个对于未来执行的Job的请求。
一个Job Queue是一个FIFO(先进先出)的关于PendingJob records的队列。每一个Job Queue都有一个ECMAScript实现(implementation)的名字。每一个ECMAScript实现至少有以下两个Job Queue。
Name | Purpose |
---|---|
ScriptJobs | Jobs that validate and evaluate ECMAScript Script and Module source text. See clauses 10 and 15. |
PromiseJobs | Jobs that are responses to the settlement of a Promise (see 25.4). |
这里强调以下,PromiseJobs是用来处理Promise的。
在ecma-262规范25.4.5.3.1可以看到,对于一个promise.then的操作,不管结果是被fulfilled(resolve)还是rejected(reject),最终都会被添加到PromiseJobs。promise.catch会调用promise.then。
在Promise/A+规范2.2.4也提到:
onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
也就是说只有当execution context stack只包含平台代码不包含项目代码的时候,resolve和reject才会执行。那么什么是平台代码呢?
Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.
平台代码指引擎、环境和promise实现代码。实际上,这要求确保resolve和reject是在事件循环执行到then函数后异步执行。这种异步执行的机制可以通过macro-task
机制(比如setTimeout
和setImmediate
)实现,也可以通过micro-task
机制(比如MutationObserver
或者process.nextTick
)实现。因为promise实现考虑到平台代码,promise自己可能包括一个task-scheduling
队列或者trampoline
。
但是根据上文ecma-262规范,promise.then中执行的函数会被添加到PromiseJobs队列。
micro-task & macro-task
实际上,ecma-262规范中并没有提到macro-task。但是根据某些大神的博文和某些相关概念推测,规范中的task queue即macro-task queue。job queue即micro-task queue。所以promise.then中的函数会被添加到micro-task。结合上述的Promise/A+规范,我们可以得到一些概念:
macro-task: setTimeout、setImmediate、传递事件对象Event、HTML解析、任务的回调、请求网络资源完成、操作DOM(除了前面两个,后面的来自于whatwg规范)
micro-task: mutationObserver、process.nextTick(和宿主环境有关)、promise.then
有了上述基础,下面来谈谈事件循环机制。
事件循环
如果用一段伪代码来表示事件循环:
var someTaskQueue = [];
var task;
while(true) {
if(someTaskQueue.length > 0) {
task = someTaskQueue.shift(); // 先进先出
try {
task();
} catch (err) {
handleError(err);
}
}
}
每一轮循环称作一次tick。每一次tick都会执行事件循环队列中的一个任务。这里的任务即macro-task。所以每次tick都执行一个macro-task。那什么时候执行micro-task呢?这确实是个问题。在《你不知道的JavaScript中卷》看到:
我认为对于任务队列(这里指micro-task,原文是job queue)最好的理解方式就是,它是挂在事件循环队列的每个tick(每个macro-task)之后的一个队列。在事件循环的每个tick中,可能出现的异步动作(process.then等)不会导致一个完成的新事件添加到事件循环队列中(不会成为一个新的tick,节约资源),而会在当前tick的任务队列(micro-task)末尾添加一个任务。
也就是说,micro-task队列会挂载当前tick(某个macro-task)的尾部。当当前这个macro-task执行完毕后,就会执行micro-task队列。理论上来说,micro-task会导致无限循环(比如process.nextTick中一直调用process.nextTick),导致无法进行下一个macro-task。
whatwg规范8.1.4.2中提到了event loop具体是怎么执行的。大致如下:
- 找到一个oldest Task(macro-task)执行,如果找不到,就进入micro-task。
- 设置事件循环的currently running task为这个任务。
- 运行这个任务(原子操作)。
- 设置事件循环的currently running task为null。
- 从macro-task队列中移除该oldest Task。
- 运行micro-task(步骤同上)。
- 更新rendering(涉及到一系列操作)。
- 处理Web Worker开辟的事件循环(如果有的话)。
好吧,一个简单的问题
setImmediate(function(){
console.log(1);
},0);
setTimeout(function(){
console.log(2);
},0);
new Promise(function(resolve){
console.log(3);
resolve();
console.log(4);
}).then(function(){
console.log(5);
});
console.log(6);
process.nextTick(function(){
console.log(7);
});
console.log(8);
作者:何幻
链接:http://www.jianshu.com/p/3ed992529cfc
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
如果还有不明白的,可以去上面网址看看解析。new Promise是一个构造函数,其中的代码是同步的。所以会在主线程执行。所以3 4会首先打印。
参考
Browsing contexts from https://whatwg-cn.github.io
Browsing contexts from https://developer.mozilla.org
Event loops from https://whatwg-cn.github.io
[JavaScript] Macrotask Queue和Microtask Queue from http://www.jianshu.com
从Promise来看JavaScript中的Event Loop、Tasks和Microtasks from https://github.com/creeperyang
Promise/A+ from https://promiseaplus.com
MutationObserver from https://developer.mozilla.org
Vue 中如何使用 MutationObserver 做批量处理? from https://www.zhihu.com