Heim > Web-Frontend > js-Tutorial > Hauptteil

Ausführliche Erläuterung der asynchronen Aktualisierungs-DOM-Strategie von Vue.j und der nextTick-Instanz

小云云
Freigeben: 2018-01-25 11:17:29
Original
1416 Leute haben es durchsucht

Dieser Artikel stellt hauptsächlich die asynchrone Update-DOM-Strategie und nextTick aus dem Vue.js-Quellcode vor. Ich hoffe, dass er jedem helfen kann, Vue.js asynchron zu verstehen.

Vorne geschrieben

Da ich mich sehr für Vue.js interessiere und der Technologie-Stack, an dem ich normalerweise arbeite, auch Vue.js ist, habe ich einiges ausgegeben In den letzten Monaten habe ich den Vue.js-Quellcode studiert und eine Zusammenfassung und Ausgabe erstellt.

Ursprüngliche Adresse des Artikels: https://github.com/answershuto/learnVue.

Während des Lernprozesses wurden chinesische Kommentare zu Vue https://github.com/answershuto/learnVue/tree/master/vue-src hinzugefügt. Ich hoffe, dass es anderen helfen kann, die lernen möchten Vue-Quellcode. Freunde sind hilfreich.

Es kann zu Unstimmigkeiten im Verständnis kommen. Gerne können Sie Probleme ansprechen und darauf hinweisen, um gemeinsam zu lernen und Fortschritte zu erzielen.

Operation DOM

Bei der Verwendung von vue.js müssen Sie manchmal das DOM aufgrund bestimmter Geschäftsszenarien bedienen, wie zum Beispiel diesem:


<template>
 <p>
 <p ref="test">{{test}}</p>
 <button @click="handleClick">tet</button>
 </p>
</template>
Nach dem Login kopieren
Nach dem Login kopieren


export default {
 data () {
  return {
   test: &#39;begin&#39;
  };
 },
 methods () {
  handleClick () {
   this.test = &#39;end&#39;;
   console.log(this.$refs.test.innerText);//打印“begin”
  }
 }
}
Nach dem Login kopieren

Warum haben wir den Test offensichtlich auf „Ende“ gesetzt, um den echten DOM-Knoten zu erhalten? das „Ende“ erhalten, das wir erwartet haben, aber den vorherigen Wert „Anfang“ erhalten?

Watcher-Warteschlange

Bei Fragen haben wir die Watch-Implementierung des Vue.js-Quellcodes gefunden. Wenn sich bestimmte reaktive Daten ändern, benachrichtigt seine Setter-Funktion Dep im Abschluss und Dep ruft alle von ihm verwalteten Watch-Objekte auf. Lösen Sie die Update-Implementierung des Watch-Objekts aus. Werfen wir einen Blick auf die Implementierung von Update.


update () {
 /* istanbul ignore else */
 if (this.lazy) {
  this.dirty = true
 } else if (this.sync) {
  /*同步则执行run直接渲染视图*/
  this.run()
 } else {
  /*异步推送到观察者队列中,下一个tick时调用。*/
  queueWatcher(this)
 }
}
Nach dem Login kopieren

Wir haben festgestellt, dass Vue.js standardmäßig die asynchrone Ausführung von DOM-Updates verwendet.

Wenn die Aktualisierung asynchron ausgeführt wird, wird die Funktion queueWatcher aufgerufen.


 /*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
export function queueWatcher (watcher: Watcher) {
 /*获取watcher的id*/
 const id = watcher.id
 /*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
 if (has[id] == null) {
 has[id] = true
 if (!flushing) {
  /*如果没有flush掉,直接push到队列中即可*/
  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 >= 0 && queue[i].id > watcher.id) {
  i--
  }
  queue.splice(Math.max(i, index) + 1, 0, watcher)
 }
 // queue the flush
 if (!waiting) {
  waiting = true
  nextTick(flushSchedulerQueue)
 }
 }
}
Nach dem Login kopieren

Als wir uns den Quellcode von queueWatcher ansahen, stellten wir fest, dass das Watch-Objekt die Ansicht nicht sofort aktualisierte, sondern in eine Warteschlange verschoben wurde. Der Status befindet sich im Wartezustand. Zu diesem Zeitpunkt werden Überwachungsobjekte weiterhin in diese Warteschlange verschoben. Beim Warten auf den nächsten Tick werden diese Überwachungsobjekte durchlaufen und entfernt, und die Ansicht wird aktualisiert. Gleichzeitig werden Beobachter mit doppelten IDs nicht mehrmals zur Warteschlange hinzugefügt, da wir uns beim endgültigen Rendern nur um das Endergebnis der Daten kümmern müssen.

