先了解下任务队列
此时区分为浏览器的事件循环和Node端的事件循环。下面将一一详解。
主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)
上图过程是一个宏观的表述,实际上callback queue
任务队列是分为macro task
宏任务和micro task
微任务两种。
在一个事件循环当中,执行的步骤如下:
从以上我们得知重点是执行栈为空后优先处理微任务再处理宏任务。
写过vue的同学应该熟悉一个方法this.$nextTick(() => { ... })
,此时对于宏任务和微任务的分类我们不知所措,那就看看vue源码中是如何处理的。
其中定义的很清楚
setImmediate/setTimeout
setImmediate为IE特有的,我们可以在IE浏览器环境下做测试
setImmediate(() => {
console.log('setImmediate')
})
setTimeout(() => {
console.log('setTimeout')
}, 0) // 延迟设置为0,实际上是会有4ms的延迟。
// => setImmediate
// setTimeout
MessageChannel
H5的API,兼容性不怎么好,前往mdn查看使用
再做如下测试
let channel = new MessageChannel()
let port1 = channel.port1
let port2 = channel.port2
port1.postMessage('hello careteen ~')
port2.onmessage = function (e) {
console.log(e.data)
}
// => hello careteen ~
promise的then方法
Promise.resolve(100).then(data => {
console.log(data)
})
// => 100
MutationObserver 这也是一个属于微任务的异步方法,前往mdn查看使用
此方法在vue1.0中有使用到,但是再2.0以后则废弃了。
下面简单介绍下使用,场景为为页面创建多个节点,当节点创建完成后告诉我们。
html
<div id="box"></div>
js
let observer = new MutationObserver(function() {
console.log('dom 更新完了')
console.log(box.children.length)
})
// 监听后代的变化
observer.observe(box, {
childList: true
})
for (let i = 0; i < 5; i++) {
box.appendChild(document.createElement('sapn'))
}
for (let i = 0; i < 10; i++) {
box.appendChild(document.createElement('p'))
}
// => 当节点创建完成后
// dom 更新完了
// 15
case1:
Promise.resolve().then(() => {
console.log('then1')
setTimeout(() => {
console.log('timer1')
}, 0)
})
console.log('start')
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(() => {
console.log('then2')
})
}, 0)
// => start -> then1 -> timer2 -> then2 -> timer1
根据上面的执行流程可得知结果。
start
,then1
timer2
then2
以上也只是宏观上的一个描述,和浏览器一样,异步事件方法的队列也细分了几个。前往Node官网可查看详细说明
下面摘自Node官网
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
从上面的模型中我们可以得知机制的执行顺序:
外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段...
细分的几个异步队列:
setTimeout
方法setInterval
方法fs.readFile()
方法setImmediate()
的回调会在这个阶段执行socket.on('close', ...)
这种close事件的回调和浏览器的事件循环机制有所不同:
poll
阶段
poll queue
中是否有事件setImmediate
的回调check
阶段去执行这些回调timer
timer
的回调按照调用顺序放到timer queue
,然后循环进入timer
阶段执行队列中的回调setImmediate
和timer
的判断顺序不是固定的,受代码运行环境的影响setImmediate
和timer
的队列都是空的,那么循环会在poll
阶段停留,直到有一个I/O事件返回,循环会进入I/O callback
阶段并立即执行这个事件的回调check
阶段
setImmediate
的回调,当poll
阶段进入空闲状态,并且setImmediate
队列中有callback时,事件循环进入这个阶段close
阶段
socket.destroy()
方法),close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()
方法发送出去timer
阶段
I/O callback
阶段
process.nextTick()
这个方法虽然没有在上面列入,但是却在每个阶段执行完毕准备进入下一个阶段时优先调用
poll queue
的任务不同的是,这个操作在队列清空前是不会停止的。也就是说错误使用会导致node进入一个死循环,直到内存泄露上面说了一大推,看起来很枯燥,那么下面来几个case深入理解
case1:
Promise.resolve().then(() => {
console.log('then1')
setTimeout(() => {
console.log('timer1')
}, 0)
})
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(() => {
console.log('then2')
})
}, 0)
// => then1 -> timer2 -> then2 -> timer1
// or then1 -> timer2 -> timer1 -> then2
上面代码的结果有两种可能,then2
执行后,timer1
可能还没到时间也可能到时间了,因为setTimeout
方法第二个参数如果设置为0
,实际执行时会有4ms
的延迟。
case2:
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
// => setImmediate -> timeout
// or timeout -> setImmediate
多运行几次,运行结果是不一定的,这取决于运行代码的环境。运行和环境中的各种复杂情况会导致在同步队列里两个方法的顺序随机决定。
再来看一个例子case3:
let fs = require('fs')
fs.readFile('1.setImmediate.html', () => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
})
// => setImmediate -> timeout
回顾上面提到的阶段,在I/O事件
的回调中,setImmediate
的回调永远优先于setTimeout
的回调执行。所以返回结果的顺序是固定的。
Node系列-下一节手摸手带你撸一个commonjs规范