blog's Introduction
blog's People
blog's Issues
如果你问我什么是 Event Loop
Event Loop
Event Loop 是主线程之外的异步任务调度模型。
js 用 Event Loop 解决单线程带来的一些问题。
浏览器
浏览器的 Event Loop 模型在HTML5标准中有明确定义,但每个浏览器有稍微不同的具体实现。
根据任务的差异,将异步任务分为两类,并放入不同的队列:
- Task(macroTask):setTimeout, setInterval, setImmediate, I/O, UI rendering,dispatch
- microtask:Promise, process.nextTick, Object.observe, MutationObserver, MutaionObserver
两个队列有不同的调度方式。Event Loop 具体调度规则如下:
- 清空 microtask 队列,直到没有新的microtask加入。
- 取出最早的一个task,执行。
- 在执行 task 过程中,如果 call stack 为空,也立即清空 microtask。
EV的触发时机是主线程 call stack 为空。
在浏览器的 EV 中,最难以把握的行为其实是 Timer API。
对于 Timer API,有两个最基本的注意点:
- HTML5标准规定的 setTimeout 最短间隔不得低于4毫秒。但浏览器有可能有不同实现。
- setTimeout 第二个参数传入的时间,只是将回调放入 microtask 队列的时间,不是执行的时间。如果 call stack 一直不为空,那就一直没有机会执行。
这是一个经典案例(tasks-microtasks-queues-and-schedules)
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');
// Let's listen for attribute changes on the
// outer element
new MutationObserver(function() {
console.log('mutate');
}).observe(outer, {
attributes: true
});
// Here's a click listener…
function onClick() {
console.log('click');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
outer.setAttribute('data-random', Math.random());
}
// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
如果我们点击 inner div,以下事情将会发生:
- main script 执行完毕,call stack 为空
- 点击 inner 后,onclick dispatch 事件进入task。
- 【call stack 为空】第一轮 EV:
- microtask 为空,执行第一个 task。
- 执行 dispatch,将 onclick1 函数加入 call stack。
- 输出click。
- 将 setTimeout cb 放入 task。
- 将 promise cb 放入 microtask。
- 将 mutation cb 放入 microtask。
- 尽管我们还在 某个task中,由于此时 onclick1 执行完毕,call stack 为空,立即清空 microtask。
- 输出 promise。
- 输出 mutation。
- microtask 为空。
- 将 onclick2 函数加入 call stack。
- 重复 b ~ f。
- 执行完一个 task,且 microtask 为空,则退出本次 EV。
- 【call stack 为空】第二轮 EV:
- microtask为空。
- 执行第一个 task。输出 timeout。
- 执行第二个 task。输出 timeout。
如果我们不是点击,而是执行 inner.click() 呢?事情和上面有什么不一样吗?
最重要的区别在于,在冒泡结束完之前,call stack 不会为空!也就是意味着上面的 3.f. 步骤不会发生。
我们完整地重新来一遍,如果执行 inner.click(),以下事情将会发生:
- inner.click 入栈并执行。
- onclick1 入栈并执行。
- 输出click。
- 将 setTimeout cb 放入 task。
- 将 promise cb 放入 microtask。
- 将 mutation cb 放入 microtask。
- onclick1 出栈,onclick2 入栈并执行。
- 输出click。
- 将 setTimeout cb 放入 task。
- 将 promise cb 放入 microtask。
- 因为有一个还没执行的 mutation,因此不加入新的。
- onclick2 出栈,inner.click 出栈。
- 【call stack 为空】第一轮 EV:
- 输出 promise。
- 输出 mutation。
- 输出 promise。
- 【call stack 为空】第二轮 EV:
- microtask为空。
- 执行第一个 task。输出 timeout。
- 执行第二个 task。输出 timeout。
第二个例子,来自(https://zhuanlan.zhihu.com/p/33087629)
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
//process.nextTick(() => {
// console.log(3)
//})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
//process.nextTick(() => {
// console.log(6)
//})
setTimeout(() => {
console.log(9)
//process.nextTick(() => {
// console.log(10)
//})
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
有了上面的铺垫,这一题答案应该比较明显了,输出为:1 7 8 2 4 5 9 11 12。
Libuv
这里其实应该把 node 基于 libuv 的实现和 libuv 的实现 分开讨论的。这个之后再梳理。
Node 中的 Event Loop 是基于 libuv 的,因此它的具体实现是确定的。本节介绍 libuv。
libuv is a multi-platform support library with a focus on asynchronous I/O. It was primarily developed for use by Node.js, but it's also used by Luvit, Julia, pyuv, and others.
核心代码在deps/uv/src/unix/core.c (代码解析文章)
libuv 的实现都是建立在 handle 和 request 的基础上。(uvbook 中文版)
handle代表了持久性对象。在异步的操作中,相应的handle上有许多与之关联的request。request是短暂性对象(通常只维持在一个回调函数的时间),通常对映着handle上的一个I/O操作。request用来在初始函数和回调函数之间,传递上下文。
简单理解,handle (句柄)就是一个对象,用来保存持久性对象的状态。常见的句柄,例如 tcp 句柄、udp 句柄、进程句柄、文件流句柄。
回到 libuv 事件循环上,其分为六个阶段,如图所示。
timers阶段相当于libuv自己提供的api,在不断的loop中可以注册触发回调。
IO callbacks相当于运行io的callback,也就是stream流式传输的回调,比如我用libuv中提供的流读文件,或者IPC之类的。
而idle是为了越过poll直接执行check的,所以idle必须在前面。
而prepare则类似于poll之前的缓冲,因为poll会阻塞,idle又可以越过poll,所以可以单独在prepare阶段特殊处理,嵌入特定idle。
poll阶段为什么放到后边,因为有uv_stop的存在,你可以用uv_stop来关闭事件循环,而这个开关是放在prepare和poll之间的,也就是如果我在poll之后stop了,libuv还会跑一次循环直到跑完prepare,这样正好保证了IO callbacks的完整退出。
check和close感觉就没必要介绍了。
reference
timers
timers 阶段执行所有超时的计时器回调。
值得注意的是,并不是在任务到期之后,把回调放入所谓的「timer queue」,而是用最小堆管理所有的计时器任务。根据最小堆的性质,每次判断头部节点有没有超时,没有的话说明后面的节点也不会超时,有的话继续往下判断即可。
idle prepare check
代码:https://github.com/libuv/libuv/blob/v1.x/src/unix/loop-watcher.c
代码解析
这三类 handle 的功能非常类似,只是优先级不同。都是存放用户自定义的回调,并在每次事件循环时触发一次。
idle 和 prepare 最大的区别在于,当存在活跃的 idle 时,poll 的超时时间会被设置为 0。
由于 idle 每次事件循环都会执行一次回调,因此在实现上,回调出队执行完成后,会重新回到对尾。其他阶段的回调都是用完即丢。
setImmediate是利用 idle 这个阶段实现的。
poll
poll 阶段的任务是阻塞等待执行回调。但是这个阻塞不是无止境,而是带有超时时间的。
在 poll 开始之前,会计算超时时间。
- 超时时间为距离现在最近的 timer。
- 在满足以下条件时,超时时间会被设置为0:
- 显式设置为非阻塞模式:uv_run处于UV_RUN_NOWAIT模式下
- 显式调用 stop:uv_stop()被调用
- 没有活跃的handles和request
- 有活跃的idle handles
- 有等待关闭的handles
- 如果上述都不符合,则会一直阻塞下去。
poll 开始后,会发生以下几种常见:
- callback queue 中的回调依次被执行,直到超时时间到,poll 结束。
- 如果没有超时,且queue 队列为空,则阻塞,直到下一个 fd 返回。
- 如果阻塞过程中,有新的 timer 到期,则跳回 timers 阶段。
close
循环关闭所有的closing handles。
Nodejs Event Loop
简单过了一下 libuv,接下来就可以讲一讲 nodejs 是如何基于 libuv 来做事件循环的。
nodejs 在 libuv 的六阶段里,分别作了这些事情:
- timers: 执行 setTimeout 和 setInterval 中到期的回调。
- I/O callbacks: 执行上一轮残留的一些回调。
- idle:仅内部使用,用来控制 setImmediate 的执行。
- prepare:仅内部使用。
- poll:除了其他环节的少量回调,其他所有任务都在这里完成。
- check:执行 setImmediate 的回调。
- close callbacks:循环关闭所有的closing handles,如 socket.on("close", cb)。
node.js v11 之后的每个阶段的一个 callback 执行完成后,都会检查并执行 process.nextTick。
setImmediate 的实现
https://cnodejs.org/topic/5a9e3e1819b2e3db18959ad4
setImmediate 源码解析另外提一下的就是setImmediate和setTimeout谁先谁后的问题。这个其实是不一定的。从uv_run中我们看到执行定时器的代码比是比uv__run_check先的,但是如果我们在执行完定时器之后,uv__run_check之前,又新增了一个定时器和执行了setImmediate,那么setImmediate的回调就会先执行。
process.nextTick()是node早期版本无setImmediate时的产物,node作者推荐我们尽量使用setImmediate。
待补充,只需要知道 nodejs 是通过 idle 阶段设置一个活跃的 idle handle 开关,使得 poll 超时时间为 0,从而跳过 poll 阻塞,进入 check 阶段,并在 check 阶段执行 setImmedate 回调。
实战练习
console.log(1)
setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
process.nextTick(() => {
console.log(3)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
process.nextTick(() => {
console.log(6)
})
分析以下流程:
- 输出 1
- 注册 setTimeout 的回调,超时时间是1ms。
- 执行 promise1,输出 7。
- 注册 promise1.then。
- 注册 nextTick6
- 此时 EV 应处于 poll 阻塞阶段。拿到 promise1.then 后马上执行。
- 注册 promise2
- 检查 nextTick,输出 6
- 此时 EV 应处于 poll 阻塞阶段。执行 promise 2。
- 输出 8
- 此时 EV 应处于 poll 阻塞阶段。直到 setTimeout 到期。
- 执行 setTimeout 回调。
- 输出 2
- 执行promise4,输出 4。
- 注册 promise4.then,nextTick3
- 又到了 poll 阶段。执行 promise4.then。
- 输出 5
- 检查 nextTick,输出 3
setImmediate 和 setTimeout 的顺序
没有所谓固定的优先级,其实执行顺序的本质在于,此时 EV 是否已经阻塞在 poll 阶段了。
case1:这种情况下,输出是不固定的,取决于当次的运行状态。
setImmediate(function () {
console.log('1');
});
setTimeout(function () {
console.log('2');
}, 0);
case 2:有其他代码执行的情况,EV 在晚于 4ms 之后才到达 timers 阶段,因此先输出2,再输出1。
setImmediate(function () {
console.log('1');
});
setTimeout(function () {
console.log('2');
}, 0);
console.log(3)
因此在实际的大多数情况下,setImmediate 都能在 setTimeout 之前运行。
如果要保证 setTimeout 先运行,只需在 setImmedate 前执行同步代码,保证在 timers 前定时器到期。
setTimeout(function () {
console.log('2');
}, 0);
let now = Date.now();
while (Date.now() - now < 10) {}
setImmediate(function () {
console.log('1');
});
console.log(3)
感谢:
- https://juejin.im/post/5aa747b6f265da2377191272
- node event loop 讨论:https://cnodejs.org/topic/5ab1c30719b2e3db18959fca
- libuv:https://juejin.im/post/5d91a500f265da5b981a7074
- libuv 实战:https://www.jianshu.com/p/8e0ad01c41dc
- https://cnodejs.org/topic/5a9108d78d6e16e56bb80882
- uv__io_poll 代码解析:https://www.jianshu.com/p/cd381ee1600c
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.