Also, was ist der nächste Tick?

nextTick

vue.js stellt eine nextTick-Funktion bereit, bei der es sich tatsächlich um den oben aufgerufenen nextTick handelt.

Die Implementierung von nextTick ist relativ einfach. Der Zweck der Ausführung besteht darin, eine Funktion in eine Mikrotask oder Aufgabe zu verschieben und nextTick auszuführen, nachdem der aktuelle Stapel ausgeführt wurde (möglicherweise müssen auch einige Aufgaben ausgeführt werden). Schauen Sie sich den Quellcode für die übergebene Funktion an:


/**
 * Defer a task to execute it asynchronously.
 */
 /*
 延迟一个任务使其异步执行,在下一个tick时执行,一个立即执行函数,返回一个function
 这个函数的作用是在task或者microtask中推入一个timerFunc,在当前调用栈执行完以后以此执行直到执行到timerFunc
 目的是延迟到当前调用栈执行完以后执行
*/
export const nextTick = (function () {
 /*存放异步执行的回调*/
 const callbacks = []
 /*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
 let pending = false
 /*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
 let timerFunc

 /*下一个tick时的回调*/
 function nextTickHandler () {
 /*一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/
 pending = false
 /*执行所有callback*/
 const copies = callbacks.slice(0)
 callbacks.length = 0
 for (let i = 0; i < copies.length; i++) {
  copies[i]()
 }
 }

 // the nextTick behavior leverages the microtask queue, which can be accessed
 // via either native Promise.then or MutationObserver.
 // MutationObserver has wider support, however it is seriously bugged in
 // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
 // completely stops working after triggering a few times... so, if native
 // Promise is available, we will use it:
 /* istanbul ignore if */

 /*
 这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法
 优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法都会在microtask中执行,会比setTimeout更早执行,所以优先使用。
 如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。
 */
 if (typeof Promise !== &#39;undefined&#39; && isNative(Promise)) {
 /*使用Promise*/
 var p = Promise.resolve()
 var logError = err => { console.error(err) }
 timerFunc = () => {
  p.then(nextTickHandler).catch(logError)
  // in problematic UIWebViews, Promise.then doesn&#39;t completely break, but
  // it can get stuck in a weird state where callbacks are pushed into the
  // microtask queue but the queue isn&#39;t being flushed, until the browser
  // needs to do some other work, e.g. handle a timer. Therefore we can
  // "force" the microtask queue to be flushed by adding an empty timer.
  if (isIOS) setTimeout(noop)
 }
 } else if (typeof MutationObserver !== &#39;undefined&#39; && (
 isNative(MutationObserver) ||
 // PhantomJS and iOS 7.x
 MutationObserver.toString() === &#39;[object MutationObserverConstructor]&#39;
 )) {
 // use MutationObserver where native Promise is not available,
 // e.g. PhantomJS IE11, iOS7, Android 4.4
 /*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会触发回调*/
 var counter = 1
 var observer = new MutationObserver(nextTickHandler)
 var textNode = document.createTextNode(String(counter))
 observer.observe(textNode, {
  characterData: true
 })
 timerFunc = () => {
  counter = (counter + 1) % 2
  textNode.data = String(counter)
 }
 } else {
 // fallback to setTimeout
 /* istanbul ignore next */
 /*使用setTimeout将回调推入任务队列尾部*/
 timerFunc = () => {
  setTimeout(nextTickHandler, 0)
 }
 }

 /*
 推送到队列中下一个tick时执行
 cb 回调函数
 ctx 上下文
 */
 return function queueNextTick (cb?: Function, ctx?: Object) {
 let _resolve
 /*cb存到callbacks中*/
 callbacks.push(() => {
  if (cb) {
  try {
   cb.call(ctx)
  } catch (e) {
   handleError(e, ctx, &#39;nextTick&#39;)
  }
  } else if (_resolve) {
  _resolve(ctx)
  }
 })
 if (!pending) {
  pending = true
  timerFunc()
 }
 if (!cb && typeof Promise !== &#39;undefined&#39;) {
  return new Promise((resolve, reject) => {
  _resolve = resolve
  })
 }
 }
})()
Nach dem Login kopieren

