NextTick の紹介
公式サイトの簡単な紹介によると、nextTick
次のDOMアップデートの更新を待つツール方法です。
型定義は次のとおりです。
function nextTick(callback?: () => void): Promise<void> {}
公式 Web サイトの詳細な紹介によると、nextTick
一般的な実装アイデアと使用法を知ることができます。
で
Vue
リアクティブ状態を変更すると、最終的なDOM
更新は同期されませんが、Vue
次の「ティック」までキューにキャッシュされます。これは、状態変化が何回発生したかに関係なく、各コンポーネントが確実に 1 回の更新のみを実行するようにするためです。
nextTick()
ステータス変更直後に使用して、DOM
更新が完了するまで待つことができます。コールバック関数を引数として渡すことも、await
それを返すこともできますPromise
。
公式サイトの説明が詳しく書かれているので深読みはせず、次は分析です。
nextTick の詳細と使用法
nextTickの使い方
nextTick
まず、公式サイトの紹介によると、次の2つの使い方があることが分かります。
- コールバック関数を渡す
nextTick(() => {// DOM 更新了
})
- を返す
Promise
nextTick().then(() => {// DOM 更新了
})
では、これら 2 つの方法を混合することはできるのでしょうか?
nextTick(() => {// DOM 更新了
}).then(() => {// DOM 更新了
})
次へティック現象
非常に単純なものを書いたところdemo
、混合できることがわかり、興味深い現象を発見しました。
const {createApp, h, nextTick} = Vue;
const app = createApp({data() {return {count: 0};},methods: {push() {nextTick(() => {console.log('callback before');}).then(() => {console.log('promise before');});this.count++;nextTick(() => {console.log('callback after');}).then(() => {console.log('promise after');});}},render() {console.log('render', this.count);const pushBtn = h("button", {innerHTML: "增加",onClick: this.push});const countText = h("p", {innerHTML: this.count});return h("div", {}, [pushBtn, countText]);}
});
app.mount("#app");
ここでは簡単のため使用しています
vue.global.js
。使用方法はVue3
同じですが、ESM
使用せずに紹介します。
実行結果は次のとおりです。
この例では、追加ボタンをクリックするとcount
増分操作が実行されます。このメソッドは 3 つの部分に分けることができます。
1.コールバック関数との混合使用nextTick
2.ペアに 1 を追加3.コールバック関数との混合使用Promise
count
nextTick
Promise
最初に登録されたものは1 を追加するnextTick
前に実行されcount
、2 番目に登録されたものは1 を追加したnextTick
後に実行されますcount
。
しかし、最終的な結果は非常に興味深いものです。
callback before
render 1
promise before
callback after
promise after
最初に登録されたnextTick
コールバック関数はrender
、その前に実行されますPromise
がrender
その後に実行されます。
2 番目に登録されたnextTick
コールバック関数はrender
、Promise
その後render
。
そして、どちらのnextTick
コールバック関数も実行より優先されますPromise
。
この現象はどうやって説明すればいいのでしょうか?nextTick
まずは分析の実装から始めます。
nextTickの実装
nextTick
ファイルには 200 行を超えるソース コードしかありませんpackages/runtime-core/src/scheduler.ts
。興味がある場合は、そのバージョンのソース コードに直接アクセスできますts
。引き続き、パッケージ化されたソース コードを確認します。
const resolvedPromise = /*#__PURE__*/ Promise.resolve();
let currentFlushPromise = null;
function nextTick(fn) {const p = currentFlushPromise || resolvedPromise;return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
一見するとびっくりしますが、nextTick
コードの量がそんなに少ないのですか?詳しく見てみると、nextTick
実装が実際にはPromise
カプセル化されていることがわかります。
現時点では他のことは無視して、このコードを見るだけで次のことがわかります。
nextTick
返されるのは、Promise
nextTick
コールバック関数はPromise
次のthen
メソッドで実行されます。
さて、前に述べたことに戻りますがdemo
、実際に答えの一部が見つかりました。
nextTick(() => {console.log('callback before');
}).then(() => {console.log('promise before');
});
this.count++;
上記の最終的な実行シーケンスをコードで表すと、次のようになります。
function nextTick(fn) {// 2. 返回一个 Promise, 并且在 Promise 的 then 方法中执行回调函数return Promise.resolve().then(fn);
}
// 1. 调用 nextTick,注册回调函数
const p = nextTick(() => {console.log('callback before');
})
// 3. 在 Promise 的 then 方法注册一个新的回调
p.then(() => {console.log('promise before');
});
// 4. 执行 count++
this.count++;
逆アセンブルされたコードからわかることは次のとおりです。
nextTick
返されるのは、Promise
nextTick
コールバック関数はPromise
次のthen
メソッドで実行されます。
Promise
の特性によれば、Promise
チェーンで呼び出すことができることがわかっているので、次のように書くことができます。
Promise.resolve().then(() => {// ...
}).then(() => {// ...
}).then(() => {// ...
});
そして の特性に従って、Promise
毎回Promise
新しいものが返されますPromise
。
同時に、メソッドが非同期で実行されることもわかっているため、上記のコードの実行順序についてはいくつかの推測がありますが、現時点では結論を出すことはできません。さらに深く掘り下げてみましょうPromise
。then
nextTickの実装詳細
上記のソースコードは非常に短いですが、currentFlushPromise
その中に変数があり、この変数はlet
using で宣言されています。すべての変数は using で宣言さconst
れています。この変数は宣言let
に使用されて、在庫がなければなりません。
検索を通じて、この変数が使用されている場所を見つけ、この変数を使用する方法が 2 つあることがわかります。
queueFlush
:currentFlushPromise
に設定されますPromise
flushJobs
:currentFlushPromise
に設定されますnull
キューフラッシュ
// 是否正在刷新
let isFlushing = false;
// 是否有任务需要刷新
let isFlushPending = false;
// 刷新任务队列
function queueFlush() {// 如果正在刷新,并且没有任务需要刷新if (!isFlushing && !isFlushPending) {// 将 isFlushPending 设置为 true,表示有任务需要刷新isFlushPending = true;// 将 currentFlushPromise 设置为一个 Promise, 并且在 Promise 的 then 方法中执行 flushJobscurrentFlushPromise = resolvedPromise.then(flushJobs);}
}
実際、これらのコードはコメントを書かなくても簡単に理解できます。名前を見れば意味がわかります。実際、ここで既にその概要を確認できます。
queueFlush
タスクキューをリフレッシュするために使用されるメソッドですisFlushing
リフレッシュしているかどうかを示しますが、このメソッドでは使用されません。isFlushPending
更新する必要があるタスク (キューに入れられたタスク) があるかどうかを示します。currentFlushPromise
現在更新する必要があるタスクを示します
上記の実装と組み合わせるとnextTick
、実際に非常に興味深い点が見つかります。resolvedPromise
両方とも使用されています。
const resolvedPromise = Promise.resolve();
function nextTick(fn) {// nextTick 使用 resolvedPromise return resolvedPromise.then(fn);
}
function queueFlush() {// queueFlush 也使用 resolvedPromisecurrentFlushPromise = resolvedPromise.then(flushJobs);
}
上記のコードを単純化すると、実際には次のようになります。
const resolvedPromise = Promise.resolve();
resolvedPromise.then(() => {// ...
});
resolvedPromise.then(() => {// ...
});
実際、このメソッドは複数のコールバック関数を登録するPromise
ために使用されthen
、リフレッシュが必要なすべてのタスクは同じPromise
メソッドに登録されるthen
ため、これらのタスクの実行順序が保証されます。これがキューです。
フラッシュジョブ
上記のメソッドでは、それがタスク キューをリフレッシュするために使用されるメソッドであるqueueFlush
ことがわかります。queueFlush
では、どのタスクを更新する必要があるのでしょうか? とにかく、最後に渡されるのはメソッドでありflushJobs
、このメソッドでも使用されていますcurrentFlushPromise
。これは単なる文字列ではありませんか?見てみましょう:
// 任务队列
const queue = [];
// 当前正在刷新的任务队列的索引
let flushIndex = 0;
// 刷新任务
function flushJobs(seen) {// 将 isFlushPending 设置为 false,表示当前没有任务需要等待刷新了isFlushPending = false;// 将 isFlushing 设置为 true,表示正在刷新isFlushing = true;// 非生产环境下,将 seen 设置为一个 Mapif ((process.env.NODE_ENV <img src="https://github.com/evanw/esbuild/issues/1610)const check = (process.env.NODE_ENV !== 'production')? (job) => checkRecursiveUpdates(seen, job): NOOP;// 检测递归调用是一个非常巧妙的操作,感兴趣的可以去看看源码,这里不做讲解try {for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {const job = queue[flushIndex];if (job && job.active !== false) {if ((process.env.NODE_ENV !== 'production') && check(job)) {continue;}// 执行任务callWithErrorHandling(job, null, 14 /* ErrorCodes.SCHEDULER */);}}}finally {// 重置 flushIndexflushIndex = 0;// 快速清空队列,直接给 数组的 length属性 赋值为 0 就可以清空数组queue.length = 0;// 刷新生命周期的回调flushPostFlushCbs(seen);// 将 isFlushing 设置为 false,表示当前刷新结束isFlushing = false;// 将 currentFlushPromise 设置为 null,表示当前没有任务需要刷新了currentFlushPromise = null;// pendingPostFlushCbs 存放的是生命周期的回调,// 所以可能在刷新的过程中又有新的任务需要刷新// 所以这里需要判断一下,如果有新添加的任务,就需要再次刷新if (queue.length || pendingPostFlushCbs.length) {flushJobs(seen);}" style="margin: auto" />
}
flushJobs
まず、タスクの現在のバッチの更新がすでに開始されているため、待つ必要はありません。その後、isFlushPending
に設定され、更新中であることを示します。false
isFlushing
true
これはメソッドとはまったく逆ですが、属性を必要とするタスクが現在存在することを意味し、そのタスクが現在リフレッシュ中であることを意味するため、queueFlush
これらの機能は相互に補完します。queueFlush
flushJobs
タスクの実行はメソッドを通じて実行され、内部のコードは非常に単純で、メソッドを実行して実行中にエラーをキャプチャし、そのエラーを処理のためにメソッドcallWithErrorHandling
に渡すというものです。onErrorCaptured
リフレッシュ タスクはqueue
属性に格納されます。これがqueue
上で説明したタスク キューです。このタスク キューに格納されているのは、リフレッシュする必要があるタスクです。
最後に、メソッドをクリアしqueue
て実行します。メソッドには通常、などのライフサイクル コールバックが保存されます。flushPostFlushCbs
flushPostFlushCbs
mounted
updated
キューへのタスクの追加
上で述べたようにqueue
、queue
タスクを追加するにはどうすればよいでしょうか?
queueJob
検索を通じて、タスクの追加に使用されるメソッドを見つけることができます。
// 添加任务,这个方法会在下面的 queueFlush 方法中被调用
function queueJob(job) {// 通过 Array.includes() 的 startIndex 参数来搜索任务队列中是否已经存在相同的任务// 默认情况下,搜索的起始索引包含了当前正在执行的任务// 所以它不能递归地再次触发自身// 如果任务是一个 watch() 回调,那么搜索的起始索引就是 +1,这样就可以递归调用了// 但是这个递归调用是由用户来保证的,不能无限递归if (!queue.length ||!queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) {// 如果任务没有 id 属性,那么就将任务插入到任务队列中if (job.id == null) {queue.push(job);}// 如果任务有 id 属性,那么就将任务插入到任务队列的合适位置else {queue.splice(findInsertionIndex(job.id), 0, job);}// 刷新任务队列queueFlush();}
}
ここにjob
関数があります。これは更新する必要があるタスクですが、この関数は 、 などのいくつかの属性を展開id
しpre
ますactive
。
ts
ソース コードには1 対の型定義がありますjob
。
export interface SchedulerJob extends Function {// id 就是排序的依据id?: number// 在 id 相同的情况下,pre 为 true 的任务会先执行// 这个在刷新任务队列的时候,在排序的时候会用到,本文没有讲解这方面的内容pre?: boolean// 标识这个任务是否明确处于非活动状态,非活动状态的任务不会被刷新active?: boolean// 标识这个任务是否是 computed 的 gettercomputed?: boolean/** * 表示 effect 是否允许在由 scheduler 管理时递归触发自身。 * 默认情况下,scheduler 不能触发自身,因为一些内置方法调用,例如 Array.prototype.push 实际上也会执行读取操作,这可能会导致令人困惑的无限循环。 * 允许的情况是组件更新函数和 watch 回调。 * 组件更新函数可以更新子组件属性,从而触发“pre”watch回调,该回调会改变父组件依赖的状态。 * watch 回调不会跟踪它的依赖关系,因此如果它再次触发自身,那么很可能是有意的,这是用户的责任来执行递归状态变更,最终使状态稳定。 */allowRecurse?: boolean/** * 在 renderer.ts 中附加到组件的渲染 effect 上用于在报告最大递归更新时获取组件信息。 * 仅限开发。 */ownerInstance?: ComponentInternalInstance
}
queueJob
この方法では、まずqueue
同じタスクが既に存在するかどうかを判定し、同じタスクが存在する場合には再度追加する必要はありません。
ここに保存されているタスクのほとんどはデータを変更するときにトリガーされるため、これは主に再帰呼び出しの問題を扱います。
データを変更する場合、 などの配列メソッドが使用されます。forEach
これらmap
のメソッドは実行時にトリガーされ、getter
メソッドもgetter
順番にトリガーされqueueJob
、再帰呼び出しが行われます。
したがって、ここで判断されisFlushing
、リフレッシュされている場合は;flushIndex
に設定されます+1
。
flushIndex
これは現在更新中のタスクのインデックスであり、+1
次のタスクから検索が開始されるため、同じタスクが繰り返し追加されて再帰呼び出しが発生することはありません。
コールwatch
バックは再帰的に呼び出すことができます。これはユーザーによって制御されるため、ここに追加の属性があります。コールバックallowRecurse
の場合は、に設定されます。watch
allowRecurse
true
これにより、再帰呼び出しの問題を回避できます。これは非常に賢い設計です。
queueJob
watchEffect
最後のものはエクスポートされ、これは、watch
などのタスクを追加する他のモジュールに使用されます。
フラッシュポストフラッシュCBS
flushPostFlushCbs
メソッドは、などのライフサイクル コールバックを実行するために使用されますmounted
。updated
flushPostFlushCbs
あまり詳しくは説明しませんが、全体的なプロセスはflushJobs
ほぼ同じです。
違いは、flushPostFlushCbs
タスクがバックアップされてから順次実行され、例外はキャッチされずに直接呼び出されることです。
興味のある学生は自分でソースコードを確認することができます。
問題の始まり
demo
元の質問 (記事の冒頭の例)に戻り、demo
最初にコードを確認してみましょう。
nextTick(() => {console.log('callback before');
}).then(() => {console.log('promise before');
});
this.count++;
nextTick(() => {console.log('callback after');
}).then(() => {console.log('promise after');
});
印刷結果は次のとおりです。
callback before
render 1
promise before
callback after
promise after
実際、ソース コードを見てみるとよくわかりますが、nextTick
最初のコードを登録したとき、queue
その中にはタスクがありませんでした。
また、メソッドはnextTick
呼び出されずqueueJob
、flushJobs
メソッドも呼び出されないため、この時点ではタスク キューは更新されません。
しかし、resolvedPromise
これは成功であるpromise
ため、nextTick
渡されたコールバック関数はマイクロタスク キューに配置され、実行を待機します。
nextTick
1 つも返されるpromise
ため、返されるコールバック関数もマイクロタスク キューに配置されますが、promise
コールバック関数then
よりも確実に遅れます。nextTick
次に、それを実行しますthis.count++
。ここでは内部実装ロジックにはまだ触れていません。queueJob
メソッドがトリガーされ、タスクがタスク キューに追加されることだけを知っておく必要があります。
最後に再度実行しましたがnextTick
、この時点ではqueue
すでにタスクが存在していたので、flushJobs
タスクキュー内のタスクを順番に実行するメソッドが呼び出されています。
強調:currentFlushPromise
この時点では値があり、その値はresolvedPromise
実行完了後に返されますPromise
。
初回との違いは、初回実行時に;nextTick
を使用したことcurrentFlushPromise
です。undefined
resolvedPromise
nextTick
初回実行時はflushJobs
メソッド登録と同じタスクが使用されることが分かりますPromise
。
2 回目の実行ではnextTick
、使用されるタスクはメソッドによって登録されたcurrentFlushPromise
タスクと同じではありませPromise
ん。flushJobs
Promise
これにより、nextTick
登録されたコールバック関数は、flushJobs
メソッドの登録されたコールバック関数の後に確実に実行されます。
具体的なプロセスは、次のコード例で見ることができます。
const resolvedPromise = Promise.resolve();
let count = 0;
// 第一次注册 nextTick
resolvedPromise.then(() => {console.log('callback before', count);
}).then(() => {console.log('promise before', count);
});
// 执行 this.count++
// 这里会触发 queueJob 方法,将任务添加到任务队列中
const currentFlushPromise = resolvedPromise.then(() => {count++;console.log('render', count);
});
// 第二次注册 nextTick
currentFlushPromise.then(() => {console.log('callback after', count);
}).then(() => {console.log('promise after', count);
});
上記のコードの結果をブラウザで実行すると、それが私たちの期待と一致していることがわかります。
具体的なプロセスを次の図に示します。
graph TD
A[resolvedPromise] -->|注册 nextTick 回调| B[nextTick callback before]
B -->|在 nextTick 返回的 promise 注册 then 的回调| C[nextTick promise then]
A -->|执行 value++ 会触发 queueJob| D[value++]
D -->|执行 flushJobs 会将 resolvedPromise 返回的 promise 赋值到 currentFlushPromise| E[currentFlushPromise]
E -->|注册 nextTick 回调使用的是 currentFlushPromise| F[nextTick callback after]
F -->|在 nextTick 返回的 promise 注册 then 的回调| G[nextTick promise after]
上記の同期マクロタスクが実行され、次のステップはマイクロタスクキューであり、そのプロセスは次のとおりです。
graph TD
A[resolvedPromise] -->|直接调用 then 里面注册的回调函数| B[then callbacks]
B -->|注册了多个,依次执行| C[nextTick callback before]
C -->|注册了多个,依次执行| D[value++]
こうして第二波のタスクも終わり、今回は主にタスクキューを更新するタスクですが、ここで実行されるのは実はnextTick
前回のタスクですtick
(公式サイトを見れば直到下一个“tick”才一起执行
意味が分かりました)。
次に、次の処理tick
(つまり、手動の犬の頭) を実行します。プロセスは次のとおりです。
graph TD
A[nextTick promise then] -->|因为是先注册的,所以先执行| B[nextTick promise before]
これで終わりです。はい、今回のタスクは、nextTick
返されたコールバックpromise
関数を実行することですthen
。
nextTick
返される合計は同じではないpromise
ため、返されるのは単一のタスクであり、優先度が高くなります。currentFlushPromise
promise
nextTick
promise
then
currentFlushPromise
このミッションが終了すると、次のミッションが行われますtick
。プロセスは次のとおりです。
graph TD
A[currentFlushPromise then] -->|因为是后注册的,所以相对于上面的后执行| B[nextTick callback after]
今回のタスクはコールバック関数を実行することでありcurrentFlushPromise
、これは戻り値が割り当てられるthen
呼び出しでもあります。flushJobs
flushJobs
resolvedPromise
Promise
currentFlushPromise
このミッションは終了し、最後のミッションですtick
。プロセスは次のとおりです。
graph TD
A[nextTick promise after] -->|最后一个| B[nextTick promise after]
この時点でプロセスは終了です。このプロセスは非常に頭を使うものですが、理解した後は非常に賢いものであることがわかり、私の思考能力と非同期についての理解が大幅に向上しました。
要約する
この記事では主に実装原理を分析しますが、ソースコードを分析すると、実装原理が非常に巧妙であるVue3
ことがわかりました。nextTick
nextTick
nextTick
実装原理はPromise
によって実装され、nextTick
これは 1 を返しPromise
、nextTick
コールバック関数はマイクロタスク キューに配置され、実行を待機します。
タスクキューが存在する状態で登録した場合nextTick
、nextTick
タスクキュー内のタスクが実行された後にコールバック関数が実行されます。
ここで使用されているアイデアは非常にシンプルで、Promise
連鎖呼び出し機能を利用するというものです。誰もが日常の開発で使用したことがあるかもしれませんが、このような方法で使用されるとは予想していませんでした。これは本当に非常に賢いものです。
やっと
JS の面接でよく聞かれる 75 の質問をまとめ、その回答と分析を提供しているので、JS に関する面接官の質問に基本的に対処できることが保証されます。
必要な友達は下のカードをクリックして受け取り、無料で共有できます