この記事では、Vueの非同期更新に関連する原理について説明します。この記事の中心的な理解は、キューという2つの単語です。
非同期更新
なぜ非同期更新が必要なのでしょうか?
this.a = 1;this.b = 2;this.c = 3;this.a = 5;this.a = 6;
考えてみてください。前の解説で説明したVueの反応性の原理に基づくと、反応性データは値の割り当て時にsetterが実行され、ビューの更新がトリガーされます。しかし、上記の例のように連続した代入のシナリオは非常に一般的です。ReactではsetState
内に書くことができますが、Vueにはそのようなメソッドはありません。
すべてのデータ更新ごとにページを即座にリフレッシュするわけにはいきません。各代入ごとにrender
、patch
を実行すると、ページ全体が非常に遅くなってしまいます。
この問題を解決するための鍵は、実行する必要がある更新関数をキューに保存し(また、キューに追加する際に同じ関数は重複して追加されません)、キューを非同期に実行することです。
関連する変数
Vueの非同期キューの原理を正式に分析する前に、使用されるこれらの変数を見てみましょう:
var circular = {}; // 循環参照の検出に使用される開発環境変数var queue = []; // ウォッチャーキューvar activatedChildren = [];var has = {}; // キューに追加されたかどうかのフラグvar waiting = false; // フラッシュを待っているかどうかを表すフラグvar flushing = false; // キューの処理が開始されたかどうかを表すフラグvar index = 0; // 現在のキューの実行位置
おそらく、waitingとflushingという非常に似た2つの変数がなぜ存在するのか疑問に思うかもしれません。
最初は私も疑問に思いました。flushing中かどうか、flushingを待っているか(waiting)か、微妙な違いがありますが:
- waiting中は非同期タスクが開始されていません
- flushing中はキュータスクが確定して開始されています
微妙な違いがありますが、それらのリセットタイミング(resetSchedulerState
)は同じです。2つの変数を分ける必要性はないように思えるので、一時的にコードをより自己説明的にするために2つの変数を使用しているのかもしれません。
キューに戻ると、このシステムには2つのキューが存在します。前の記事で説明したように、queueWatcher
は更新が必要なウォッチャーをキューに入れて、次回まとめて更新します。まずはウォッチャーのキューについて詳しく説明します。
queueWatcher
function queueWatcher(watcher) { var id = watcher.id; if (has[id] == null) { has[id] = true; // ウォッチャーがキューに追加されたことをマークする if (!flushing) { // キューがまだ更新されていない場合、直接キューに追加します queue.push(watcher); } else { // キューが処理中の場合、Vueのアプローチは新しいウォッチャーを現在の位置に挿入することです // これには隠されたロジックが含まれています // すでに実行されたウォッチャーは、この時点で次の実行にすぐに移行します var i = queue.length - 1; while (i > index && queue[i].id > watcher.id) { i--; } queue.splice(i + 1, 0, watcher); } // キューをフラッシュする if (!waiting) { waiting = true; nextTick(flushSchedulerQueue); } }}
最後の nextTick(flushSchedulerQueue)
は、ウォッチャーキューを nextTickキューに入れるためのものです(引数はありませんが、flushSchedulerQueue
はqueueWatcher
が処理したqueue
を読み取ることができます)。
この短い関数呼び出しは、2つの重要な要素を結びつけています:flushSchedulerQueue
はウォッチャーキューを処理するための中核であり、nextTick
はVueの非同期レンダリングの中核です。
flushSchedulerQueue
公式のコメントによると、queue.sort
はキューをソートするために使用されます。これは次のことを保証するためです:
- 親コンポーネントが子コンポーネントよりも先に処理されること
- ユーザーが定義したウォッチャー関数がレンダリングウォッチャーよりも先に処理されること
- コンポーネントが親コンポーネントで破棄された場合、このウォッチャーをスキップすることができること(ウォッチャーのアクティブプロパティを使用)
function flushSchedulerQueue() { flushing = true; var watcher, id;
queue.sort(function (a, b) { return a.id - b.id; });
// queueの長さは可変なので、queue.lengthはキャッシュできません for (index = 0; index < queue.length; index++) { watcher = queue[index]; id = watcher.id; has[id] = null; // 処理済みのウォッチャーをhasから削除 watcher.run(); }
// 状態をリセットする前にポストキューのコピーを保持する var activatedQueue = activatedChildren.slice(); var updatedQueue = queue.slice();
resetSchedulerState(); // キュー関連の変数をリセット
callActivatedHooks(activatedQueue); callUpdatedHooks(updatedQueue);}
簡単に言えば、flushSchedulerQueue
はqueueWatcher
で整理されたqueue
を処理するためのものです。
ここで、簡単な実践的な質問を提起します:ウォッチャーのコールバック関数内で最新のDOMにアクセスできますか?
答えは上記のコードに隠されています:flushSchedulerQueue
関数のqueue
のループ処理中、queue
はまだqueueWatcher
によってウォッチャーが追加されることがあります。これは主にユーザー定義のウォッチ関数やコンポーネント間の更新トリガーに使用されますが、次のループにスケジュールされずに直接queue
に挿入されます。
したがって、値の変化をウォッチし、コールバック関数内で最新のDOMを取得できると思わないでください。なぜなら、それらは同じタイミングで実行されるからです(また、コンポーネントのレンダリング関数は常に最後に実行されるためです)。
nextTick
var nextTick = (function() { var callbacks = [] var pending = false var timerFunc
function nextTickHandler() { pending = false var copies = callbacks.slice(0) callbacks.length = 0 for (var i = 0; i < copies.length; i++) { copies[i]() } }
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = function() { setImmediate(nextTickHandler) }} else if ( typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]')) { var channel = new MessageChannel() var port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = function() { port.postMessage(1) }} else if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex var p = Promise.resolve() timerFunc = function() { p.then(nextTickHandler) }} else { // fallback to setTimeout timerFunc = function() { setTimeout(nextTickHandler, 0) }}
return function queueNextTick(cb, ctx) { var _resolve callbacks.push(function() { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise(function(resolve, reject) { _resolve = resolve }) }}})()
まず、Vueが非同期実行を行うための方法を説明します。
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = function() { setImmediate(nextTickHandler) }} else if ( typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]')) { var channel = new MessageChannel() var port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = function() { port.postMessage(1) }} else if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex var p = Promise.resolve() timerFunc = function() { p.then(nextTickHandler) }} else { // fallback to setTimeout timerFunc = function() { setTimeout(nextTickHandler, 0) }}
return function queueNextTick(cb, ctx) { var _resolve callbacks.push(function() { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise(function(resolve, reject) { _resolve = resolve }) }}})()
まず、Vueが非同期実行を行うための方法を説明します。
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = function() { setImmediate(nextTickHandler) }} else if ( typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]')) { var channel = new MessageChannel() var port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = function() { port.postMessage(1) }} else if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex var p = Promise.resolve() timerFunc = function() { p.then(nextTickHandler) }} else { // fallback to setTimeout timerFunc = function() { setTimeout(nextTickHandler, 0) }}
return function queueNextTick(cb, ctx) { var _resolve callbacks.push(function() { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise(function(resolve, reject) { _resolve = resolve }) }}})()
まず、Vueが非同期実行を行うための方法を説明します。
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = function() { setImmediate(nextTickHandler) }} else if ( typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]')) { var channel = new MessageChannel() var port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = function() { port.postMessage(1) }} else if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex var p = Promise.resolve() timerFunc = function() { p.then(nextTickHandler) }} else { // fallback to setTimeout timerFunc = function() { setTimeout(nextTickHandler, 0) }}
return function queueNextTick(cb, ctx) { var _resolve callbacks.push(function() { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise(function(resolve, reject) { _resolve = resolve }) }}})()
まず、Vueが非同期実行を行うための方法を説明します。
非同期の遅延メカニズムです。バージョン2.4以前では、マイクロタスク(Promise/MutationObserver)を使用していましたが、マイクロタスクは実際には優先度が高すぎて、本来連続的に発生するイベントの間(例:#4521、#6690)や、同じイベントのバブリングの間(#6566)に挟まれて発生してしまいます。技術的には、setImmediateが理想的な選択肢ですが、どこでも利用できるわけではありません。また、同じループ内でトリガされたすべてのDOMイベントの後にコールバックをキューに入れる唯一のポリフィルは、MessageChannelを使用する方法です。
公式のコメントによると、最初のバージョンでは、VueはPromiseなどのマイクロタスクを使用して更新キューを作成していましたが、更新のタイミングが適切でないことがわかり、修正が行われました。
Vueでは、次の4つの方法で非同期呼び出しを作成できます:
- setImmediate
- MessageChannel(IE 10以降で使用可能で、Promiseよりも広範囲に使用できることに驚くかもしれません)
- Promise(Weex用に使用される)
- setTimeout 0(IE 10以下)
では、nextTick
は実際には何をしているのでしょうか?
コードを見ると、これは即時実行関数であり、queueNextTick
関数を返します。したがって、実際に使用しているのはqueueNextTick(cb, ctx)
です。(ただし、なぜここでcallbacks
やtimerFunc
などの変数をクロージャでラップして外部に直接置かないのかはよくわかりません)
nextTick(flushSchedulerQueue)
の場合、flushSchedulerQueue
はコールバック関数cbです。
- まず、cbはcallbacksにプッシュされます。これは非同期で実行されるキューであり、ウォッチャーキューとは異なります。
!pending
の条件に基づいて、pendingの場合はtimerFuncの実行をスキップし、前回のnextTickHandlerが完了するまでスキップします。
ちなみに、私たちが普段使っている$nextTick
は実際にはnextTick
関数と同じものです:
Vue.prototype.$nextTick = function (fn) { return nextTick(fn, this);};
結論
2つのキュー:
- queueWatcherのキュー
- nextTickのcallbacks
queueは比較的自由で、コンポーネントの更新時に必要な変更を迅速に追加するのに便利です。
callbacks
はキューのように見えますが、実際には常に1つの関数がキューイングされている状態を保持しています(理論的にはそうであるはずですが、どのように検証するかわかりません。間違いがあれば指摘してください)
フロー:
- 非同期更新の目的は、更新関数の重複実行を防ぎ、スムーズな実行を実現することです。
- setterがウォッチャーをトリガーした後、コールバック関数は即座に実行されず、
queueWatcher
関数を介してウォッチャーがqueue
に追加されます。 - アイドル状態のとき、
queueWatcher
関数はnextTick(flushSchedulerQueue)
を実行し、queue
を次のタイマーで実行します。 flushSchedulerQueue
関数はウォッチャーのキューを実行します。nextTick
は通常の$nextTick
と同じように、キューの実行を次のタイマーに遅延させるものです。- Vueは4つの方法で次のタイマーを作成します:setImmediate、MessageChannel、Promise、setTimeout