参考文献
Vue2のソースコード
問題の説明
このブログを書こうと思ったきっかけは、友人から vue の nextTick の実装原理について聞かれたためで、私はだいたいわかっているのですが、他の人にはまだよくわかっていないので、次の nextTick 部分のソースコードを読んでみました。 vue2 。
ここに来てください Xiaowei は一生懸命勉強しなければなりません
nextTick ソースコード
まずはvue(v2.7.7)のnextTick部分のソースコードを添付します(コメントは削除しています)
/* globals MutationObserver */
import {
noop } from 'shared/util'
import {
handleError } from './error'
import {
isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
const callbacks: Array<Function> = []
let pending = false
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick(): Promise<void>
export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
/**
* @internal
*/
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
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 => {
_resolve = resolve
})
}
}
ソースコードを分析する
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
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 => {
_resolve = resolve
})
}
}
nextTick のソース コードは実際には非常にシンプルで、最終的に nextTick メソッドを外部に公開しています。このメソッドでは、cb は渡されるコールバック関数で、ctx はこれが指すオブジェクトです。コールバック関数 cb が渡された場合、コールバック関数を格納する配列にコールバック関数をプッシュすると、すべてのコールバック関数がこの配列にプッシュされ、一律に呼び出されて実行されます。
const callbacks: Array<Function> = []
配列にプッシュした後、まず保留(非同期ロック)状態を判定し、初期状態は false で、入力後に保留を true に設定し、関数 timerFunc を呼び出します。nextTick がコールバック関数を渡さない場合は、Promise ベースの呼び出しを返します。
nextTick では多くのことが行われてきました。関数 timerFunc が何を行うかを見てみましょう。
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
この関数はたくさんのことをしているように見えますが、実際には現在の環境を判断しており、現在の環境に Promise が存在する場合、Promise は非同期パッケージング ソリューションとして使用されます。Promise がない場合は非同期パッケージング ソリューションとして MutationObserver を使用し、MutationObserver がない場合は setImmediate を使用し、setImmediate が存在しない場合は最も安全な setTimeout を使用します。
非同期でラップし、flushCallbacks 関数を呼び出します。
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
flashCallbacks は、非同期ロックをリセットし、ループ内のコールバックにプッシュされたコールバック関数を呼び出します。
要約すると、nextTick は実際に受信コールバック関数を配列にプッシュし、この配列内のコールバック関数の呼び出しを非同期タスクに入れます。
ソースコードを分析したら、nextTickの実装原理を分析しましょう
準備
nextTick がどのように機能するかを理解するには、まずいくつかのことを理解する必要があります。
1. nextTick は何のためにあるのですか
? Vue では dom をあまり操作することが推奨されていないことはわかっていますが、dom を操作しなければならない場合はどうすればよいでしょうか? その場合、問題が発生する可能性があります。つまり、dom が更新される前に dom を操作してしまった場合、期待する結果が得られない可能性があります。このとき、nextTickをdomの外側に置くと問題が解決する場合が多いです。
this.$nextTick(_ => {
document.getElementById('box').innerHtml
});
2. jsのイベントループ(イベントループ)とは
jsがシングルスレッドであることは皆さんご存知かと思いますが、なぜjsがシングルスレッドなのかというと、この言語を発明した目的がブラウザのスクリプト言語として機能するためです。js の主な目的はユーザーと対話し、dom を操作することです。これにより、js はシングルスレッドのみに対応できると判断されます。そうでない場合は、非常に複雑な同期の問題が発生します。
そして、js はシングルスレッドであるため、次のタスクは js の前のタスクが完了した後にのみ実行されます。しかし、タスクの 1 つに長い時間がかかる場合 (たとえば、10 秒のタイマーがある場合)、後者のタスクは、前のタスクが実行されるのを待ってから実行する必要がありますが、これは明らかに非科学的です。
この場合、jsはタスクを同期タスクと非同期タスクに分けており、jsを上から順に実行すると、非同期タスクに遭遇すると、非同期タスクを順番に非同期実行キューに入れ、メインスレッドの後に待機します。 (同期タスク) がすべて実行されると、戻って非同期実行キュー内のタスクを実行します。実行順序は原則として先入れ先出しで実行されます。メインスレッドはタスクキューからイベントを読み出し、この処理が継続的に周期的に行われるため、この動作機構をイベントループと呼びます。
3. 非同期マクロタスクと非同期マイクロタスクの上記のイベント ループを理解すると、実行順序については、原則として先入れ先出しの順序で実行されると書きましたが、実行も同様であることがわかります。非同期タスクキュー内の順序はまだ 実際には、非同期タスクは非同期マイクロタスクと非同期マクロタスクに分けられるため、非同期マイクロタスクは非同期マクロタスクの前に実行する必要があります。もちろん、非同期マイクロタスクが実行されると、非同期マイクロタスクに従って非同期実行に入らなければなりません。キューが実行されます。非同期マクロタスクについても同様です。では、非同期マイクロタスクと非同期マクロタスクとは何でしょうか?
一般的なものは次のとおりです。
マイクロタスク: Promise.then MutationObserver Promise.nextTick
マクロタスク: setInterval setTimeout postMessage setImmediate
問題分析
上記の 3 点を理解した後で、nextTick の実装原理を正式に分析してみましょう。
nextTick の実装原理を理解するには、なぜ dom が更新された後に nextTick が実行されるのかを理解することに他なりません。考えてみましょう、実際にこのようなコードをコードに書きました。
this.$nextTick(_ => {
});
しかし、なぜこれは魔法のように、dom が更新された後に実行しなければならないのでしょうか? dom の更新が完了したことをどのようにして知ることができるのでしょうか? あなたは唖然として好奇心を持っていますか?心配しないでください。これを理解するには、vue のレンダリング メカニズムを理解する必要があります。
まず、vue オブジェクトを構築するときに、受信データに対してゲッター/セッター変換が実行されることを知っています (vue の応答性の原則)。それをデータとしてマークし、コンポーネントがレンダリングされるときに、対応するコンポーネントがウォッチャー (オブザーバー) は、使用されるデータ内のデータを依存関係の依存関係として記録し、データ内のデータが変更されると、どのウォッチャーが依存関係の変更された部分を記録するかを確認し、データが変更されたかどうかを通知するようにウォッチャーに通知します。コマンドが変更された場合は、すぐにビューを更新してください。しかし、ウォッチャーが複数回トリガーされた場合、トリガーされるたびにビューの更新がトリガーされるのでしょうか? 答えは「いいえ」です。これは非常にパフォーマンスを消費するためです。ウォッチャーが複数回トリガーされた場合、最後のトリガーの結果のみが更新されます。
そして、ウォッチャーは update メソッドを呼び出してビューを更新します。Vue のソース コードを開いて、ウォッチャーの更新部分のコードを見つけます。
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
/*同步则执行run直接渲染视图*/
// 基本不会用到sync
} else {
queueWatcher(this)
}
}
ソースコードからウォッチャーの更新メソッドを確認できます。vue データが変更されても、ページはすぐには更新されませんが、重複排除などを行う必要があるため (上記の複数のトリガー ウォッチャーは最後にのみ更新されるため、重複排除する必要があります)、この更新では次を使用します。 queueWatcher メソッド。
次に、queueWatcher のソース コードを見つけます。
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
if (has[id] != null) {
return
}
if (watcher === Dep.target && watcher.noRecurse) {
return
}
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
if (__DEV__ && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
前のコードに関係なく、queueWatcher が表示されます。最後に、 nextTick メソッドを呼び出してビューを更新します。
この時点で、nextTick の動作原理がわかると思います。
-
まず第一に、データが更新されます。データの変更をリッスンした後、ウォッチャーはすぐに更新メソッドをトリガーし (これはコードに記述した nextTick の前に実行されます)、次に queueWatcher をトリガーし、最後にnextTick メソッド。関数呼び出しは nextTick によって非同期的にラップされた後、非同期実行キューに置かれます。
-
前のステップが完了すると、コードに記述した nextTick コールバック関数の呼び出しが非同期的にラップされ、前述のイベント ループ非同期タスクの実行順序と組み合わせて非同期実行キューに入れられます。非同期パッケージ化メソッドが同じであり、ビュー更新メソッドが最初にキューに入るからです。したがって、コードに記述された nextTick コールバックの実行は、常に dom が更新された後に実行されます。
要約すると、これが nextTick 実装の原則です。