Vue.jsの
nextTickは、DOMの非同期更新に関連する機能であり、非常に興味深い概念です。この記事では、nextTickの基本的な使い方と内部動作について解説します。
一、実装例
まず、Vue.jsにおけるDOM更新とnextTickの挙動を理解するための例を見てみましょう。
テンプレート
<div class="app">
<div ref="messageDiv">{{message}}</div>
<div v-if="message1">$nextTick外で取得したメッセージ: {{message1}}</div>
<div v-if="message2">$nextTick内で取得したメッセージ: {{message2}}</div>
<div v-if="message3">$nextTick外で取得したメッセージ: {{message3}}</div>
<button @click="updateMessage">
メッセージを変更
</button>
</div>
Vueインスタンス
new Vue({
el: '.app',
data: {
message: 'こんにちは、Vue。',
message1: '',
message2: '',
message3: ''
},
methods: {
updateMessage() {
this.message = "こんにちは、世界。"
this.message1 = this.$refs.messageDiv.innerHTML
this.$nextTick(() => {
this.message2 = this.$refs.messageDiv.innerHTML
})
this.message3 = this.$refs.messageDiv.innerHTML
}
}
})
クリック前
クリック後
上記の例から、message1とmessage3は更新前の内容が表示されているのに対し、message2は更新後の内容が表示されていることがわかります。これは、Vue.jsにおけるDOM更新が非同期で行われるためです(詳細な説明は後述)。
二、使用シナリオ
次に、nextTickの主な使用シナリオとその理由について説明します。
- Vueライフサイクルの
created()フック関数内で行うDOM操作は、必ずVue.nextTick()のコールバック関数内に記述する必要があります
created()フック関数が実行される際、DOMはまだレンダリングされておらず、この時点でDOM操作を行っても効果がありません。そのため、DOM操作を行うJavaScriptコードは必ずVue.nextTick()のコールバック関数内に配置する必要があります。対照的なのがmounted()フック関数で、この関数が実行される時点ではすべてのDOMのマウントとレンダリングが完了しているため、このフック関数内でDOM操作を行っても問題ありません。
- データ変更後に実行する必要があり、その操作がデータ変更に伴って変更されるDOM構造を使用する必要がある場合、その操作は
Vue.nextTick()のコールバック関数内に配置する必要があります。
Vueの公式ドキュメントには以下のように説明されています:
VueはDOM更新を非同期で実行します。データの変更が検出されると、Vueはキューを開始し、同じイベントループで発生するすべてのデータ変更をバッファリングします。同じウォッチャーが複数回トリガーされた場合、キューには一度だけ追加されます。この重複データの排除は、不要な計算とDOM操作を避ける上で非常に重要です。次に、次のイベントループ"tick"で、Vueはキューをフラッシュし、実際の(重複排除済みの)作業を実行します。Vueは内部で、ネイティブの
Promise.thenとMessageChannelを使用して非同期キューを処理しようとします。実行環境がサポートしない場合は、setTimeout(fn, 0)を使用します。
例えば、
vm.someData = 'new value'を設定した場合、コンポーネントはすぐには再レンダリングされません。キューがフラッシュされると、コンポーネントはイベントループキューが空になった次の"tick"で更新されます。ほとんどの場合、このプロセスを気にする必要はありませんが、DOMの状態が更新された後に何かを行いたい場合、問題が発生することがあります。Vue.jsは通常、「データ駆動」の思考方式を開発者に奨励し、直接DOMに触れることを避けますが、時々直接DOMを操作する必要があります。データ変更後にVueがDOMの更新を完了するのを待ちたい場合は、データ変更直後にVue.nextTick(callback)を使用できます。これにより、コールバック関数はDOM更新完了後に呼び出されます。
三、nextTickのソースコード解析
機能
Vue.nextTickはコードの実行を遅らせるために使用され、2つのパラメータ(コールバック関数とコールバック関数の実行コンテキスト)を受け取ります。コールバック関数が提供されない場合、promiseオブジェクトが返されます。
ソースコード
/**
* タスクを非同期で実行するために遅延させる
*/
export const nextTick = (function () {
const callbackList = []
let isPending = false
let timerFunction
function nextTickHandler () {
isPending = false
const copiedCallbacks = callbackList.slice(0)
callbackList.length = 0
for (let i = 0; i < copiedCallbacks.length; i++) {
copiedCallbacks[i]()
}
}
// nextTickの動作はマイクロタスクキューを利用しており、ネイティブのPromise.thenまたはMutationObserver経由でアクセスできます
// MutationObserverはより広いサポートがありますが、iOS >= 9.3.3のUIWebViewではタッチイベントハンドラでトリガーされた場合、深刻なバグがあります
// 数回トリガーすると完全に停止してしまうため、ネイティブのPromiseが利用可能な場合はそれを使用します:
/* istanbul ignore if */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var promise = Promise.resolve()
var logError = err => { console.error(err) }
timerFunction = () => {
promise.then(nextTickHandler).catch(logError)
// 問題のあるUIWebViewでは、Promise.thenは完全に壊れるわけではなく、コールバックがマイクロタスクキューにプッシュされるもののキューがフラッシュされない奇妙な状態になることがあります
// ブラウザがタイマーなどの他の作業を行う必要があるまでです。そのため、空のタイマーを追加することでマイクロタスクキューを"強制的に"フラッシュできます。
if (isIOS) setTimeout(noop)
}
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJSとiOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// ネイティブのPromiseが利用できない場合(例: PhantomJS、iOS7、Android 4.4)にMutationObserverを使用します
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunction = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else {
// setTimeoutにフォールバック
/* istanbul ignore next */
timerFunction = () => {
setTimeout(nextTickHandler, 0)
}
}
return function queueNextTick (callback?: Function, context?: Object) {
let _resolve
callbackList.push(() => {
if (callback) {
try {
callback.call(context)
} catch (e) {
handleError(e, context, 'nextTick')
}
} else if (_resolve) {
_resolve(context)
}
})
if (!isPending) {
isPending = true
timerFunction()
}
if (!callback && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
})()
まず、nextTickで定義されている3つの重要な変数を理解しましょう。
callbackList
実行が必要なすべてのコールバック関数を格納するために使用されます
isPending
コールバック関数が実行中かどうかを示すフラグ
timerFunction
コールバック関数の実行をトリガーするために使用されます
次に、nextTickHandler()関数を見てみましょう。
function nextTickHandler () {
isPending = false
const copiedCallbacks = callbackList.slice(0)
callbackList.length = 0
for (let i = 0; i < copiedCallbacks.length; i++) {
copiedCallbacks[i]()
}
}
この関数は、callbackListに格納されているすべてのコールバック関数を実行するために使用されます。
次に、トリガーメソッドをtimerFunctionに割り当てます。
- まず、ネイティブでPromiseがサポートされているかどうかを判断し、サポートされている場合はPromiseを使用してコールバック関数の実行をトリガーします。
- それ以外の場合は、MutationObserverがサポートされているかどうかを判断し、MutationObserverのインスタンスを作成してテキストノードの変更を監視し、変更が検出されたときにすべてのコールバック関数の実行をトリガーします。
- どちらもサポートされていない場合は、タイムアウトを0に設定したsetTimeoutを使用します。
最後に、queueNextTick関数です。nextTickは即時関数であるため、queueNextTick関数が返される関数であり、ユーザーが渡したパラメータを受け取り、コールバックをcallbackListに格納するために使用されます。
上記は全体の実行フローであり、重要な点はtimerFunction()です。この関数は遅延実行の役割を果たします。上記の紹介から、timerFunction()には合計3つの実装方式があることがわかります。
PromiseMutationObserversetTimeout
その中でPromiseとsetTimeoutは理解しやすく、同期タスクとDOM更新の非同期タスクの後に具体的な関数をコールバックします。
次に、MutationObserverについて詳しく説明します。
MutationObserverはHTML5の新しいAPIで、DOMの変更を監視するためのインターフェースです。子ノードの削除、属性の変更、テキスト内容の変更など、DOMオブジェクトで発生する変更を監視できます。
呼び出しプロセスは非常に簡単ですが、少し通常とは異なります:まずコールバックをバインドする必要があります:
var mutationObserver = new MutationObserver(callback)
MutationObserverのコンストラクタにコールバックを渡すことで、MutationObserverのインスタンスを取得でき、このコールバックはMutationObserverのインスタンスが変更を検出したときにトリガーされます。
この時点では、MutationObserverのインスタンスにコールバックをバインドしただけです。具体的にどのDOMを監視し、ノードの削除を監視するのか、属性の変更を監視するのかなどはまだ設定していません。そのインスタンスのobserveメソッドを呼び出すことでこれを設定できます:
var domTarget = 監視したいDOMノード
mutationObserver.observe(domTarget, {
characterData: true // テキスト内容の変更を監視することを示します
})
nextTickにおけるMutationObserverの役割は上記の通りです。DOMの更新が検出されたら、コールバック関数を呼び出します。実際にMutationObserverを使用する理由は、nextTickが現在の同期コードの実行が完了した後に、実行したい非同期コールバックを実行するための非同期APIを必要としているからです。その中にはPromiseやsetTimeoutもこの理由に基づいています。さらに深く掘り下げるとmicrotaskなどの概念が関係してきますが、ここでは詳しく説明しません。