前端面试常常会有这样的问题困扰你,哪个先输出,哪个后输出。。。
Promise.resolve('123').then(data=>{
console.log(1);
})
process.nextTick(function() {
console.log(2)
})
setImmediate(function(){
console.log(3)
})
setTimeout(function(){
console.log(4)
})
setTimeout(()=>{
console.log('setTime1');
Promise.resolve('123').then(data=>{
console.log('p');
})
})
setImmediate(()=>{
console.log('setImmediate1')
setTimeout(()=>{
console.log('setTimeout1')
})
})
setTimeout(()=>{
console.log('setTimeout2')
process.nextTick(()=>{console.log('nextick')})
setImmediate(()=>{
console.log('setImmediate2')
})
})
- 说实话不了解事件环的时候我都是蒙逼了,这都什么鬼。。。
- 看完这一篇,解决你所有困扰。
1.js单线程
- 谈到事件环,必须要说的就是进程和线程,进程是
操作系统分配资源和调度任务的基本单位
,线程是建立在进程上的一次程序运行单位
,一个进程上可以有多个线程。打开任务管理器,我们会发现,每一个起点任务都是进程,会占用内存,cpu,所以说每个网页都是一个单独的进程,即使自己挂掉,也不会影响其他页面的进程。
1.1 单线程
- 常常会听别人说js是单线程的,实际上js的主线程是单线程的,比如:不能同时操作一个DOM。
1.2 js其他线程
- 实际上js也有其他线程,比如:子线程,比如: 异步线程 (setTimeout,浏览器事件,ajax回调函数),那什么是同步和异步呢?
1.3 同步和异步
- 同步就是发出调用后,没有得到结果之前,该调用不返回,一旦调用返回,就得到返回值了。就是调用者主动等待这个调用的结果。
- 异步就是调用者在发出调用后这个调用就直接返回了,所以没有返回结果。不会立刻得到结果,而是调用发出后,被调用者通过状态、通知或回调函数处理这个调用。
- 而通常js里面的异步基本上包括定时器,ajax,promise,回调等,我们又把这些异步分为了宏任务和微任务。
1.4 宏任务和微任务
- 谈到宏任务和微任务,分为浏览器环境下的和node环境下的。
1.4.1 浏览器环境下
- macro-task(宏任务):
setTimeout, setInterval, setImmediate(ie浏览器特有),MessageChannel
- micro-task(微任务):
Promise.then,Object.observe(已废弃), MutationObserver
1.4.2 node环境下
- macro-task(宏任务):
setTimeout, setInterval, setImmediate, I/O
- micro-task(微任务):
process.nextTick,Promise.then,Object.observe(已废弃), MutationObserver
(vue中使用但由于不兼容放弃)
1.5 队列和栈
- 队列Queue 先进先出,就像管道一样,一头进去另一头出来,如图:
- 栈Stack先进后出,也就是后进先出,如图:
场景就是我们是函数作用域,函数放进去栈的顺序是
one -> rwo -> three
,但是执行顺序是three -> two -> one
function one() {
return function two(){
return function three(){
...
}
}
}
- 那在这里说队列跟栈有什么关系呢?主要是为了说明js执行过程中,同步执行的代码是要放在栈内执行的,而异步代码是要放在队列中等待后,拿到栈里执行的,什么意思呢?一言不合就上图
- js执行过程中是在栈中执行(也就是我们所说的主线程),主线程之外,还存在一个任务队列;
- 只要异步任务有了运行结果,就在任务队列之中放置一个事件。一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将队列中的事件一个一个打放到执行栈中依次执行;
- 如果队列中的事件有微任务,会在执行完栈中后,然后执行微任务队列,再去宏任务队列中去异步任务,这个过程是循环不断的。执行顺序 主线程=>microtask=>macrotask
- 举例说明一下
2.浏览器机制
console.log('start');
Promise.resolve('123').then(data=>{
console.log('Promise1');
});
setTimeout(()=>{
console.log('setTimeout1');
Promise.resolve('123').then(data=>{
console.log('Promise2');
})
});
setImmediate(()=>{
console.log('setImmediate1');
setTimeout(()=>{
console.log('setTimeout2');
});
});
setTimeout(()=>{
console.log('setTimeout3');
setImmediate(()=>{
console.log('setImmediate2');
});
});
console.log('end');
- 首先会执行栈中的,输出:start,end,
- 然后要去执行微任务队列,输出:promise1,
- 执行完毕后,一项一项地读取宏任务,而在chrome浏览器中
setImmediate
默认没有等待时间是要比默认等待时间为4ms的setTimeout
率先放入队列中的。- 此时队列的顺序应该是setImmediate1->setTimeout1->setTimeout3根据队列先进先出,所以此时首先输出:setImmediate1,
setImmediate1
中的setTimeout2
这个定时器会在4ms后放入队列,- 然后去队列中取下一项
setTimeout1
执行,输出setTimeout1,此时setTimeout1
这个定时器中含有微任务promise.then
,会在setTimeout1
执行完后马上执行,此时输出Promise1,- 再去队列中取出
setTimeout3
放到栈中执行,到此为止如果它里面的setImmediate2
如果此时4中的setTimeout2
已经先放入队列中了,说明首先setTimeout2
会被取出,放于栈中执行,如果此时setTimeout2
尚未放入队列中,那说明setImmidiate2
会被先取出,放于栈中执行。
start//首先执行栈中
end//首先执行栈中
Promise1//promise.then是微任务
setImmediate1
setTimeout1
Promise2
setTimeout3
setImmediate2//最后两个位置不能确定看执行速度而确定
setTimeout2
3. node运行机制
- 根据1.4.2会得知node的宏任务以及微任务。
- 那么相同的情况下,node中运行机制如何呢?与浏览器环境有何不同呢?一言不合就上图
- timers 阶段: 这个阶段执行setTimeout以及 setInterval的callback;
- poll 阶段: 执行poll中的I/O, 检查定时器是否到时;
- check 阶段: 执行setImmediate() 设定的callback;
- close callbacks 阶段: 比如socket.on(‘close’)的callback会在这个阶段执行。
- 值得注意到是
- 每一种任务会放进各自的队列中,只有当本队列全部执行完毕,才会走到下一个队列中;
- 每次跳队列的过程中会走到微任务队列中执行,执行完毕才会按照顺序队列往下走,每个队列执行顺序是固定的;
- 对于同是微任务的
process.nextTick
要快于promise.then
;- 简单理解整个过程
timer->microtask->I/O->microtask->setImmidate->microtask
。- 而对于
setTimeout
与setImmediate
来说,由node
运行速度决定,node
如果<4ms开启运行,那么setImmediate
要优先setTimeout
放入队列中。- 而poll阶段,不仅执行I/O操作,同时还会检查定时器是否到时,如果此时
timer
到时间,会去执行timer quene
而不会继续往下走,所以情况很多,并且特别烧脑。