🔄记住这张图,脑子跟着浏览器的事件循环(Event Loop)转起来了
一、前言
下面按照我的理解,纯手工画了一张在浏览器执行JavaScript代码的Event Loop(事件循环) 流程图。
后文会演示几个例子,把示例代码放到这个流程图演示其执行流程。
当然,这只是简单的事件循环流程,不过,却能让我们快速掌握其原理。
二、概念
事件循环是JavaScript为了处理单线程执行代码时,能异步地处理用户交互、网络请求等任务 (异步Web API),而设计的一套任务调度机制。它就像一个永不停止的循环,不断地检查(结合上图就是不断检查Task Queue和Microtask Queue这两个队列)并需要运行的代码。
三、为什么需要事件循环
JavaScript是单线程的,这意味着它只有一个主线程来执行代码。如果所有任务(比如一个耗时的计算、一个网络请求)都同步执行,那么浏览器就会被卡住,无法响应用户的点击、输入,直到这个任务完成。这会造成极差的用户体验。
事件循环就是为了解决这个问题而生的:它让耗时的操作(如网络请求、文件读取)在后台异步执行,等这些操作完成后,再通过回调的方式来执行相应的代码,从而不阻塞主线程。
四、事件循环流程图用法演示
演示一:小菜一碟
先来一个都是同步代码的小菜,先了解一下前面画的流程图是怎样在调用栈当中执行JavaScript代码的。
console.log(1)
function funcOne() {
console.log(2)
}
function funcTwo() {
funcOne()
console.log(3)
}
funcTwo()
console.log(4)
控制台输出:
1 2 3 4
下图为调用栈执行流程
每执行完一个同步任务会把该任务进行出栈。在这个例子当中每次在控制台输出一次,则进行一次出栈处理,直至全部代码执行完成。
演示二:小试牛刀
setTimeout+Promise组合拳,了解异步代码是如何进入任务队列等待执行的。
console.log(1)
setTimeout(() => {
console.log('setTimeout', 2)
}, 0)
const promise = new Promise((resolve, reject) => {
console.log('promise', 3)
resolve(4)
})
setTimeout(() => {
console.log('setTimeout', 5)
}, 10)
promise.then(res => {
console.log('then', res)
})
console.log(6)
控制台输出:
1 promise 3 6 then 4 setTimeout 2 setTimeout 5
流程图执行-步骤一:
先执行同步代码,如遇到异步代码,则把异步回调事件放到后台监听或对应的任务队列。
-
执行
console.log(1),控制台输出1。 -
执行定时器,遇到异步代码,后台注册定时器回调事件,时间到了,把回调函数
() => {console.log('setTimeout', 2)},放到宏任务队列等待。 -
执行创建
Promise实例,并执行其中同步代码:执行console.log('promise', 3),控制台输出promise 3;执行resolve(4),此时Promise已经确定为完成fulfilled状态,把promise.then()的回调函数响应值设为4。 -
执行定时器,遇到异步代码,后台注册定时器回调事件,时间未到,把回调函数
() => { console.log('setTimeout', 5) }放到后台监听。 -
执行
promise.then(res => { console.log('then', res) }),出栈走异步代码,把回调函数4 => { console.log('then', 4) }放入微任务队列等待。
流程图执行-步骤二:
上面已经把同步代码执行完成,并且把对应异步回调事件放到了指定任务队列,接下来开始事件循环。
-
扫描微任务队列,执行
4 => { console.log('then', 4) }回调函数,控制台输出then 4。 -
微任务队列为空,扫描宏任务队列,执行
() => {console.log('setTimeout', 2)}回调函数,控制台输出setTimeout 2。 -
每执行完一个宏任务,需要再次扫描微任务队列是否存在可执行任务(假设此时后台定时到了,则会把
() => { console.log('setTimeout', 5) }加入到了宏任务队列末尾)。 -
微任务队列为空,扫描宏任务队列,执行
() => { console.log('setTimeout', 5) },控制台输出setTimeout 5。
演示三:稍有难度
setTimeout+Promise组合拳+多层嵌套Promise
console.log(1)
setTimeout(() => {
console.log('setTimeout', 10)
}, 0)
new Promise((resolve, reject) => {
console.log(2)
resolve(7)
new Promise((resolve, reject) => {
resolve(5)
}).then(res => {
console.log(res)
new Promise((resolve, reject) => {
resolve('嵌套第三层 Promise')
}).then(res => {
console.log(res)
})
})
Promise.resolve(6).then(res => {
console.log(res)
})
}).then(res => {
console.log(res)
})
new Promise((resolve, reject) => {
console.log(3)
Promise.resolve(8).then(res => {
console.log(res)
})
resolve(9)
}).then(res => {
console.log(res)
})
console.log(4)
上一个演示说明了流程图执行的详细步骤,下面就不多加赘叙了,直接看图!
talk is cheap, show me the chart
上图,调用栈同步代码执行完成,开始事件循环,先看微任务队列,发现不为空,按顺序执行微任务事件:
上图,已经把刚才排队的微任务队列全部清空了。但是在执行第一个微任务时,发现还有嵌套微任务,则把该任务放到微任务队列末尾,然后接着一起执行完所有新增任务。
最后微任务清空后,接着执行宏任务。到此全部事件已执行完毕!
控制台完整输出顺序:
1 2 3 4 5 6 7 8 9 10
演示四:setTimeout伪定时
setTimeout并不是设置的定时到了就马上执行,而是把定时回调放在task queue任务队列当中进行等待,待主线程调用栈中的同步任务执行完成后空闲时才会执行。
const startTime = Date.now()
setTimeout(() => {
const endTime = Date.now()
console.log('setTimeout cost time', endTime - startTime)
// setTimeout cost time 2314
}, 100)
for (let i = 0; i < 300000; i++) {
// 模拟执行耗时同步任务
console.log(i)
}
控制台输出:
1 2 3 ··· 300000 setTimeout cost time 2314
下图演示了其执行流程:
演示五:fetch网络请求和setTimeout
获取网络数据,fetch回调函数属于微任务,优于setTimeout先执行。
setTimeout(() => {
console.log('setTimeout', 2)
}, 510)
const startTime = Date.now()
fetch('http://localhost:3000/test').then(res => {
const endTime = Date.now()
console.log('fetch cost time', endTime - startTime)
return res.json()
}).then(data => {
console.log('data', data)
})
下图当前Call Stack执行栈执行完同步代码后,由于fetch和setTimeout都是宏任务,所以走宏任务Web API流程后注册这两个事件回调,等待定时到后了,由于定时回调是个普通的同步函数,所以放到宏任务队列;等待fetch拿到服务器响应数据后,由于fetch回调为一个Promise对象,所以放到微任务队列。
经过多番刷新网页测试,下图控制台打印展示了setTimeout延时为510ms,fetch请求响应同样是510ms的情况下,.then(data => { console.log('data', data) })先执行了,也是由于fetch基于Promise实现,所以其回调为微任务。
五、结语
这可能只是简单的JavaScript代码执行事件循环流程,目的也是让大家更直观理解其中原理。实际执行过程可能还会读取堆内存获取引用类型数据、操作dom的方法,可能还会触发页面的重排、重绘等过程、异步文件读取和写入操作、fetch发起网络请求,与服务器建立连接获取网络数据等情况。
但是,它们异步执行的回调函数都会经过图中的这个事件循环过程,从而构成完整的浏览器事件循环。