Cet article partage principalement avec vous l'analyse de la Vue
stratégie de mise à jour asynchrone et du mécanisme DOM
dans nextTick
. Les lecteurs doivent avoir une certaine expérience dans l'utilisation de Vue
et être familiers avec le modèle de boucle d'événements JavaScript. J'espère que cela aide tout le monde.
<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'; console.log(this.$refs.test.innerText);//打印“begin” } } }
Le résultat imprimé est begin
au lieu du end
que nous avons défini. Ce résultat suffit à montrer que les mises à jour de Vue
dans DOM
ne sont pas synchronisées.
Ceci est expliqué dans la Vue
documentation officielle :
Peut-être que vous ne l'avez pas encore remarqué,Vue
exécuteDOM
les mises à jour de manière asynchrone. Chaque fois qu'un changement de données est observé,Vue
ouvrira une file d'attente et mettra en mémoire tampon toutes les modifications de données qui se produisent dans la même boucle d'événements. Si le mêmewatcher
est déclenché plusieurs fois, il ne sera poussé qu'une seule fois dans la file d'attente. Cette déduplication lors de la mise en mémoire tampon est très importante pour éviter les calculs etDOM
opérations inutiles. Ensuite, dans la boucle d'événements suivante "tick
",Vue
vide la file d'attente et effectue le travail réel (dédupliqué).
En bref, toutes les modifications de données qui se produisent dans une boucle d'événement déclencheront des mises à jour de vue dans la boucle d'événement suivante Tick
, qui est également un processus de « traitement par lots ». (Notez que le Tick
de la boucle d'événement suivante peut être exécuté dans la phase d'exécution de la microtâche Tick
actuelle, ou il peut être exécuté dans la Tick
suivante, principalement selon que la fonction nextTick
utilise Promise/MutationObserver
ou setTimeout
)
Dans le code source de Watcher
, nous avons constaté que le watcher
de update
est en fait asynchrone. (Remarque : l'attribut sync
est par défaut false
, ce qui est asynchrone) Le code de la fonction
update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { /*同步则执行run直接渲染视图*/ this.run() } else { /*异步推送到观察者队列中,下一个tick时调用。*/ queueWatcher(this) } }
queueWatcher(this)
est le suivant :
/*将一个观察者对象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 { ... } // queue the flush if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } }
Cette source le code a plusieurs exigences. Choses à noter :
La première chose que vous devez savoir est que lorsque watcher
s'exécute update
, il doit être asynchrone par défaut. deux choses :
Juger s'il y a ceci has
dans le watcher
tableauid
S'il y a , il n'est pas nécessaire de mettre watcher
Ajouter à queue
, sinon aucun traitement ne sera effectué.
Dans le nextTick(flushSchedulerQueue)
, la fonction principale de la fonction flushScheduleQueue
est d'effectuer l'opération de mise à jour de la vue. Elle prendra tous les queue
dans watcher
. et effectuez les mises à jour de vue correspondantes. Le cœur de
est en fait la fonction nextTick
Examinons de plus près à quoi sert nextTick
. La fonction
nextTick
fait en fait deux choses. La première consiste à générer un timerFunc
et à participer au rappel en tant que microTask
ou macroTask
. Venez dans la boucle événementielle. La seconde consiste à placer la fonction de rappel dans une file d'attente callbacks
et à attendre le moment approprié pour s'exécuter. (Ce timing est lié aux différentes implémentations de timerFunc
)
Tout d'abord, regardons comment il génère un timerFunc
et utilise le rappel comme microTask
ou macroTask
.
if (typeof Promise !== 'undefined' && 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't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microTask queue but the queue isn'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 !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // 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) } }
Il convient de noter qu'il appellera la fonction de rappel entrant en fonction de la priorité de Promise
, MutationObserver
et setTimeout
. Les deux premiers généreront une tâche microTask
, tandis que le second générera un macroTask
. (Microtâches et macrotâches)
La raison pour laquelle une telle priorité est définie est principalement pour considérer la compatibilité entre les navigateurs (IE
n'a pas de Promise
intégré). De plus, définir Promise
comme priorité la plus élevée est dû au fait que la fonction de rappel Promise.resolve().then
appartient à une microtâche , et le navigateur effacera tout Tick actuel après avoir exécuté macroTask
dans a Tick
> Effectuez ensuite le rendu microTask
et placez l'opération de mise à jour UI
à l'étape où DOM
exécute Tick
Par rapport à l'utilisation de microTask
pour générer un setTimeout
, il y en aura un de moins. macroTask
rendu. UI
est en fait la fonction que nous voulons vraiment exécuter. nextTickHandler
function nextTickHandler () { pending = false /*执行所有callback*/ const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
variables ici sont destinées à la callbacks
consommation. Quant à la deuxième fonction de la fonction nextTickHandler
que nous avons mentionnée plus tôt, "attendre le moment approprié pour s'exécuter", c'est en fait à cause de la différence d'implémentation de nextTick
. Si c'est timerFunc
, alors le <. 🎜> le rappel est un PromiseMutationObserver
, il sera exécuté à la fin du nextTickHandler
en cours. Si c'est microTask
, alors le rappel Tick
est un setTiemout
, qui sera exécuté le nextTickHandler
suivant. macroTask
还有就是callbacks
中的成员是如何被push
进来的?从源码中我们可以知道,nextTick
是一个自执行的函数,一旦执行是return
了一个queueNextTick
,所以我们在调用nextTick
其实就是在调用queueNextTick
这个函数。它的源代码如下:
return function queueNextTick (cb?: Function, ctx?: Object) { let _resolve /*cb存到callbacks中*/ callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise((resolve, reject) => { _resolve = resolve }) } }
可以看到,一旦调用nextTick
函数时候,传入的function
就会被存放到callbacks
闭包中,然后这个callbacks
由nextTickHandler
消费,而nextTickHandler
的执行时间又是由timerFunc
来决定。
我们再回来看Watcher
中的一段代码:
/*将一个观察者对象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 { ... } // queue the flush if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } }
这里面的nextTick(flushSchedulerQueue)
中的flushSchedulerQueue
函数其实就是watcher
的视图更新。每次调用的时候会把它push
到callbacks
中来异步执行。
另外,关于waiting
变量,这是很重要的一个标志位,它保证flushSchedulerQueue
回调只允许被置入callbacks
一次。
所以,也就是说DOM
确实是异步更新,但是具体是在下一个Tick
更新还是在当前Tick
执行microTask
的时候更新,具体要看nextTcik
的实现方式,也就是具体跑的是Promise/MutationObserver
还是setTimeout
。
附:nextTick
源码带注释),有兴趣可以观摩一下。
这里面使用Promise
和setTimeout
来执行异步任务的方式都很好理解,比较巧妙的地方是利用MutationObserver
执行异步任务。MutationObserver
是H5
的新特性,它能够监听指定范围内的DOM
变化并执行其回调,它的回调会被当作microTask
来执行,具体参考MDN
,。
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) }
可以看到,通过借用MutationObserver
来创建一个microTask
。nextTickHandler
作为回调传入MutationObserver
中。
这里面创建了一个textNode
作为观测的对象,当timerFunc
执行的时候,textNode.data
会发生改变,便会触发MutatinObservers
的回调函数,而这个函数才是我们真正要执行的任务,它是一个microTask
。
注:2.5+
版本的Vue
对nextTick
进行了修改,具体参考下面“版本升级”一节。
nextTick
继续来看下面的这段代码:
<p id="example"> <p ref="test">{{test}}</p> <button @click="handleClick">tet</button> </p>
var vm = new Vue({ el: '#example', data: { test: 'begin', }, methods: { handleClick() { this.test = 'end'; console.log('1') setTimeout(() => { // macroTask console.log('3') }, 0); Promise.resolve().then(function() { //microTask console.log('promise!') }) this.$nextTick(function () { console.log('2') }) } } })
在Chrome
下,这段代码执行的顺序的1、2、promise、3
。
可能有同学会以为这是1、promise、2、3
,其实是忽略了一个标志位pending
。
我们回到nextTick
函数return
的queueNextTick
可以发现:
return function queueNextTick (cb?: Function, ctx?: Object) { let _resolve /*cb存到callbacks中*/ callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise((resolve, reject) => { _resolve = resolve }) } }
这里面通过对pending
的判断来检测是否已经有timerFunc
这个函数在事件循环的任务队列等待被执行。如果存在的话,那么是不会再重复执行的。
最后异步执行nextTickHandler
时又会把pending
置为false
。
function nextTickHandler () { pending = false /*执行所有callback*/ const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }
所以回到我们的例子:
handleClick() { this.test = 'end'; console.log('1') setTimeout(() => { // macroTask console.log('3') }, 0); Promise.resolve().then(function() { //microTask console.log('promise!') }) this.$nextTick(function () { console.log('2') }) }
代码中,this.test = 'end'
必然会触发watcher
进行视图的重新渲染,而我们在文章的Watcher
一节中就已经有提到会调用nextTick
函数,一开始pending
变量肯定就是false
,因此它会被修改为true
并且执行timerFunc
。之后执行this.$nextTick
其实还是调用的nextTick
函数,只不过此时的pending
为true
说明timerFunc
已经被生成,所以this.$nextTick(fn)
只是把传入的fn
置入callbacks
之中。此时的callbacks
有两个function
成员,一个是flushSchedulerQueue
,另外一个就是this.$nextTick()
的回调。
因此,上面这段代码中,在Chrome
下,有一个macroTask
和两个microTask
。一个macroTask
就是setTimeout
,两个microTask
:分别是Vue
的timerFunc
(其中先后执行flushSchedulerQueue
和function() {console.log('2')}
)、代码中的Promise.resolve().then()
。
上面讨论的nextTick
实现是2.4
版本以下的实现,2.5
以上版本对于nextTick
的内部实现进行了大量的修改。
首先是从Vue 2.5+
开始,抽出来了一个单独的文件next-tick.js
来执行它。
在代码中,有这么一段注释:
其大概的意思就是:在Vue 2.4
之前的版本中,nextTick
几乎都是基于microTask
实现的(具体可以看文章nextTick
一节),但是由于microTask
的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,就会导致一些诡异的问题;但是如果全部都改成macroTask
,对一些有重绘和动画的场景也会有性能的影响。所以最终nextTick
采取的策略是默认走microTask
,对于一些DOM
的交互事件,如v-on
绑定的事件回调处理函数的处理,会强制走macroTask
。
具体做法就是,在Vue
执行绑定的DOM
事件时,默认会给回调的handler
函数调用withMacroTask
方法做一层包装,它保证整个回调函数的执行过程中,遇到数据状态的改变,这些改变而导致的视图更新(DOM
更新)的任务都会被推到macroTask
。
function add$1 (event, handler, once$$1, capture, passive) { handler = withMacroTask(handler); if (once$$1) { handler = createOnceHandler(handler, event, capture); } target$1.addEventListener( event, handler, supportsPassive ? { capture: capture, passive: passive } : capture ); } /** * Wrap a function so that if any code inside triggers state change, * the changes are queued using a Task instead of a MicroTask. */ function withMacroTask (fn) { return fn._withTask || (fn._withTask = function () { useMacroTask = true; var res = fn.apply(null, arguments); useMacroTask = false; return res }) }
而对于macroTask
的执行,Vue
优先检测是否支持原生setImmediate
(高版本IE和Edge支持),不支持的话再去检测是否支持原生MessageChannel
,如果还不支持的话为setTimeout(fn, 0)
。
最后,写一段demo来测试一下:
<p id="example"> <span>{{test}}</span> <button @click="handleClick">change</button> </p>
var vm = new Vue({ el: '#example', data: { test: 'begin', }, methods: { handleClick: function() { this.test = end; console.log('script') this.$nextTick(function () { console.log('nextTick') }) Promise.resolve().then(function () { console.log('promise') }) } } })
在Vue 2.5+
中,这段代码的输出顺序是script
、promise
、nextTick
,而Vue 2.4
输出script
、nextTick
、promise
。nextTick
执行顺序的差异正好说明了上面的改变。
在Vue 2.4
版本以前使用的MutationObserver
来模拟异步任务。而Vue 2.5
版本以后,由于兼容性弃用了MutationObserver
。
Vue 2.5+
版本使用了MessageChannel
来模拟macroTask
。除了IE
以外,messageChannel
的兼容性还是比较可观的。
const channel = new MessageChannel() const port = channel.port2 channel.port1.onmessage = flushCallbacks macroTimerFunc = () => { port.postMessage(1) }
可见,新建一个MessageChannel
对象,该对象通过port1
来检测信息,port2
发送信息。通过port2
的主动postMessage
来触发port1
的onmessage
事件,进而把回调函数flushCallbacks
作为macroTask
参与事件循环。
为什么要优先MessageChannel
创建macroTask
而不是setTimeout
?
HTML5
中规定setTimeout
的最小时间延迟是4ms
,也就是说理想环境下异步回调最快也是4ms
才能触发。
Vue
使用这么多函数来模拟异步任务,其目的只有一个,就是让回调异步且尽早调用。而MessageChannel
的延迟明显是小于setTimeout
的。对比传送门
看下面的代码:
<template> <p> <p>{{test}}</p> </p> </template>
export default { data () { return { test: 0 }; }, mounted () { for(let i = 0; i < 1000; i++) { this.test++; } } }
现在有这样的一种情况,mounted
的时候test
的值会被++
循环执行1000
次。 每次++
时,都会根据响应式触发setter->Dep->Watcher->update->run。 如果这时候没有异步更新视图,那么每次<code>++
都会直接操作DOM
更新视图,这是非常消耗性能的。 所以Vue
实现了一个queue
队列,在下一个Tick
(或者是当前Tick
的微任务阶段)的时候会统一执行queue
中Watcher
的run
。同时,拥有相同id
的Watcher
不会被重复加入到该queue
中去,所以不会执行1000
次Watcher
的run
。最终更新视图只会直接将test
对应的DOM
的0
变成1000
。 保证更新视图操作DOM
的动作是在当前栈执行完以后下一个Tick
(或者是当前Tick
的微任务阶段)的时候调用,大大优化了性能。
在操作DOM
节点无效的时候,就要考虑操作的实际DOM
节点是否存在,或者相应的DOM
是否被更新完毕。
比如说,在created
钩子中涉及DOM
节点的操作肯定是无效的,因为此时还没有完成相关DOM
的挂载。解决的方法就是在nextTick
函数中去处理DOM
,这样才能保证DOM
被成功挂载而有效操作。
还有就是在数据变化之后要执行某个操作,而这个操作需要使用随数据改变而改变的DOM
时,这个操作应该放进Vue.nextTick
。
之前在做慕课网音乐webApp
的时候关于播放器内核的开发就涉及到了这个问题。下面我把问题简化:
现在我们要实现一个需求是点击按钮变换audio
标签的src
属性来实现切换歌曲。
<p id="example"> <audio ref="audio" :src="url"></audio> <span ref="url"></span> <button @click="changeUrl">click me</button> </p>
const musicList = [ 'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112003137.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3', 'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112002493.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3', 'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112004168.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3' ]; var vm = new Vue({ el: '#example', data: { index: 0, url: '' }, methods: { changeUrl() { this.index = (this.index + 1) % musicList.length this.url = musicList[this.index]; this.$refs.audio.play(); } } });
毫无悬念,这样肯定是会报错的:
Uncaught (in promise) DOMException: The element has no supported sources.
原因就在于audio.play()
是同步的,而这个时候DOM
更新是异步的,src
属性还没有被更新,结果播放的时候src
属性为空,就报错了。
解决办法就是在play
的操作加上this.$nextTick()
。
this.$nextTick(function() { this.$refs.audio.play(); });
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!