javascript,区别于后台,就是javascript是单线程的。单线程做到不阻塞,起到作用的其实就是我们常说的异步。
运行时概念
首先,我们来理解一下javascript的几个概念
- 堆(heap)
- 栈(stack)
- 任务队列(queue),这里又分为宏任务 & 微任务
浏览器的event loop
当javascript运行的时候,首先,代码会进入执行栈,变量之类的会存储在堆中,而任务队列存储的就是javascript中的异步任务。
我们来看下下面的例子,首先,script代码会进入执行栈,然后执行同步代码,接着将异步任务放到任务队列中。
先执行同步代码,打印1,2,Promise(promise中的代码是同步执行的),3。
接着将异步任务放入任务队列中,promise回调放入微任务中,setTimeout回调放到宏任务中。
在event loop中,执行栈的代码执行完之后,在微任务队列取一个事件放到执行栈中执行,当微任务队列为空时,就从宏任务中取一个事件放到执行栈中执行,如此反复循环。
console.log(1)
setTimeout(() => {
console.log('setTimeout')
})
console.log(2)
new Promise((resolve, reject) => {
console.log('Promise')
resolve()
}).then(() => {
console.log('then')
})
console.log(3)
我们修改一下代码,我们在promise的回调中又加了一个promise。
其他不变,当执行第一个promise的回调时,同步执行第二个promise,这个没有问题,此时,把第二个promise的回调加入到微任务中。
在下一次event loop中,先查看微任务队列,于是执行第二个promise的回调,打印了then1。
最后,微任务队列清空了,于是查看宏任务,执行setTimeout的回调。
console.log(1)
setTimeout(() => {
console.log('setTimeout')
})
console.log(2)
new Promise((resolve, reject) => {
console.log('Promise')
resolve()
}).then(() => {
console.log('then')
new Promise((resolve1, reject1) => {
console.log('Promise1')
resolve1()
}).then(() => {
console.log('then1')
})
})
console.log(3)
我们前面的例子其实都是立即执行的代码,当发送http请求时,请求先挂起,当请求结果回来时,再将请求回调加入到任务队列中。
node的event loop
node代码也是javascript,解析javascript的是V8引擎。异步i/o采用的是libuv。
node的event loop,有六个事件,依次循环
- poll:获取新的i/o事件,大部分事件都在这里执行
- check:执行setImmediate的回调
- close:执行socket的close事件回调
- timers:执行setTimeout、setInterval的回调
- i/o:处理上一轮循环中少量未执行的i/o回调
- idle,prepare:node内部使用
我们来看下代码的运行情况
结果是这样的:同步任务 - nextTick - 微任务 - 宏任务 - setImmediate
setTimeout(() => {
console.log('setTimeout')
})
new Promise((resolve, reject) => {
console.log('Promise')
resolve()
}).then(() => {
console.log('then')
})
setImmediate(() => {
console.log('setImmediate')
})
process.nextTick(() => {
console.log('nextTick')
})
当我们把代码嵌到异步i/o里面呢
结果是这样的:同步任务 - nextTick - 微任务 - setImmediate -宏任务
与刚刚不同的是,代码放到异步i/o里面,执行完poll之后,执行的是check,所以setImmediate会在宏任务之前
setTimeout(() => {
console.log('setTimeout')
setTimeout(() => {
console.log('setTimeout1')
})
new Promise((resolve, reject) => {
console.log('Promise')
resolve()
}).then(() => {
console.log('then')
})
setImmediate(() => {
console.log('setImmediate')
})
process.nextTick(() => {
console.log('nextTick')
})
})
最后,我们来看一下node中宏任务与微任务的顺序
结果是先把宏任务队列中的回调全部执行完毕,接着执行全部nextTick,最后执行所有的微任务。
这个就是跟浏览器不同了,浏览器是执行完一个任务之后,先执行所有微任务,然后再执行下一个宏任务。
setTimeout(() => {
console.log('setTimeout')
Promise.resolve().then(() => {
console.log('then')
})
process.nextTick(() => {
console.log('nextTick')
})
})
setTimeout(() => {
console.log('setTimeout2')
Promise.resolve().then(() => {
console.log('then2')
})
process.nextTick(() => {
console.log('nextTick2')
})
})
process.nextTick()
在上面的例子中,我们会发现,nextTick的执行总是比微任务要快。
在node中,nextTick其实是独立于event loop之外的,nextTick拥有自己的任务队列,event loop,执行完一个阶段之后,就会将nextTick中的所有任务先清空,再执行微任务。
写在最后
浏览器的event loop 与node的event loop还是有稍许不同,不过大致的概念是差不多的,只要弄懂其中的关系之后,代码中出现的问题就迎刃而解。