Vue nextTickメカニズム

6975 ワード

背景
まず、Vueの実行コードを見てみましょう.
export default { data () { return {
 msg: 0
 }
 },
 mounted () { this.msg = 1
 this.msg = 2
 this.msg = 3
 },
 watch: {
 msg () {
 console.log(this.msg)
 }
 }
}

このスクリプトの実行では、1、2、3の順に印刷されると推測されます.しかし、実際の効果では、1回:3しか出力されません.どうしてこんなことになったの?真相を探ってみましょう.
queueWatcher
watchリスニングmsgを定義し、実際にはVueによってvm.$が呼び出されます.watch(keyOrFn, handler, options).$watchは、私たちが初期化したときにvmにバインドされた関数で、Watcherオブジェクトを作成します.では、Watcherでhandlerをどのように処理しているかを見てみましょう.
this.deep = this.user = this.lazy = this.sync = false...
 update () { if (this.lazy) { this.dirty = true
 } else if (this.sync) { this.run()
 } else {
 queueWatcher(this)
 }
 }
...

初期設定deep = this.user = this.lazy = this.sync=false、すなわちupdate更新がトリガーされるとqueueWatcherメソッドが実行されます.
const queue: Array = []let has: { [key: number]: ?true } = {}let waiting = falselet flushing = false...export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) {
 has[id] = true
 if (!flushing) {
 queue.push(watcher)
 } else { // if already flushing, splice the watcher based on its id
 // if already past its id, it will be run next immediately.
 let i = queue.length - 1
 while (i > index && queue[i].id > watcher.id) {
 i--
 }
 queue.splice(i + 1, 0, watcher)
 } // queue the flush
 if (!waiting) {
 waiting = true
 nextTick(flushSchedulerQueue)
 }
 }
}

このnextTick(flushSchedulerQueue)のflushSchedulerQueue関数は、watcherのビュー更新です.
function flushSchedulerQueue () {
 flushing = true
 let watcher, id
 ... for (index = 0; index  
  

, waiting , , flushSchedulerQueue callbacks 。 nextTick , nexTick , Event Loop、microTask、macroTask ,Vue nextTick 。 , Event Loop , :

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  {
 setImmediate(nextTickHandler)
 }
 } else if (typeof MessageChannel !== 'undefined' && (
 isNative(MessageChannel) || // PhantomJS
 MessageChannel.toString() === '[object MessageChannelConstructor]'
 )) { const channel = new MessageChannel() const port = channel.port2
 channel.port1.onmessage = nextTickHandler
 timerFunc = () => {
 port.postMessage(1)
 }
 } else
 /* istanbul ignore next */
 if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex
 const p = Promise.resolve()
 timerFunc = () => {
 p.then(nextTickHandler)
 }
 } else { // fallback to setTimeout
 timerFunc = () => {
 setTimeout(nextTickHandler, 0)
 }
 } return function queueNextTick (cb?: Function, ctx?: Object) { let _resolve
 callbacks.push(() => { if (cb) { try {
 cb.call(ctx)
 } catch (e) {
 handleError(e, ctx, 'nextTick')
 }
 } else if (_resolve) {
 _resolve(ctx)
 }
 }) if (!pending) {
 pending = true
 timerFunc()
 } // $flow-disable-line
 if (!cb && typeof Promise !== 'undefined') { return new Promise((resolve, reject) => {
 _resolve = resolve
 })
 }
 }
})()

まずVueはcallback によってイベントキューをシミュレートし,イベントキュー のイベントはnexttickHandlerメソッドによって び され, が されるかはtimerFuncによって される.timeFuncの を てみましょう.
 if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
 timerFunc = () => {
 setImmediate(nextTickHandler)
 }
 } else if (typeof MessageChannel !== 'undefined' && (
 isNative(MessageChannel) || // PhantomJS
 MessageChannel.toString() === '[object MessageChannelConstructor]'
 )) { const channel = new MessageChannel() const port = channel.port2
 channel.port1.onmessage = nextTickHandler
 timerFunc = () => {
 port.postMessage(1)
 }
 } else
 /* istanbul ignore next */
 if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex
 const p = Promise.resolve()
 timerFunc = () => {
 p.then(nextTickHandler)
 }
 } else { // fallback to setTimeout
 timerFunc = () => {
 setTimeout(nextTickHandler, 0)
 }
 }

timerFuncの macroTask-->microTaskはDomのない ではweexのようなmicroTaskを いていることがわかる.
setImmediate、MessageChannel VS setTimeout
setImmediate、MessageChannelを に するのはなぜsetTimeoutではなくmacroTaskを に するのですか?HTML 5ではsettimeoutの は4 msと されているが, な では コールバックが も くても4 msでトリガできる.Vueは、コールバックを し、 に び すことを とする タスクをシミュレートするために、このような くの を します. 、MessageChannelとsetImmediateの はsetTimeoutより らかに さい.
を する
これらの があれば、 の をもう てみましょう.Vueのイベントメカニズムはイベントキューによって がスケジューリングされるため、メインプロセスが きを してからスケジューリングが われるので、すべてのプロセスの が してから します. のようなパフォーマンスのメリットがあります.
mountedの 、testの は++で1000 サイクル される があります.++のたびに、 に ってsetter->Dep->Watcher->update->runがトリガーされます.このとき でビューを しないと、++のたびにDOM ビューが され、パフォーマンスが に されます.したがって、Vueは、 のTick(または のTickのマイクロタスクフェーズ)のときにqueue のWatcherのrunを に するqueueキューを する. に、 じidを つWatcherはこのqueueに して されないので、1000 Watcherのrunは されません. ビューはtest DOMの0を 1000にするだけです.ビューの DOMを する は、 のスタックが された の のTick(または のTickのマイクロタスクフェーズ)のときに び され、パフォーマンスが に されます.
おもしろい
var vm = new Vue({ el: '#example', data: { msg: 'begin',
 },
 mounted () { this.msg = 'end'
 console.log('1')
 setTimeout(() => { // macroTask
 console.log('3')
 }, 0) Promise.resolve().then(function () { //microTask
 console.log('promise!')
 }) this.$nextTick(function () { console.log('2')
 })
 }
})

この は、1、promise、2、3の で されていることを っていると います.
  • にこれをトリガーしたからです.msg='end'は、watcherのupdateをトリガーし、 callback pushをvueのイベントキューに れます.
  • this.$nextTickはまた、setImmediate-->MessageChannel-->Promise-->setTimeoutによってtimeFuncを するイベントキューpushの しいcallback に りました.そしてresolve().thenはmicroTaskなので、promiseを に します.
  • MessageChannelとsetImmediateをサポートする 、 らの はsetTimeoutよりも される(IE 11/EdgeではsetImmediate は1 ms であり、setTimeoutでは 4 msの があるため、setImmediateはsetTimeout(0)よりも くコールバック を する. にイベントキューにはcallback が に されるので2が され、3
  • が される.
  • ですが、MessageChannelとsetImmediateがサポートされていない は、PromiseでtimeFuncが され、 バージョンVue 2.4 のバージョンでもpromiseが されます.この ,1,2,promise,3の になる.なぜならmsgは ずdom をトリガし、dom はcallbackに されて タイムキューに り、 にPromiseを する.resolve().then(function(){console.log('promise!')})のようなmicroTaskは、$nextTickを してcallbackに されます.キューは の を たすことを っているのでcallback の を します.