如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲),事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。
消息队列是类似队列的数据结构,遵循先入先出(FIFO)的规则。
执行完了后,执行栈再次为空,事件触发线程会重复上一步操作,再取出一个消息队列中的任务,这种机制就被称为事件循环(event loop)机制。
还是上面的代码:
console.log('script start') setTimeout(() => { console.log('timer over') }, 1000) // 点击页面 console.log('click page') console.log('script end') // script start // click page // script end // timer over执行过程:
主代码块(script)依次加入执行栈,依次执行,主代码块为:
console.log('script start')
setTimeout()
console.log('click page')
console.log('script end')
console.log() 为同步代码,JS引擎线程处理,打印 "script start",出栈;
遇到异步函数 setTimeout,交给定时器触发线程(异步触发函数为:setTimeout,回调函数为:() => { ... }),JS引擎线程继续,出栈;
console.log() 为同步代码,JS引擎线程处理,打印 "click page",出栈;
console.log() 为同步代码,JS引擎线程处理,打印 "script end",出栈;
执行栈为空,也就是JS引擎线程空闲,这时从消息队列中取出(如果有的话)一条任务(callback)加入执行栈,并执行;
重复第6步。
(此步的位置不确定)某个时刻(1000ms后),定时器触发线程通知事件触发线程,事件触发线程将回调函数 () => { ... } 加入消息队列队尾,等待JS引擎线程执行。
可以看出,setTimeout异步函数对应的回调函数( () => {} )会在执行栈为空,主代码块执行完了后才会执行。
零延时:
console.log('script start') setTimeout(() => { console.log('timer 1 over') }, 1000) setTimeout(() => { console.log('timer 2 over') }, 0) console.log('script end') // script start // script end // timer 2 over // timer 1 over这里会先打印 "timer 2 over",然后打印 "timer 1 over",尽管 timer 1 先被定时器触发线程处理,但是 timer 2 的callback会先加入消息队列。
上面,timer 2 的延时为 0ms,HTML5标准规定 setTimeout 第二个参数不得小于4(不同浏览器最小值会不一样),不足会自动增加,所以 "timer 2 over" 还是会在 "script end" 之后。
就算延时为 0ms,只是 timer 2 的回调函数会立即加入消息队列而已,回调的执行还是得等执行栈为空(JS引擎线程空闲)时执行。
其实 setTimeout 的第二个参数并不能代表回调执行的准确的延时事件,它只能表示回调执行的最小延时时间,因为回调函数进入消息队列后需要等待执行栈中的同步任务执行完成,执行栈为空时才会被执行。
宏任务与微任务以上机制在ES5的情况下够用了,但是ES6会有一些问题。
Promise同样是用来处理异步的:
console.log('script start') setTimeout(function() { console.log('timer over') }, 0) Promise.resolve().then(function() { console.log('promise1') }).then(function() { console.log('promise2') }) console.log('script end') // script start // script end // promise1 // promise2 // timer overWTF?? "promise 1" "promise 2" 在 "timer over" 之前打印了?
这里有一个新概念:macrotask(宏任务) 和 microtask(微任务)。
所有任务分为 macrotask 和 microtask:
macrotask:主代码块、setTimeout、setInterval等(可以看到,事件队列中的每一个事件都是一个 macrotask,现在称之为宏任务队列)
microtask:Promise、process.nextTick等
JS引擎线程首先执行主代码块。
每次执行栈执行的代码就是一个宏任务,包括任务队列(宏任务队列)中的,因为执行栈中的宏任务执行完会去取任务队列(宏任务队列)中的任务加入执行栈中,即同样是事件循环的机制。
在执行宏任务时遇到Promise等,会创建微任务(.then()里面的回调),并加入到微任务队列队尾。
microtask必然是在某个宏任务执行的时候创建的,而在下一个宏任务开始之前,浏览器会对页面重新渲染(task >> 渲染 >> 下一个task(从任务队列中取一个))。同时,在上一个宏任务执行完成后,渲染页面之前,会执行当前微任务队列中的所有微任务。
也就是说,在某一个macrotask执行完后,在重新渲染与开始下一个宏任务之前,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。
这样就可以解释 "promise 1" "promise 2" 在 "timer over" 之前打印了。"promise 1" "promise 2" 做为微任务加入到微任务队列中,而 "timer over" 做为宏任务加入到宏任务队列中,它们同时在等待被执行,但是微任务队列中的所有微任务都会在开始下一个宏任务之前都被执行完。
在node环境下,process.nextTick的优先级高于Promise,也就是说:在宏任务结束后会先执行微任务队列中的nextTickQueue,然后才会执行微任务中的Promise。
执行机制:
执行一个宏任务(栈中没有就从事件队列中获取)
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
渲染完毕后,JS引擎线程继续,开始下一个宏任务(从宏任务队列中获取)
总结