ホームページ >ウェブフロントエンド >jsチュートリアル >Vue.js の非同期更新 DOM 戦略と nextTick インスタンスの詳細な説明

Vue.js の非同期更新 DOM 戦略と nextTick インスタンスの詳細な説明

小云云
小云云オリジナル
2018-01-25 11:17:291439ブラウズ

この記事では主に 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: &#39;begin&#39;
  };
 },
 methods () {
  handleClick () {
   this.test = &#39;end&#39;;
   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 を使用することしかできません。

なぜマイクロタスクを使用するのですか?


HTML 標準によれば、各タスクの実行後に UI が再レンダリングされ、マイクロタスク内でデータの更新が完了し、現在のタスクが終了すると最新の UI を取得できます。


逆に、データを更新するために新しいタスクを作成すると、レンダリングが 2 回実行されます。


Gu YilingのZhihuの回答を参照してください

最初はPromiseです。(Promise.resolve()).then()はマイクロタスクにコールバックを追加できます。

MutationObserverはtextNodeの新しいDOMオブジェクトを作成し、MutationObserverを使用してtextNodeをバインドしますDOM そして、コールバック関数を指定します。DOM が変更されると、コールバックがトリガーされます。つまり、textNode.data = String(counter) の場合、コールバックが追加されます。

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&#39;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&#39;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 !== &#39;production&#39; && has[id] != null) {
  circular[id] = (circular[id] || 0) + 1
  if (circular[id] > MAX_UPDATE_COUNT) {
  warn(
   &#39;You may have an infinite update loop &#39; + (
   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(&#39;flush&#39;)
 }
}

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: &#39;begin&#39;
  };
 },
 methods () {
  handleClick () {
   this.test = &#39;end&#39;;
   this.$nextTick(() => {
    console.log(this.$refs.test.innerText);//打印"end"
   });
   console.log(this.$refs.test.innerText);//打印“begin”
  }
 }
}

使用Vue.js的global API的$nextTick方法,即可在回调中获取已经更新好的DOM实例了。

相关推荐:

jQuery中DOM节点操作方法总结

DOM简介及节点、属性、查找节点

几种jQuery查找dom的方法


以上がVue.js の非同期更新 DOM 戦略と nextTick インスタンスの詳細な説明の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。