原文在个人博客, 未经容许请不要转载javascript
网上一搜事件循环, 不少文章标题的前面会加上 JavaScript, 可是我以为事件循环机制跟 JavaScript 没什么关系, JavaScript 只是一门解释型语言, 方便开发和理解的, 由V8 JIT将 JavaScript 编译成机器语言来调用底层, 至于浏览器怎么执行 JavaScript 代码, JavaScript 管不着也不关心. 所以, “JavaScript事件循环机制”这种说法是不合理的. 事件循环机制是由运行时环境实现的, 具体来讲有浏览器、Node等. 这篇文章就先来讲说浏览器中实现的事件循环机制.html
首先,javascript 在浏览器端运行是单线程的,这是由浏览器决定的,这是为了不多线程执行不一样任务会发生冲突的状况。也就是说咱们写的javascript 代码只在一个线程上运行,称之为主线程(HTML5提供了web worker API可让浏览器开一个线程运行比较复杂耗时的 javascript任务,可是这个线程仍受主线程的控制)。单线程的话,若是咱们作一些“sleep”的操做好比说:java
var now = + new Date()
while (+new Date() <= now + 1000){
//这是一个耗时的操所
}
复制代码
那么在这将近一秒内,线程就会被阻塞,没法继续执行下面的任务。node
还有些操做好比说获取远程数据、I/O操做等,他们都很耗时,若是采用同步的方式,那么进程在执行这些操做时就会由于耗时而等待,就像上面那样,下面的任务也只能等待,这样效率并不高。web
那浏览器是怎么作的呢?算法
咱们找到WHATWG规范对Event loop的介绍:chrome
为了协调事件,用户交互,脚本,渲染,网络等,用户代理必须使用事件循环。api
任务队列
(task queues)。任务队列是task的有序列表,task是调度Events,Parsing,Callbacks,Using a resource,Reacting to DOM manipulation这些任务的算法;任务源
(task source)(好比鼠标键盘事件)。来自同一个特定任务源且属于特定事件循环的任务必须被加入到同一个任务队列中,来自不一样任务源的任务能够放在不一样的任务队列中;在调用任务的过程当中, 会产生新的任务, 浏览器就会不断执行任务, 所以称为事件循环.promise
还有一些特殊任务, 它们不会被放在task queues中, 会放在一个叫作microtask(微任务) queue中, 继续看标准:浏览器
Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue.
任务队列能够有多个, 可是微任务队列只有一个.
那么哪些任务是放在task queue, 哪些放在microtask queue呢? 一般对浏览器和Node.js来讲:
setTimeout
, setInterval
, setImmediate
, I/O, UI rendering等process.nextTick
, Promises
(这里指浏览器实现的原生 Promise), Object.observe
, MutationObserver
等请尤为注意macrotask中执行总体代码也是一个宏任务
整体来讲, 浏览器端事件循环的一个回合(go-around或者叫cycle)就是:
不管在执行macrotask仍是microtask, 都有可能产生新的macrotask或者microtask, 就这样继续执行.
这里有一些常见异步操做:
const interval = setInterval(() => {
console.log('setInterval')
}, 0)
setTimeout(() => {
console.log('setTimeout 1')
Promise.resolve().then(() => {
console.log('promise 3')
}).then(() => {
console.log('promise 4')
}).then(() => {
setTimeout(() => {
console.log('setTimeout 2')
Promise.resolve().then(() => {
console.log('promise 5')
}).then(() => {
console.log('promise 6')
}).then(() => {
clearInterval(interval)
})
}, 0)
})
}, 0)
Promise.resolve().then(() => {
console.log('promise 1')
}).then(() => {
console.log('promise 2')
})
复制代码
结果(Chrome 63.0.3239.84; Mac OS):
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval // 大部分状况下2次, 少数状况下一次
setTimeout 2
promise 5
promise 6
复制代码
这个顺序是如何得来的?
咱们先讲promise 4后面只出现一次setInterval的状况, 画个图简单表示一下这个过程:
注意
本图为了方便把各时间段(Cycle)队列的任务都画在队列中去了, 实际上执行一个task 和 microtask 后就会把这个任务从相应队列中删除
首先, 主任务就是执行脚本, 也就是执行上述代码, 这也是一个task. 在执行代码过程当中, 遇到setTimeout、setInterval 就会将回调函数添加到task queue中, 遇到 promise 就会将then回调添加到 microtask 中去.
Task执行完, 接着取全部 microtask 执行, 全部microtask 执行完了, microtask queue也就空了, 接着再取task执行, 若是microtask queue为空, 没有任务, 则继续取下一个task执行, 就这样循环执行. 图中箭头就表示执行的顺序.
那么为何promise 4后面大部分状况下出现2次setInterval, 少数状况出现1次呢?
我猜想这是由于setInterval是有最短间隔时间的(chrome下4ms左右), 这个时间不一样机子、不一样浏览器都有可能不同. 代码中的参数是0, 意味着尽量短的时间内就会产生一个task加入到 task queue中. 浏览器在执行setInterval后到执行下一个task前, 时间间隔就可能超过这个最短期, 所以会产生一个setInterval task.
我是这样论证的:
我把含有promise五、promise6回调函数的setTimeout的时间设置大一点, 让它推迟插入task queue中:
...
setTimeout(() => {
console.log('setTimeout 2')
Promise.resolve().then(() => {
console.log('promise 5')
}).then(() => {
console.log('promise 6')
}).then(() => {
clearInterval(interval)
})
}, 10) //这里加上10ms
...
复制代码
结果是promise 4后面的setInterval出现了5次, 所以我以为promise 4后面大部分状况下出现2次setInterval、少数状况出现一次的缘由就是浏览器在执行setInterval回调函数后、执行setTimeout回调函数前, 时间间隔大部分状况超过了这个最短期.
另外, 我试着再依次加上1ms, 直到14ms——也就是加上4ms时, promise 4后面的setInterval变成了6次, 能够认为setInterval最短间隔时间在Chrome下约为4ms(不考虑机子性能、设置).
首先说明一下, 在Node中也体现了任务队列的机制, 可是这不是Node实现的, 这是V8实现的, 由Node调用了V8任务队列机制的API. 至于为何是V8实现的, 咱们翻翻ECMA 262 标准对 Job 和 Job queue 的介绍就能够得知
可是让人摸不着头脑的是, 这段代码在node v8.5.0下有时会出现这样的结果:
promise 1
promise 2
setInterval
setTimeout 1
promise 3
promise 4
setInterval
setTimeout 2
setInterval // 为何会出现setInterval???
promise 5
promise 6
复制代码
按理说应该是setTimeout 2 => promise 5 => promise 6, 由于输出setTimeout 2的回调函数是task, 执行完这个task后应该调用microtask 输出promise 5 => promise 6啊? 很奇怪! Node对V8确实有些改动, 不知道是否是这方面缘由...
还请大神解惑!
总结一下:
学习技术仍是有捷径的, 那就是读标准 ;)