Es ist eine Funktion zur sofortigen Ausführung und gibt eine queueNextTick-Schnittstelle zurück.

Der eingehende CB wird in Rückrufe gepusht und gespeichert, und dann wird timerFunc ausgeführt (ausstehend ist eine Statusmarkierung, um sicherzustellen, dass timerFunc vor dem nächsten Tick nur einmal ausgeführt wird).

Was ist timerFunc?

Nach dem Lesen des Quellcodes habe ich festgestellt, dass timerFunc die aktuelle Umgebung erkennt und unterschiedliche Implementierungen hat. Tatsächlich basiert es auf der Priorität von Promise, MutationObserver und setTimeout wird im ungünstigsten Umfeld eingesetzt.

Erklären Sie hier, dass es drei Möglichkeiten gibt, timerFunc zu erhalten: Promise, MutationObserver und setTimeout.
Verwenden Sie zuerst Promise und verwenden Sie MutationObserver, wenn Promise nicht vorhanden ist. Die Rückruffunktionen dieser beiden Methoden werden früher als setTimeout ausgeführt, sodass sie zuerst verwendet werden.
Wenn die Umgebung die beiden oben genannten Methoden nicht unterstützt, wird setTimeout verwendet. Diese Funktion wird am Ende der Aufgabe gepusht und wartet auf die Ausführung des Aufrufs.

Warum sollten Sie zuerst Microtask verwenden? Ich habe aus Gu Yilings Antwort auf Zhihu gelernt:

Die Ereignisschleife von JS unterscheidet bei der Ausführung zwischen Aufgaben und Mikroaufgaben. Nachdem die Engine die Ausführung jeder Aufgabe abgeschlossen hat, nimmt sie eine Aufgabe aus der Warteschlange zur Ausführung. Zuvor werden alle Mikrotasks in der Mikrotask-Warteschlange zuerst ausgeführt.

Der setTimeout-Rückruf wird einer neuen Aufgabe zur Ausführung zugewiesen, und die Promise-Resolver- und MutationObserver-Rückrufe werden so angeordnet, dass sie in einer neuen Mikrotask ausgeführt werden, die vor der von setTimeout generierten Aufgabe ausgeführt wird .

Um eine neue Mikrotask zu erstellen, verwenden Sie zuerst Promise. Wenn der Browser dies nicht unterstützt, versuchen Sie es mit MutationObserver.

Es funktioniert wirklich nicht, ich kann nur setTimeout verwenden, um eine Aufgabe zu erstellen.

Warum Microtask verwenden?

Gemäß HTML-Standard wird die Benutzeroberfläche nach der Ausführung jeder Aufgabe neu gerendert, dann wird die Datenaktualisierung in der Mikrotask abgeschlossen und die neueste Benutzeroberfläche ist verfügbar, wenn die aktuelle Aufgabe ausgeführt wird endet.

Wenn andererseits eine neue Aufgabe zum Aktualisieren von Daten erstellt wird, wird das Rendering zweimal durchgeführt.

Siehe Gu Yilings Zhihu-Antwort

Zuerst ist Promise, (Promise.resolve()).then() kann seinen Rückruf in Mikrotask hinzufügen,

Erstellen Sie einen neuen MutationObserver Ein DOM-Objekt von textNode. Verwenden Sie MutationObserver, um das DOM zu binden und eine Rückruffunktion anzugeben. Der Rückruf wird in die Mikrotask eingegeben, wenn textNode.data = String(counter) ist Rückruf wird hinzugefügt.

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;)
 }
}
Nach dem Login kopieren

flushSchedulerQueue是下一个tick时的回调函数,主要目的是执行Watcher的run函数,用来更新视图

为什么要异步更新视图

来看一下下面这一段代码


<template>
 <p>
 <p>{{test}}</p>
 </p>
</template>
Nach dem Login kopieren


export default {
 data () {
  return {
   test: 0
  };
 },
 created () {
  for(let i = 0; i < 1000; i++) {
  this.test++;
  }
 }
}
Nach dem Login kopieren

现在有这样的一种情况,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>
Nach dem Login kopieren
Nach dem Login kopieren


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”
  }
 }
}
Nach dem Login kopieren

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

相关推荐:

jQuery中DOM节点操作方法总结

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

几种jQuery查找dom的方法


Das obige ist der detaillierte Inhalt vonAusführliche Erläuterung der asynchronen Aktualisierungs-DOM-Strategie von Vue.j und der nextTick-Instanz. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage