Open soulcm opened 7 years ago
先看一段代码,请使用chrome验证你所得出的答案
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end');
要答对上面的问题,就需要理解js本身的event loop。我们都知道js是单线程的,同一时间只能执行一个任务,那怎么才能做到异步的感觉呢。这就需要引入几个概念了, task、microtask、macrotask。
所有同步任务都将在主线程上执行,形成一个task队列和microtask的队列,然后先将task按顺序压入执行栈执行,task队列清空后就将microtask压入执行栈执行,在一次event loop中都清空后,就会进行一次视图的渲染,然后执行macrotask。
microtask紧跟着task,一旦没有task压入执行栈,microtask就会被压入而执行。常见的microtask有process.nextTick、Promise、MutationObserver等。
那macrotask又是什么呢,它其实就是setTimeout以及node中的setImmediat的回调,它们都是创建一个异步任务,会在event loop的末尾才执行,它其实也属于一个task。至于它与microtask的区别就是microtask会影响IO回调,microtask不执行完的话,界面会一直卡住,macrotask就不会有这个问题。
回到最开始,我们看下那段代码的执行过程
代码执行,同步代码全部放入在task或microtask中
tasks: [concole.log, setTimeout cb, concole.log] stack: [console.log] microtask: [promise then]
task的代码依次执行,先打印出script start,然后在打印出script end,此时剩余一个macrotask和microtask
script start
script end
tasks: [setTimeout cb] stack: [promise then] microtask: [promise then]
然后promise执行,打印promise 1,然后接着另一个promise打印promise 2
promise 1
promise 2
tasks: [setTimeout cb] stack: [setTimeout cb] microtask: []
打印setTimeout,至此队列全部清空,一个event loop完成。
setTimeout
tasks: [] stack: [] microtask: []
接着上面的知识,我们可以来看vue的nextTick方法了。在vue中,数据监测都是通过Object.defineProperty来重写里面的set和get方法实现的,vue更新DOM是异步的,每当观察到数据变化时,vue就开始一个队列,将同一事件循环内所有的数据变化缓存起来,等到下一次event loop,将会把队列清空,进行dom更新,内部使用的microtask MutationObserver来实现的。
虽然数据驱动建议避免直接操作dom,但有时也不得不需要这样的操作,这时就该Vue.nextTick(callback)出场了,它接受一个回调函数,在dom更新完成后,这个回调函数就会被调用。不管是vue.nextTick还是vue.prototype.$nextTick都是直接用的nextTick这个闭包函数
Vue.nextTick(callback)
export const nextTick = (function () { const callbacks = [] let pending = false let timerFunc function nextTickHandler () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } //other code })()
callbacks就是缓存的所有回调函数,nextTickHandler就是实际调用回调函数的地方。让这个函数延迟执行,vue优先用promise来实现,其次是html5的MutationObserver,然后是setTimeout。前两者属于microtask,后一个属于macrotask。都是达到一个异步的过程。
if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve() var logError = err => { console.error(err) } timerFunc = () => { p.then(nextTickHandler).catch(logError) if (isIOS) setTimeout(noop) } } else if (typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { var counter = 1 var observer = new MutationObserver(nextTickHandler) var textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } } else { timeFunc = () => { setTimeout(nextTickHandle, 0) } }
来看最后一部分
return function queueNextTick(cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) cb.call(ctx) if (_resolve) _resolve(ctx) }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
这就是我们正在调用的nextTick函数,在一个event loop内它会将调用nextTick的cb回调函数都放入callbacks中,pending用于判断是否有队列正在执行回调,例如有可能在nextTick中还有一个nextTick,此时就应该属于下一个循环了。最后几行代码是promise化,可以将nextTick按照promise方式去书写(暂且用的较少)。
event loop
Tasks microtasks queues and schedules
MutationObserver
从event loop看vue的nextTick
先看一段代码,请使用chrome验证你所得出的答案
要答对上面的问题,就需要理解js本身的event loop。我们都知道js是单线程的,同一时间只能执行一个任务,那怎么才能做到异步的感觉呢。这就需要引入几个概念了, task、microtask、macrotask。
所有同步任务都将在主线程上执行,形成一个task队列和microtask的队列,然后先将task按顺序压入执行栈执行,task队列清空后就将microtask压入执行栈执行,在一次event loop中都清空后,就会进行一次视图的渲染,然后执行macrotask。
microtask紧跟着task,一旦没有task压入执行栈,microtask就会被压入而执行。常见的microtask有process.nextTick、Promise、MutationObserver等。
那macrotask又是什么呢,它其实就是setTimeout以及node中的setImmediat的回调,它们都是创建一个异步任务,会在event loop的末尾才执行,它其实也属于一个task。至于它与microtask的区别就是microtask会影响IO回调,microtask不执行完的话,界面会一直卡住,macrotask就不会有这个问题。
回到最开始,我们看下那段代码的执行过程
代码执行,同步代码全部放入在task或microtask中
task的代码依次执行,先打印出
script start
,然后在打印出script end
,此时剩余一个macrotask和microtask然后promise执行,打印
promise 1
,然后接着另一个promise打印promise 2
打印
setTimeout
,至此队列全部清空,一个event loop完成。接着上面的知识,我们可以来看vue的nextTick方法了。在vue中,数据监测都是通过Object.defineProperty来重写里面的set和get方法实现的,vue更新DOM是异步的,每当观察到数据变化时,vue就开始一个队列,将同一事件循环内所有的数据变化缓存起来,等到下一次event loop,将会把队列清空,进行dom更新,内部使用的microtask MutationObserver来实现的。
虽然数据驱动建议避免直接操作dom,但有时也不得不需要这样的操作,这时就该
Vue.nextTick(callback)
出场了,它接受一个回调函数,在dom更新完成后,这个回调函数就会被调用。不管是vue.nextTick还是vue.prototype.$nextTick都是直接用的nextTick这个闭包函数callbacks就是缓存的所有回调函数,nextTickHandler就是实际调用回调函数的地方。让这个函数延迟执行,vue优先用promise来实现,其次是html5的MutationObserver,然后是setTimeout。前两者属于microtask,后一个属于macrotask。都是达到一个异步的过程。
来看最后一部分
这就是我们正在调用的nextTick函数,在一个event loop内它会将调用nextTick的cb回调函数都放入callbacks中,pending用于判断是否有队列正在执行回调,例如有可能在nextTick中还有一个nextTick,此时就应该属于下一个循环了。最后几行代码是promise化,可以将nextTick按照promise方式去书写(暂且用的较少)。
参考资料
event loop
Tasks microtasks queues and schedules
MutationObserver