【JavaScript】Event Loop
原文链接:《从 JS Event Loop 机制看 Vue 中 nextTick 的实现原理》
Event Loop 即事件循环机制,是理解 JavaScript 运行机制的最关键的一点,文章中通过抛出一道题来引入这节课所要讲解的内容。
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function executor(resolve) {
console.log(2);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(3);
}).then(function() {
console.log(4);
});
console.log(5);
// result: 2, 3, 5, 4, 1
单线程的 JavaScript
-
定义:所谓单线程,是指在 JS 引擎中负责解释和执行 JavaScript 代码的线程只有一个;
-
特点:JS 运行在浏览器中,是单线程的,每个 window 一个线程;
-
原因:若为多线程,在 dom 操作中会产生混乱,如 A 线程修改 dom,B 线程却删除了这个 dom;
-
效率:JavaScript 中有很多其他的类线程,也成为异步事件,如:Ajax请求,监控用户事件,定时器,读写文件等等。
-
过程:当异步事件发生时,将他们放入执行队列,(主线程)等待当前代码执行完成。就不会长时间阻塞主线程。等主线程的代码执行完毕,然后再读取任务队列,返回主线程继续处理。如此循环这就是事件循环机制。
JavaScript 的内存空间
-
栈数据结构
- 结构:
- 特点:先进后出,后进先出(FILO)
- 结构:
-
堆数据结构
- 结构:key - value 结构;
- 特点:存储的 key - value 是无序的,通过 key 取出,无需关心顺序;
-
队列数据结构
- 结构:
- 特点:先进先出,后进后出(FIFO)
- 结构:
执行上下文 (Execution Context) & 函数调用栈
- 每当控制器转到可执行代码的时候,就会进入一个执行上下文;
- 运行环境:
- 全局环境:JavaScript 代码运行起来会首先进入该环境;
- 函数环境:当函数被调用执行时,会进入当前函数中执行代码;
- 栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文;
- 需要注意的是:函数中,遇到 return 关键字能直接终止可执行代码的执行,因此会直接将当期那上下文弹出栈;
- 总结:
- 单线程,依次自顶而下执行,遇到函数就会创建函数执行上下文,并入栈;
- 同步执行,只有栈顶的上下文处于执行中,其他上下文需等待;
- 全局上下文只有一个,它在浏览器关闭时出栈;
- 函数执行上下文的个数没有限制;
- 每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此。
事件循环
-
在 JavaScript 代码执行的过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列来搞定另一些代码的执行;
-
特点:即任务队列的特点,先进先出;
-
图例:
-
任务队列:一个 JS 文件里事件循环只有一个,但是任务队列可以有多个,因此任务队列可以分为:
-
macro-task (task)
// macro-task (task) 包括: 1. setTimeout / setInterval; 2. setImmediate; 3. I/O operation; 4. UI Rendering
-
micro-task (job)
// micro-task (job) 包括: 1. process.nextTick; 2. Promise; 3. Object.observe (已废弃); 4. MutationObserver(html5新特性);
-
-
以上这些我们称他们为事件源,事件源作为任务分发器,他们的回调函数才是被分发到任务队列,而本身会立即执行,例如:
setTimeout
第一个参数被分发到任务队列,Promise
的then
方法的回调函数被分发到任务队列(catch
方法同理); -
不同事件源的事件被分发到不同的任务队列,其中 setTimeout 和 setInterval 属于同源;
-
流程:整体代码开始第一次循环。全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的 job。当所有可执行的 job 执行完毕之后。循环再次从 task 开始,找到其中一个任务队列执行完毕,然后再执行所有的 job,这样一直循环下去。
-
规律:task–job–task–job…,往复循环直到没有可执行代码。
-
有趣的栗子:
console.log(1);
// 执行到此, Promise 的回调是同步执行,then / catch 才会被分发到 job 中
new Promise(function(resolve){
console.log(2);
resolve();
}).then(function(){
console.log(3)
})
// 执行到此,setTimeout 执行将回调 function 分发至 task 中
setTimeout(function(){
console.log(4);
process.nextTick(function(){
console.log(5);
})
new Promise(function(resolve){
console.log(6);
resolve()
}).then(function(){
console.log(7)
})
})
// 执行到此, process.nextTick 的回调会被分发到 job 中
process.nextTick(function(){
console.log(8)
})
// 同 setTimeout 原理相同
setImmediate(function(){
console.log(9);
new Promise(function(resolve){
console.log(10);
resolve()
}).then(function(){
console.log(11)
})
process.nextTick(function(){
console.log(12);
})
})
// 最后结果: 1, 2, 8, 3, 4, 6, 5, 7, 9, 10, 12, 11
-
注意:
- 执行遇到了
Promise
,Promise
构造函数的回调函数是同步执行; nextTick
任务队列会比Promise
的队列先执行;
- 执行遇到了