Heim > Artikel > Web-Frontend > Analyse der asynchronen Batch-Aktualisierung und des NextTick-Prinzips im Vue-Quellcode
Dieser Artikel stellt Ihnen die Analyse von asynchronen Batch-Updates und nextTick-Prinzipien im Vue-Quellcode vor. Er hat einen gewissen Referenzwert.
Vue ist bereits ein Drittel des inländischen Front-End-Webends. Es ist auch einer meiner wichtigsten Technologie-Stacks und ich bin gespannt, warum In letzter Zeit sind viele Communities zum Lesen von Vue-Quellcodes entstanden. Ich werde diese Gelegenheit nutzen, um einige meiner Gedanken beim Lesen des Quellcodes zusammenzufassen und einige Artikel zu verfassen als Zusammenfassung meiner eigenen Gedanken
Target Vue-Version: 2.5.17-beta.0
Vue-Quellcode-Kommentare: https://github.com/SHERlocked93/vue-analysis
Erklärung: Die Syntax des Quellcodes im Artikel verwendet Flow und der Quellcode basiert auf Alles wird bei Bedarf gekürzt (um nicht verwirrt zu werden@_@ Wenn Sie die Vollversion sehen möchten, geben Sie bitte die ein). Github-Adresse oben. Siehe die Artikeladresse unten~
s defineReactive
erfassten Abonnements nacheinander, wenn sie Aktualisierungen durchführen. Werfen wir einen Blick auf die Implementierung der setter
-Methode: dep.notify()
// src/core/observer/watcher.js /* Subscriber接口,当依赖发生改变的时候进行回调 */ update() { if (this.computed) { // 一个computed watcher有两种模式:activated lazy(默认) // 只有当它被至少一个订阅者依赖时才置activated,这通常是另一个计算属性或组件的render function if (this.dep.subs.length === 0) { // 如果没人订阅这个计算属性的变化 // lazy时,我们希望它只在必要时执行计算,所以我们只是简单地将观察者标记为dirty // 当计算属性被访问时,实际的计算在this.evaluate()中执行 this.dirty = true } else { // activated模式下,我们希望主动执行计算,但只有当值确实发生变化时才通知我们的订阅者 this.getAndInvoke(() => { this.dep.notify() // 通知渲染watcher重新渲染,通知依赖自己的所有watcher执行update }) } } else if (this.sync) { // 同步 this.run() } else { queueWatcher(this) // 异步推送到调度者观察者队列中,下一个tick时调用 } }
dep
Wenn es nicht subs
oder update
ist, wird der aktuelle Watcher, der update aufruft, in die Scheduler-Warteschlange verschoben und aufgerufen Schauen Sie beim nächsten Häkchen nach: // src/core/observer/scheduler.js /* 将一个观察者对象push进观察者队列,在队列中已经存在相同的id则 * 该watcher将被跳过,除非它是在队列正被flush时推送 */ export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { // 检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验 has[id] = true queue.push(watcher) // 如果没有正在flush,直接push到队列中 if (!waiting) { // 标记是否已传给nextTick waiting = true nextTick(flushSchedulerQueue) } } } /* 重置调度者状态 */ function resetSchedulerState () { queue.length = 0 has = {} waiting = false }
Hier wird eine computed watcher
Hash-Map verwendet, um zu überprüfen, ob die aktuelle Watcher-ID vorhanden ist. Wenn nicht, wird sie übersprungen vorhanden ist, wird es in die Warteschlange sync
verschoben und in der Hash-Tabelle für die nächste Überprüfung markiert, um wiederholte Hinzufügungen zu verhindern. Dies ist ein Prozess der Deduplizierung. Es ist zivilisierter, als jedes Mal in die Warteschlange gehen zu müssen, um beim Rendern die Änderungen desselben Beobachters zu überprüfen Die Ansicht wird hundertmal synchron geändert. Bei asynchroner queueWatcher
Aktualisierung der Daten wird nur die letzte Änderung aktualisiert.
Die Methode has
wird hier verwendet, um zu markieren, ob queue
an patch
übergeben wurde. Wenn es übergeben wurde, wird es nur in die Warteschlange verschoben, ohne patch
an . Warten Sie, bis
auf waiting
zurückgesetzt, sodass flushSchedulerQueue
an den Rückruf des nächsten Ticks übergeben werden kann > Rückruf darf nur einmal in einem Tick übergeben werden. Schauen wir uns an, was der an nextTick
übergebene Rückruf flushSchedulerQueue
bewirkt: nextTick
// src/core/observer/scheduler.js /* nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers */ function flushSchedulerQueue () { flushing = true let watcher, id queue.sort((a, b) => a.id - b.id) // 排序 for (index = 0; index MAX_UPDATE_COUNT) { // 持续执行了一百次watch代表可能存在死循环 warn() // 进入死循环的警告 break } } } resetSchedulerState() // 重置调度者状态 callActivatedHooks() // 使子组件状态都置成active同时调用activated钩子 callUpdatedHooks() // 调用updated钩子 }
resetSchedulerState
führt die waiting
-Methode in der false
-Methode aus. Diese Methode führt die flushSchedulerQueue
der Beobachter in eins nach dem anderen > Methode. Wir sehen, dass es zunächst eine flushSchedulerQueue
-Methode gibt, die die Beobachter in der Warteschlange nach ID von klein nach groß sortiert. Dadurch kann sichergestellt werden, dass: nextTick
flushSchedulerQueue
Die Reihenfolge der Komponentenaktualisierungen von der ist Reihenfolge der übergeordneten Komponente zur untergeordneten Komponente, da übergeordnete Komponenten immer vor untergeordneten Komponenten erstellt werden. nextTick
flushSchedulerQueue
queue
run
Benutzer-Watcher (Listener-Watcher) einer Komponente werden vor Render-Watchern ausgeführt, da Benutzer-Watcher oft früher erstellt werden als Render-Watcher queue.sort()
hier wird die Länge nicht zwischengespeichert wird durchgeführt, da während der Ausführung der Verarbeitung vorhandener Watcher-Objekte möglicherweise weitere Watcher-Objekte in die Warteschlange verschoben werden.
index Hier sehen Sie, was
übergeben wurde. Aber zuerst müssen Sie die Konzepte von 数据更改 -> setter -> Dep -> Watcher -> nextTick -> patch -> 更新视图
,
nextTick
nextTick
Erklären Sie, wann der Hauptthread die Synchronisierung abschließt Aufgabe: EventLoop
macro task
micro task
浏览器环境中常见的异步任务种类,按照优先级:
macro task
:同步代码、setImmediate
、MessageChannel
、setTimeout/setInterval
micro task
:Promise.then
、MutationObserver
有的文章把 micro task
叫微任务,macro task
叫宏任务,因为这两个单词拼写太像了 -。- ,所以后面的注释多用中文表示~
先来看看源码中对 micro task
与 macro task
的实现: macroTimerFunc
、microTimerFunc
// src/core/util/next-tick.js const callbacks = [] // 存放异步执行的回调 let pending = false // 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送 /* 挨个同步执行callbacks中回调 */ function flushCallbacks() { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i { setImmediate(flushCallbacks) } } else if (typeof MessageChannel !== 'undefined' && ( isNative(MessageChannel) || MessageChannel.toString() === '[object MessageChannelConstructor]' // PhantomJS )) { const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = flushCallbacks macroTimerFunc = () => { port.postMessage(1) } } else { macroTimerFunc = () => { setTimeout(flushCallbacks, 0) } } // 微任务 if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() microTimerFunc = () => { p.then(flushCallbacks) } } else { microTimerFunc = macroTimerFunc // fallback to macro }
flushCallbacks
这个方法就是挨个同步的去执行callbacks中的回调函数们,callbacks中的回调函数是在调用 nextTick
的时候添加进去的;那么怎么去使用 micro task
与 macro task
去执行 flushCallbacks
呢,这里他们的实现 macroTimerFunc
、microTimerFunc
使用浏览器中宏任务/微任务的API对flushCallbacks
方法进行了一层包装。比如宏任务方法 macroTimerFunc=()=>{ setImmediate(flushCallbacks) }
,这样在触发宏任务执行的时候 macroTimerFunc()
就可以在浏览器中的下一个宏任务loop的时候消费这些保存在callbacks数组中的回调了,微任务同理。同时也可以看出传给 nextTick
的异步回调函数是被压成了一个同步任务在一个tick执行完的,而不是开启多个异步任务。
注意这里有个比较难理解的地方,第一次调用 nextTick
的时候 pending
为false,此时已经push到浏览器event loop中一个宏任务或微任务的task,如果在没有flush掉的情况下继续往callbacks里面添加,那么在执行这个占位queue的时候会执行之后添加的回调,所以 macroTimerFunc
、microTimerFunc
相当于task queue的占位,以后 pending
为true则继续往占位queue里面添加,event loop轮到这个task queue的时候将一并执行。执行 flushCallbacks
时 pending
置false,允许下一轮执行 nextTick
时往event loop占位。
可以看到上面 macroTimerFunc
与 microTimerFunc
进行了在不同浏览器兼容性下的平稳退化,或者说降级策略:
macroTimerFunc
:setImmediate -> MessageChannel -> setTimeout
。首先检测是否原生支持 setImmediate
,这个方法只在 IE、Edge 浏览器中原生实现,然后检测是否支持 MessageChannel,如果对 MessageChannel
不了解可以参考一下这篇文章,还不支持的话最后使用 setTimeout
;
为什么优先使用 setImmediate
与 MessageChannel
而不直接使用 setTimeout
呢,是因为HTML5规定setTimeout执行的最小延时为4ms,而嵌套的timeout表现为10ms,为了尽可能快的让回调执行,没有最小延时限制的前两者显然要优于 setTimeout
。
microTimerFunc
:Promise.then -> macroTimerFunc
。首先检查是否支持 Promise
,如果支持的话通过 Promise.then
来调用 flushCallbacks
方法,否则退化为 macroTimerFunc
;
vue2.5之后 nextTick
中因为兼容性原因删除了微任务平稳退化的 MutationObserver
的方式。
最后来看看我们平常用到的 nextTick
方法到底是如何实现的:
// src/core/util/next-tick.js export function nextTick(cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true if (useMacroTask) { macroTimerFunc() } else { microTimerFunc() } } if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } } /* 强制使用macrotask的方法 */ export function withMacroTask(fn: Function): Function { return fn._withTask || (fn._withTask = function() { useMacroTask = true const res = fn.apply(null, arguments) useMacroTask = false return res }) }
nextTick
在这里分为三个部分,我们一起来看一下;
首先 nextTick
把传入的 cb
回调函数用 try-catch
包裹后放在一个匿名函数中推入callbacks数组中,这么做是因为防止单个 cb
如果执行错误不至于让整个JS线程挂掉,每个 cb
都包裹是防止这些回调函数如果执行错误不会相互影响,比如前一个抛错了后一个仍然可以执行。
然后检查 pending
状态,这个跟之前介绍的 queueWatcher
中的 waiting
是一个意思,它是一个标记位,一开始是 false
在进入 macroTimerFunc
、microTimerFunc
方法前被置为 true
,因此下次调用 nextTick
就不会进入 macroTimerFunc
、microTimerFunc
方法,这两个方法中会在下一个 macro/micro tick
时候 flushCallbacks
异步的去执行callbacks队列中收集的任务,而 flushCallbacks
方法在执行一开始会把 pending
置 false
,因此下一次调用 nextTick
时候又能开启新一轮的 macroTimerFunc
、microTimerFunc
,这样就形成了vue中的 event loop
。
最后检查是否传入了 cb
,因为 nextTick
还支持Promise化的调用:nextTick().then(() => {})
,所以如果没有传入 cb
就直接return了一个Promise实例,并且把resolve传递给_resolve,这样后者执行的时候就跳到我们调用的时候传递进 then
的方法中。
Vue源码中 next-tick.js
文件还有一段重要的注释,这里就翻译一下:
在vue2.5之前的版本中,nextTick基本上基于micro task
来实现的,但是在某些情况下micro task
具有太高的优先级,并且可能在连续顺序事件之间(例如#4521,#6690)或者甚至在同一事件的事件冒泡过程中之间触发(#6566)。但是如果全部都改成macro task
,对一些有重绘和动画的场景也会有性能影响,如 issue #6813。vue2.5之后版本提供的解决办法是默认使用micro task
,但在需要时(例如在v-on附加的事件处理程序中)强制使用macro task
。
为什么默认优先使用 micro task
呢,是利用其高优先级的特性,保证队列中的微任务在一次循环全部执行完毕。
强制 macro task
的方法是在绑定 DOM 事件的时候,默认会给回调的 handler 函数调用 withMacroTask
方法做一层包装 handler = withMacroTask(handler)
,它保证整个回调函数执行过程中,遇到数据状态的改变,这些改变都会被推到 macro task
中。以上实现在 src/platforms/web/runtime/modules/events.js 的 add
方法中,可以自己看一看具体代码。
刚好在写这篇文章的时候思否上有人问了个问题 vue 2.4 和2.5 版本的@input事件不一样 ,这个问题的原因也是因为2.5之前版本的DOM事件采用 micro task
,而之后采用 macro task
,解决的途径参考 中介绍的几个办法,这里就提供一个在mounted钩子中用 addEventListener
添加原生事件的方法来实现,参见 CodePen。
说这么多,不如来个例子,执行参见 CodePen
<p> <span>{{ name }}</span> <button>change name</button> </p><p></p> <script> new Vue({ el: '#app', data() { return { name: 'SHERlocked93' } }, methods: { change() { const $name = this.$refs.name this.$nextTick(() => console.log('setter前:' + $name.innerHTML)) this.name = ' name改喽 ' console.log('同步方式:' + this.$refs.name.innerHTML) setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML)) this.$nextTick(() => console.log('setter后:' + $name.innerHTML)) this.$nextTick().then(() => console.log('Promise方式:' + $name.innerHTML)) } } }) </script>
执行以下看看结果:
同步方式:SHERlocked93 setter前:SHERlocked93 setter后:name改喽 Promise方式:name改喽 setTimeout方式:name改喽
为什么是这样的结果呢,解释一下:
同步方式: 当把data中的name修改之后,此时会触发name的 setter
中的 dep.notify
通知依赖本data的render watcher去 update
,update
会把 flushSchedulerQueue
函数传递给 nextTick
,render watcher在 flushSchedulerQueue
函数运行时 watcher.run
再走 diff -> patch
那一套重渲染 re-render
视图,这个过程中会重新依赖收集,这个过程是异步的;所以当我们直接修改了name之后打印,这时异步的改动还没有被 patch
到视图上,所以获取视图上的DOM元素还是原来的内容。
Vor dem Setter: Warum wird der ursprüngliche Inhalt vor dem -Setter gedruckt? Das liegt daran, dass nextTick
die Callbacks beim Aufruf einzeln in das Callback-Array schiebt und dann ausführt. Wenn for
durchgeschleift und einzeln ausgeführt wird, ähnelt es dem Konzept der Warteschlange: Zuerst wird der Name geändert, der Render-Watcher wird in die Warteschlange schedulerQueue
gefüllt und seine Ausführungsfunktion übergeben flushSchedulerQueue
bis nextTick
, zu diesem Zeitpunkt befindet sich bereits setter前函数
in der Rückrufwarteschlange, da dieses cb
nach setter前函数
in die Rückrufwarteschlange verschoben wurde. Wenn dann Rückrufe in Rückrufen auf einem First-In ausgeführt werden, First-out-Basis, , das setter前函数
des Render-Watchers wird zu diesem Zeitpunkt nicht ausgeführt, sodass das gedruckte DOM-Element immer noch der ursprüngliche Inhalt ist. watcher.run
Nach dem Setter: Nach dem Setter wurde zu diesem Zeitpunkt ausgeführt. Zu diesem Zeitpunkt hat sich der Render-Watcher bereits flushSchedulerQueue
in die Ansicht geändert. Es wird also zu diesem Zeitpunkt der DOM-Inhalt nach der Änderung erhalten. patch
Versprechensmodus: entspricht zum Ausführen dieser Funktion. Zu diesem Zeitpunkt hat sich das DOM geändert. Promise.then
setTimeout-Methode: Führen Sie schließlich die Makroaufgabe aus, wenn sich das DOM geändert hat.
der synchrone Code ausgeführt wurde, die asynchronen Aufgaben noch nicht ausgeführt wurden und alle setter前函数
Funktionen ebenfalls ausgeführt wurden. Alle Rückrufe werden in die Rückrufwarteschlange verschoben, um auf die Ausführung zu warten. Wenn also $nextTick
ausgeführt wird, sieht die Rückrufwarteschlange wie folgt aus: [setter前函数
, setter前函数
, flushSchedulerQueue
, setter后函数
], was a ist Die Mikroaufgabenwarteschlange führt die Makroaufgabe Promise方式函数
nach Abschluss aus, sodass die oben genannten Ergebnisse gedruckt werden. setTimeout
, setImmediate
, MessageChannel
verschiedene Arten von Aufgaben in der Makroaufgabenwarteschlange des Browsers vorhanden sind, werden diese nacheinander in der Reihenfolge ausgeführt, in der sie dem Ereignis hinzugefügt werden Schleife in der oben genannten Reihenfolge. Wenn der Browser also setTimeout/setInterval
unterstützt und MessageChannel
nextTick
ausführt, dann werden von macroTimerFunc
Aufgaben und vom Benutzer gleichzeitig Aufgaben vom Typ nextTick
hinzugefügt In der Makrotask-Warteschlange wird , da die Priorität von setTimeout
höher ist als die von nextTick
, und das Gleiche gilt für MessageChannel
. setTimeout
setImmediate
Verwandte Empfehlungen:
Das obige ist der detaillierte Inhalt vonAnalyse der asynchronen Batch-Aktualisierung und des NextTick-Prinzips im Vue-Quellcode. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!