前言
众所周知, javascript 是一门单线程语言, 而当我们使用 ajax 和服务端进行通信的时候是需要一定时间的, 这样当前线程就会被阻塞, 使浏览器失去相应. 因此, 当 js 执行执行一些长时间的任务时, 我们希望有一种异步的方式处理这种任务. 事件循环 (event loop) 就是如何处理异步执行顺序的一种机制.
- $.get(url, function (data) {
- //do something
- });
浏览器中的事件循环
接下来会一一介绍, 事件循环中的执行栈, 事件队列, 宏任务, 微任务等概念
什么是执行栈
执行栈就是 js 代码运行的地方, 上图 call stack 所示. 当下面程序运行时, 会推送的调用栈中被执行.
- console.log('Hi');
- setTimeout(function cb1() {
- console.log('cb1');
- }, 500);
- console.log('Bye');
什么是事件队列
当浏览器中的事件监听函数被触发 (DOM), 网络请求的相应(ajax), 定时器被触发(setTimeout) 相对应的回调函数就会被推送到事件队列中, 等待执行; 如上图中的 Callback Queue.
什么是事件循环
事件循环是一个这样的过程: 当执行栈中的任务结束之后, 会将事件队列中的第一个任务推入到执行栈中执行, 当任务处理完毕, 又会取事件队列中的第一个任务, 如此往复, 便构成了事件循环.
对应到下面代码中.
- console.log('Hi');
- setTimeout(function cb1() {
- console.log('cb1');
- }, 500);
- console.log('Bye');
程序推送到执行栈中被执行
执行 console 语句, 输出 Hi
执行 setTimeou 语句
执行 console 语句, 输出 Bye
500ms 的时候, setTimeout 的回调函数被推送到事件队列中
此时事件队列中只有 setTimeout 的回调函数这一个任务, 会被推到执行栈中执行
console 语句执行, 输出 cb1
通过上面的例子会对执行栈和事件队列有个基本的认识. 由于 JS 是单线程的, 同步任务会造成浏览器阻塞, 我们把任务分成一个一个的异步任务, 通过事件循环来执行事件队列中的任务. 这就使得当我们挂起某一个任务的时候可以去做一些其他的事情, 而不需要等待这个任务执行完毕. 所以事件循环的运行机制大致分为以下步骤:
1, 检查事件队列是否为空, 如果为空, 则继续检查; 如不为空, 则执行 2;
2, 取出事件队列的首部, 压入执行栈;
3, 执行任务;
4, 检查执行栈, 如果执行栈为空, 则跳回第 1 步; 如不为空, 则继续检查;
浏览器渲染时机
我们知道 DOM 操作会触发浏览器渲染, 如增, 删节点, 改变背景颜色. 那么这类操作是如何在浏览器当中奏效的?
至此我们已经知道了事件循环是如何执行的, 事件循环器会不停的检查事件队列, 如果不为空, 则取出队首压入执行栈执行. 当一个任务执行完毕之后, 事件循环器又会继续不停的检查事件队列, 不过在这间, 浏览器会对页面进行渲染. 这就保证了用户在浏览页面的时候不会出现页面阻塞的情况, 这也使 JS 动画成为可能.
- function move() {
- setTimeout(() => {
- dom.style.left = dom.offsetLeft + 10 + 'px'
- move()
- }, 15);
- }
- move()
现在用事件循环的机制说明 js 动画的过程. 上面代码会在执行栈中执行, move 函数被调用, setTimeout 的回调函数 15ms 之后会被推送到事件队列中. 此时执行栈中的任务结束, 浏览器渲染, 检查事件队列不断循环. 当 15ms 之后事件队列中有任务时, 会被推送到执行栈中执行, 这时 dom 节点向右偏移 10px,move 函数执行, 执行栈结束, 浏览渲染, 检查事件队列. 如此往复就形成了动画.
宏任务和微任务(microtask)
先看一段代码, 是如何输出的;
- console.log('script start');
- setTimeout(function () {
- console.log('setTimeout');
- }, 0);
- Promise.resolve().then(function () {
- console.log('promise1');
- }).then(function () {
- console.log('promise2');
- });
- console.log('script end');
答案是:'script start','script end','promise1','promise2','setTimeout'.
setTimeout 的回调函数是宏任务, Promise 的回调函数是微任务. 微任务和宏任务一样遵循事件循环机制, 但是他们还是有些差别.
1, 宏任务和微任务的事件队列是相互独立的;
2, 微任务队列的检查时机早于宏任务.(执行栈中任务结束就会马上清空微任务事件队列)
根据上面的规则, 解释代码的输出.
执行栈中的代码执行, 宏任务推入宏任务事件队列, 微任务推入微任务事件队列, 执行栈任务结束
检查微任务事件队列, 此时已经有 Promise 的回调函数, 推入执行栈, 输出 promise1.Promise 还有回调函数, 推入微任务事件队列, 执行栈结束.
检查微任务事件队列, 推入执行栈, 输出 promise2, 执行栈结束.
检查微任务事件队列, 此时被清空
检查宏任务事件队列, 推入执行栈, 输出 setTimeout, 执行栈结束.
宏任务有: **setTimeout** ,**setImmediate** , **MessageChannel**
微任务有: **setTimeout** ,**setImmediate** , **MessageChannel**
Node.js 中的事件循环
Node 中的事件循环是和浏览器有很大区别的
当 Node.js 启动时, 会初始化 event loop; 每个 event loop 都会包含按如下顺序六个循环阶段
- > timers
- I/O callbacks
- idle, prepare
- incoming:
poll < connections,
data, etc.
- check
- close callbacks
timers 阶段: 这个阶段执行 setTimeout(callback) and setInterval(callback)预定的 callback;
I/O callbacks 阶段: 执行除了 close 事件的 callbacks, 被 timers(定时器, setTimeout,setInterval 等)设定的 callbacks,setImmediate()设定的 callbacks 之外的 callbacks;
idle, prepare 阶段: 仅 node 内部使用;
poll 阶段: 获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里;
check 阶段: 执行 setImmediate() 设定的 callbacks;
close callbacks 阶段: 比如 socket.on('close', callback)的 callback 会在这个阶段执行.
每一个阶段都有一个装有 callbacks 的 fifo queue(队列), 当 event loop 运行到一个指定阶段时, node 将执行该阶段的 fifo queue(队列), 当队列 callback 执行完或者执行 callbacks 数量超过该阶段的上限时, event loop 会转入下一下阶段.
Node.js 中的宏任务和微任务
宏任务: setTimeout 和 setImmediate
setTimeout 设计在 poll 阶段为空闲时, 且设定时间到达后执行; 但其在 timer 阶段执行
setImmediate 设计在 check 阶段执行;
谁先输出, 谁后输出?
- setTimeout(function timeout () {
- console.log('timeout');
- },0);
- setImmediate(function immediate () {
- console.log('immediate');
- });
答案是不确定的. 有两个前提我们是需要清楚的;
event loop 初始化是需要一定时间的
setTimeout 有最小毫秒数的, 通常是 4ms.
当: event loop 准备时间> setTimeout 最小毫秒数. 从 timers 阶段检查, 此时队列中已经有 setTimeout 的任务, 所以 timeout 先输出;
当: event loop 准备时间 <setTimeout 最小毫秒数. 从 timers 阶段检查, 此时队列是空的就下检查接下来的阶段, 到 check 阶段, 已经有 setImmediate 的任务, 所以 immediate 先输出;
微任务: process.nextTick()和 Promise.then()
微任务不在 event loop 的任何阶段执行, 而是在各个阶段切换的中间执行, 即从一个阶段切换到下个阶段前执行; nextTick 比 Promise.then()先执行
下面代码是如何执行的.
- setImmediate(() => {
- console.log('setImmediate1')
- setTimeout(() => {
- console.log('setTimeout1')
- }, 0);
- })
- setTimeout(()=>{
- process.nextTick(()=>console.log('nextTick'))
- console.log('setTimeout2')
- setImmediate(()=>{
- console.log('setImmediate2')
- })
- },0);
从前面的知识知道, 此时 setTimeout 和 setImmediate 执行顺序是不确定的.
假设 setImmediate 先执行, 输出 setImmediate1,setTimeout 的任务添加到 timer 阶段
检查 timer 阶段, 这时已经有两个任务. 先执行之前的第一个任务, nextTick 添加到微任务队列, 输出 setTimeout2,setImmediate 的任务添加到 check 阶段.
timer 中还有一个任务, 执行输出 setTimeout1
切换阶段, 微任务执行, 输出 nextTick
检查 check 阶段, 输出 setImmediate2
思考题
- let fs = require('fs')
- fs.readFile('./1.txt', 'utf8', function (err, data) {
- setTimeout(() => {
- console.log('setTimeout')
- }, 0);
- setImmediate(() => {
- console.log('setImmediate')
- })
- })
这种情况下的 setTimeout 和 setImmediate 执行的顺序确定吗? readFile 的回调函数是在 poll 阶段执行 答案是 setImmediate 比 setTimeout 先执行
结语
浏览器中和 Node.js 中的事件循环可以说是两套不同的机制, 做个总结, 希望有所帮助.
来源: https://juejin.im/post/5b097df46fb9a07aa213cf88