javascript - event loop

761 阅读4分钟

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还是有稍许不同,不过大致的概念是差不多的,只要弄懂其中的关系之后,代码中出现的问题就迎刃而解。