浏览器中的事件循环
总所周知 JS 运行在浏览器中,以单线程方式运行,每个window一个JS线程。那么浏览器是如何处理js中的I/O读取、用户点击、setTimeout等异步事件,并使其他js代码不被阻塞的呢?
浏览器中的 事件循环 就是其解决方式。简单来说浏览器中的事件循环的机制是将产生的异步事件产生的回调暂时存储在 事件队列 中,等到合适的时机再去执行队列中的异步事件的回调。
运行时概念
要了解浏览器中的事件循环,需要先弄明白两个重要的运行时概念。
栈
执行栈,函数调用时形成一个调用帧,并压入栈中,当函数返回时,则帧弹栈。
队列
任务队列,每一个任务都包含一个处理该任务的函数,当任务产生时,任务及其处理函数会被作为一个整体推入任务队列中(例如:一个setTimeout,到达时间时,setTimeOut及其回调函数会作为一个任务被推入任务队列中)。任务队列按照先进先出的顺序执行。当任务队列里的任务需要被处理的时候(即调用任务的处理函数时),将会被移出队列,调用其处理函数,此时形成一个调用帧,并压入执行栈。
此时执行栈中的调用帧,直到执行栈为空,然后再去处理队列中的另一个任务。
浏览器任务
浏览器中的任务分为两种:task(macroTask 宏任务)和microtask(微任务)。不同的任务按照不同的规则执行。
task
一个事件循环里有多个task queue,其中的包含多个任务,每个任务严格的按照先进先出的顺序执行。在一个task执行结束后下一个task执行之前,浏览器可对页面进行重新渲染。 task queue中包含:
- script整体代码
- 浏览器事件(鼠标事件、键盘事件等)
- 定时事件(setTimeout、setInterval、setImmediate)
- I/O事件(资源读取等)
- UI渲染
microTask
一个事件循环中包含一个microTask queue。 microTask queue包含:
- Promise
- Object.observe、MutationObserver
事件循环
执行至完成
一个任务完整的执行后,其他任务才会被执行。 即:执行栈中的调用帧,直到执行栈为空,然后再去处理队列中的另一个任务。
添加任务至队列
在浏览器中,当事件发生并且该事件绑定了事件监听时,该事件发生后的任务才会被添加至队列。
例如:为一个DOM元素button绑定onclick一个处理事件,只有当button元素上的click事件发生时,该事件发生后的任务会被添加至队列。
再例如: setTimeout 接受两个参数:待加入队列的任务和一个延迟。延迟代表了任务被添加至任务队列的时间,只有经过了延迟的时间,该任务才会被加入队列。添加至队列以后是否被处理,取决于队列里是否有其他任务。因此延迟的时间表示最少延迟时间,而非确切的等待时间。
事件循环进程模型
事件循环进程模型 步骤如下:
- 选择第一个进入到 task queue中的任务[task],如果task queue中没有任务,则直接进入第6步;
- 将当前事件循环的任务设置为第一步选出的[task];
- 执行任务[task];
- 将当前事件循环任务设置为null;
- 在task queue中删除执行完毕的[task]任务;
- microtask阶段:进入microtask检查点;
- 按照浏览器界面更新策略渲染界面;
- 返回第1步;
其中第6步,microtask阶段步骤如下:
- 设置microtask检查点标记为true;
- 重复检查microtask queue是否为空,若为空直接进入第3步;若不为空:
- 选择microtask queue中的第一个[microtask];
- 将当前事件循环的任务设置为第3步选出的[microtask];
- 执行任务[microtask];
- 将当前事件循环任务设置为null;
- 在microtask queue中删除执行完毕的[microtask]任务;
- 回到第2步
- 清理index database 事务;
- 设置microtask检查点的标记为false;
事件循环进程模型总结
在事件循环中,首先从task queue中选择最先进入的task执行,每执行完一个task都会检查microtask queue是否为空,若不为空则执行完microtsk queue中的所有任务。然后再选择task queue中最先进入的task执行,以此循环。
总结上述步骤为流程图:
用代码解释
代码栗子1:
console.log('这是开始');
setTimeout(function cb() {
console.log('这是来自第一个回调的消息');
}, 100);
console.log('这是一条消息');
setTimeout(function cb1() {
console.log('这是来自第二个回调的消息');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('这是结束');
复制代码
输出结果为:
这是开始
这是一条消息
这是结束
promise1
promise2
这是来自第二个回调的消息
这是来自第一个回调的消息
复制代码
步骤解析:
-
初始状态队列的信息为:
- task queue:run script;
- microtask queue:【空】;
-
从task queue中拿出run script执行;执行完成后从task queue中删除run script任务;
- 当前事件循环执行栈:run script;
- task queue:setTimeout2 callback;
- microtask queue:promise1 then;
输出:
这是开始
这是一条消息
这是结束
复制代码
-
进入microtask检查点;从microtask queue中拿出promise1 then执行;将promise2 then推入microtask queue;执行完成后从microtask queue中删除promise1 then任务;
- 当前事件循环执行栈:promise1 then;
- task queue:setTimeout2 callback;
- microtask queue:promise2 then;
输出:
这是开始
这是一条消息
这是结束
promise1
复制代码
-
查看microtask queue中是否还有任务;有则从microtask queue中拿出promise2 then执行;执行完成后从microtask queue中删除promise2 then任务;
- 当前事件循环执行栈:promise2 then;
- task queue:setTimeout2 callback;
- microtask queue:【空】;
输出:
这是开始
这是一条消息
这是结束
promise1
promise2
复制代码
-
到达100ms后,将setTimeout1 callback推入task queue;次步骤和3、4步无明确的前后关系,依据所设定的时间长短而定;setTimeout1 callback被推入task queue中以后不一定会立刻执行,因为task queue中可能存在其他任务尚未执行;因此setTimeout1 callback实际执行时间点>=100ms;
- task queue:setTimeout2 callback、setTimeout1 callback;
- microtask queue:【空】;
-
microtask queue为空,继续取出task queue中的任务setTimeout2 callback执行,执行完成后从task queue中删除setTimeout2 callback任务;
- 当前事件循环执行栈:setTimeout2 callback;
- task queue:setTimeout1 callback;
- microtask queue:【空】;
输出:
这是开始
这是一条消息
这是结束
promise1
promise2
这是来自第二个回调的消息
复制代码
-
检查microtask检查点,microtask queue为空,继续取出task queue中的任务setTimeout1 callback执行,执行完成后从task queue中删除setTimeout1 callback任务;
- 当前事件循环任务:setTimeout1 callback;
- task queue:【空】;
- microtask queue:【空】;
输出:
这是开始
这是一条消息
这是结束
promise1
promise2
这是来自第二个回调的消息
这是来自第一个回调的消息
复制代码
复杂的代码栗子2:
console.log('script start')
async function async1() {
await async2();
console.log('async1 end');
setTimeout(function() {
console.log('async1 setTimeout')
}, 0);
}
async function async2() {
console.log('async2 end');
setTimeout(function() {
console.log('async2 setTimeout')
}, 0);
}
async1();
setTimeout(function() {
Promise.resolve().then(function() {
console.log('setTimeout promise');
})
console.log('setTimeout');
}, 0);
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
复制代码
输出结果为:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
async2 setTimeout
setTimeout
setTimeout promise
async1 setTimeout
复制代码
步骤解析:
async函数是promise的一个语法糖,简单理解为:await中的语句相当于在promise.resolve()中;await后面的语句相当于.then中的语句
-
初始状态的队列信息
- task queue:run script;
- microtask queue:【空】;
-
【task 阶段】执行run script;根据上述对await的解释,async1中的await async2()直接执行,async1中的await 后面的语句相当于promise then去处理
- 当前事件循环执行栈:run script;
输出:
script start
复制代码
-- 2.1 执行到调用async1()语句,在async1中执行await async2,async2中的语句直接执行,async2 setTimeout callback被推入task queue中;async1中的await async2后面的语句相当于promise then被推入microtask queue中; - task queue:async2 setTimeout callback; - microtask queue:async1中的await async2后面的语句;