ホームページ > 記事 > ウェブフロントエンド > Vue.js の非同期更新 DOM 戦略と nextTick インスタンスの詳細な説明
この記事では主に Vue.js のソース コードから非同期更新 DOM 戦略と nextTick を紹介します。興味のある方は参考にしてください。
前に書いています
私は Vue.js に非常に興味があり、私が普段取り組んでいる技術スタックは Vue.js なので、過去数か月間、Vue.js のソース コードとアウトプット付きでまとめを作成しました。
記事の元のアドレス: https://github.com/answershuto/learnVue。
学習プロセス中に、Vue https://github.com/answershuto/learnVue/tree/master/vue-src に中国語のコメントが追加されました。Vue のソース コードを学習したい他の友人に役立つことを願っています。 。
理解にずれがある可能性がありますので、一緒に学び、進歩するために問題を提起し、指摘することを歓迎します。
DOM の操作
vue.js を使用する場合、次のような特定のビジネス シナリオにより DOM を操作する必要がある場合があります。明らかに test を「end」に設定していますが、実際の DOM ノードの innerText を取得すると、期待した「end」は取得されず、前の値「begin」が取得されます。
Watcher Queue
質問の結果、Vue.js ソース コードの Watch 実装が見つかりました。特定のリアクティブ データが変更されると、そのセッター関数がクロージャ内の Dep に通知し、Dep は管理するすべての Watch オブジェクトを呼び出します。 Watch オブジェクトの更新実装をトリガーします。アップデートの実装を見てみましょう。
<template> <p> <p ref="test">{{test}}</p> <button @click="handleClick">tet</button> </p> </template>Vue.js はデフォルトで DOM 更新の非同期実行を使用することがわかりました。
更新が非同期で実行される場合、queueWatcher関数が呼び出されます。
export default { data () { return { test: 'begin' }; }, methods () { handleClick () { this.test = 'end'; console.log(this.$refs.test.innerText);//打印“begin” } } }
queueWatcher のソースコードを見ると、Watch オブジェクトはすぐにはビューを更新せず、キューにプッシュされていることがわかりました。この時点では、ステータスは待機状態になっています。 Watch オブジェクトは引き続きこのキューにプッシュされ、次のティックを待っているときに、これらの Watch オブジェクトが走査されて取り出され、ビューが更新されます。同時に、繰り返しの ID を持つウォッチャーがキューに何度も追加されることはありません。これは、最終レンダリング中はデータの最終結果のみを考慮すればよいためです。
nextTick
vue.js は、実際には上記で呼び出された nextTick である nextTick 関数を提供します。
nextTick の実装は比較的単純です。実行の目的は、関数をマイクロタスクまたはタスクにプッシュし、現在のスタックが実行された後に nextTick によって渡された関数を実行することです (いくつかのタスクも実行する必要がある場合があります)。これは、queueNextTick インターフェイスを返す即時実行関数です。
受信した cb はコールバックにプッシュされて保存され、次に timerFunc が実行されます (pending は、timerFunc が次のティックの前に 1 回だけ実行されるようにするためのステータス マークです)。 timerFuncとは何ですか?
ソースコードを読んだ後、timerFunc は現在の環境を検出し、実際には Promise、MutationObserver、setTimeout の優先順位に基づいて、最悪の場合に SetTimeout が使用されることがわかりました。環境。
ここで説明します。timerFunc を取得するには、Promise、MutationObserver、setTimeout の 3 つの方法があります。
最初に Promise を使用し、Promise が存在しない場合は MutationObserver を使用します。これら 2 つのメソッドのコールバック関数は、setTimeout よりも前に実行されるため、最初に使用されます。環境が上記の 2 つのメソッドをサポートしていない場合は、setTimeout が使用され、この関数はタスクの最後にプッシュされ、呼び出しが実行されるのを待ちます。
最初にマイクロタスクを使用する必要があるのはなぜですか? Zhihu での Gu Yiling の回答から学びました。
JS のイベント ループは、実行時にタスクとマイクロタスクを区別します。各タスクが実行されると、エンジンはまず実行を完了します。その後、キューからすべてのマイクロタスクが実行されます。マイクロタスクキュー。setTimeout コールバックは実行のために新しいタスクに割り当てられ、Promise リゾルバーと MutationObserver コールバックは新しいマイクロタスクで実行されるように調整され、setTimeout によって生成されたタスクの前に実行されます。
新しいマイクロタスクを作成するには、ブラウザがサポートしていない場合は、まず Promise を使用して、MutationObserver を試してください。
本当に機能しません。タスクを作成するには setTimeout を使用することしかできません。
なぜマイクロタスクを使用するのですか?
Gu YilingのZhihuの回答を参照してください
最初はPromiseです。(Promise.resolve()).then()はマイクロタスクにコールバックを追加できます。
setTimeout是最后的一种备选方案,它会将回调函数加入task中,等到执行。
综上,nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。
flushSchedulerQueue
/*Github:https://github.com/answershuto*/ /** * Flush both queues and run the watchers. */ /*nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers*/ function flushSchedulerQueue () { flushing = true let watcher, id // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers can be skipped. /* 给queue排序,这样做可以保证: 1.组件更新的顺序是从父组件到子组件的顺序,因为父组件总是比子组件先创建。 2.一个组件的user watchers比render watcher先运行,因为user watchers往往比render watcher更早创建 3.如果一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过。 */ queue.sort((a, b) => a.id - b.id) // do not cache length because more watchers might be pushed // as we run existing watchers /*这里不用index = queue.length;index > 0; index--的方式写是因为不要将length进行缓存,因为在执行处理现有watcher对象期间,更多的watcher对象可能会被push进queue*/ for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id /*将has的标记删除*/ has[id] = null /*执行watcher*/ watcher.run() // in dev build, check and stop circular updates. /* 在测试环境中,检测watch是否在死循环中 比如这样一种情况 watch: { test () { this.test++; } } 持续执行了一百次watch代表可能存在死循环 */ if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn( 'You may have an infinite update loop ' + ( watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.` ), watcher.vm ) break } } } // keep copies of post queues before resetting state /**/ /*得到队列的拷贝*/ const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() /*重置调度者的状态*/ resetSchedulerState() // call component updated and activated hooks /*使子组件状态都改编成active同时调用activated钩子*/ callActivatedHooks(activatedQueue) /*调用updated钩子*/ callUpdateHooks(updatedQueue) // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit('flush') } }
flushSchedulerQueue是下一个tick时的回调函数,主要目的是执行Watcher的run函数,用来更新视图
为什么要异步更新视图
来看一下下面这一段代码
<template> <p> <p>{{test}}</p> </p> </template>
export default { data () { return { test: 0 }; }, created () { for(let i = 0; i < 1000; i++) { this.test++; } } }
现在有这样的一种情况,created的时候test的值会被++循环执行1000次。
每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。
如果这时候没有异步更新视图,那么每次++都会直接操作DOM更新视图,这是非常消耗性能的。
所以Vue.js实现了一个queue队列,在下一个tick的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,所以不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。
保证更新视图操作DOM的动作是在当前栈执行完以后下一个tick的时候调用,大大优化了性能。
访问真实DOM节点更新后的数据
所以我们需要在修改data中的数据后访问真实的DOM节点更新后的数据,只需要这样,我们把文章第一个例子进行修改。
<template> <p> <p ref="test">{{test}}</p> <button @click="handleClick">tet</button> </p> </template>
export default { data () { return { test: 'begin' }; }, methods () { handleClick () { this.test = 'end'; this.$nextTick(() => { console.log(this.$refs.test.innerText);//打印"end" }); console.log(this.$refs.test.innerText);//打印“begin” } } }
使用Vue.js的global API的$nextTick方法,即可在回调中获取已经更新好的DOM实例了。
相关推荐:
以上がVue.js の非同期更新 DOM 戦略と nextTick インスタンスの詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。