skip to content
usubeni fantasy logo Usubeni Fantasy

Vueの非同期更新メカニズム

/ 16 min read

This Post is Available In: CN EN ES JA

この記事では、Vueの非同期更新に関連する原理について説明します。この記事の中心的な理解は、キューという2つの単語です。

非同期更新

なぜ非同期更新が必要なのでしょうか?

this.a = 1;
this.b = 2;
this.c = 3;
this.a = 5;
this.a = 6;

考えてみてください。前の解説で説明したVueの反応性の原理に基づくと、反応性データは値の割り当て時にsetterが実行され、ビューの更新がトリガーされます。しかし、上記の例のように連続した代入のシナリオは非常に一般的です。ReactではsetState内に書くことができますが、Vueにはそのようなメソッドはありません。

すべてのデータ更新ごとにページを即座にリフレッシュするわけにはいきません。各代入ごとにrenderpatchを実行すると、ページ全体が非常に遅くなってしまいます。

この問題を解決するための鍵は、実行する必要がある更新関数をキューに保存し(また、キューに追加する際に同じ関数は重複して追加されません)、キューを非同期に実行することです。

関連する変数

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キューに入れるためのものです(引数はありませんが、flushSchedulerQueuequeueWatcherが処理した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);
}

簡単に言えば、flushSchedulerQueuequeueWatcherで整理された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)です。(ただし、なぜここでcallbackstimerFuncなどの変数をクロージャでラップして外部に直接置かないのかはよくわかりません)

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
评论组件加载中……