vue openlayer单击地图事件循环多次执行_Vue中DOM的异步更新策略以及nextTick机制

本篇文章主要是对Vue中的DOM异步更新策略和nextTick机制的解析,需要读者有一定的Vue使用经验并且熟悉掌握JavaScript事件循环模型。

引入:DOM的异步更新

13fa1165c9ffa2a6f219417040d98444.png

打印的结果是begin, 而不是我们设置的end。这个结果足以说明Vue中DOM的更新并非同步。

在Vue官方文档中是这样说明的:

可能你还没有注意到,Vue异步执行DOM更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作上非常重要。然后,在下一个的事件循环“tick”中,Vue刷新队列并执行实际 (已去重的) 工作。

简而言之,就是在一个事件循环中发生的所有数据改变都会在下一个事件循环的Tick中来触发视图更新,这也是一个“批处理”的过程。(注意下一个事件循环的Tick有可能是在当前的Tick微任务执行阶段执行,也可能是在下一个Tick执行,主要取决于nextTick函数到底是使用Promise/MutationObserver还是setTimeout)

Watcher队列

在Watcher的源码中,我们发现watcher的update其实是异步的。(注:sync属性默认为false,也就是异步)

5cf91d0c8aa43f3c15ac4f3011964281.png

queueWatcher(this)函数的代码如下:

30683c67b8a54cff243bc92107280146.png

这段源码有几个需要注意的地方:

1、首先需要知道的是watcher执行update的时候,默认情况下肯定是异步的,它会做以下的两件事:

  • 判断has数组中是否有这个watcher的id
  • 如果有的话是不需要把watcher加入queue中的,否则不做任何处理。

2、这里面的nextTick(flushSchedulerQueue)中,flushScheduleQueue函数的作用主要是执行视图更新的操作,它会把queue中所有的watcher取出来并执行相应的视图更新。

3、核心其实是nextTick函数了,下面我们具体看一下nextTick到底有什么用。

nextTick

nextTick函数其实做了两件事情,一是生成一个timerFunc,把回调作为microTask或macroTask参与到事件循环中来。二是把回调函数放入一个callbacks队列,等待适当的时机执行。(这个时机和timerFunc不同的实现有关)

首先我们先来看它是怎么生成一个timerFunc把回调作为microTask或macroTask的。

0362cca0d33ca551588fddae666d2d08.png

值得注意的是,它会按照Promise、MutationObserver、setTimeout优先级去调用传入的回调函数。前两者会生成一个microTask任务,而后者会生成一个macroTask。(微任务和宏任务)

之所以会设置这样的优先级,主要是考虑到浏览器之间的兼容性(IE没有内置Promise)。另外,设置Promise最优先是因为Promise.resolve().then回调函数属于一个微任务,浏览器在一个Tick中执行完macroTask后会清空当前Tick所有的microTask再进行UI渲染,把DOM更新的操作放在Tick执行microTask的阶段来完成,相比使用setTimeout生成的一个macroTask会少一次UI的渲染。

而nextTickHandler函数,其实才是我们真正要执行的函数。

ed5ada5e4e4f6255efd255c5a250cffa.png

这里的callbacks变量供nextTickHandler消费。而前面我们所说的nextTick函数第二点功能中“等待适当的时机执行”,其实就是因为timerFunc的实现方式有差异,如果是PromiseMutationObserver则nextTickHandler回调是一个microTask,它会在当前Tick的末尾来执行。如果是setTiemout则nextTickHandler回调是一个macroTask,它会在下一个Tick来执行。

还有就是callbacks中的成员是如何被push进来的?从源码中我们可以知道,nextTick是一个自执行的函数,一旦执行是return了一个queueNextTick,所以我们在调用nextTick其实就是在调用queueNextTick这个函数。它的源代码如下:

3657716678506d8b91b370f4a6aa0c23.png

可以看到,一旦调用nextTick函数时候,传入的function就会被存放到callbacks闭包中,然后这个callbacks由nextTickHandler消费,而nextTickHandler的执行时间又是由timerFunc来决定。

我们再回来看Watcher中的一段代码:

30683c67b8a54cff243bc92107280146.png

这里面的nextTick(flushSchedulerQueue)中的flushSchedulerQueue函数其实就是watcher的视图更新。每次调用的时候会把它push到callbacks中来异步执行。

另外,关于waiting变量,这是很重要的一个标志位,它保证flushSchedulerQueue回调只允许被置入callbacks一次。

所以,也就是说DOM确实是异步更新,但是具体是在下一个Tick更新还是在当前Tick执行microTask的时候更新,具体要看nextTcik的实现方式,也就是具体跑的是Promise/MutationObserver还是setTimeout。

这里面使用Promise和setTimeout来执行异步任务的方式都很好理解,比较巧妙的地方是利用MutationObserver执行异步任务。MutationObserver是H5的新特性,它能够监听指定范围内的DOM变化并执行其回调,它的回调会被当作microTask来执行,具体参考MDN,

d917b2fa8b709d8411d2b69d525bf3cd.png

可以看到,通过借用MutationObserver来创建一个microTask。nextTickHandler作为回调传入MutationObserver中。

这里面创建了一个textNode作为观测的对象,当timerFunc执行的时候,textNode.data会发生改变,便会触发MutatinObservers的回调函数,而这个函数才是我们真正要执行的任务,它是一个microTask。

注:2.5+版本的Vue对nextTick进行了修改,具体参考下面“版本升级”一节。

关于Vue暴露的全局nextTick

继续来看下面的这段代码:

bb4e062c871579d98fe09c52f30ed916.png

在Chrome下,这段代码执行的顺序的1、2、promise、3。

可能有同学会以为这是1、promise、2、3,其实是忽略了一个标志位pending。

我们回到nextTick函数return的queueNextTick可以发现:

3657716678506d8b91b370f4a6aa0c23.png

这里面通过对pending的判断来检测是否已经有timerFunc这个函数在事件循环的任务队列等待被执行。如果存在的话,那么是不会再重复执行的。

最后异步执行nextTickHandler时又会把pending置为false。

ed5ada5e4e4f6255efd255c5a250cffa.png

所以回到我们的例子, 代码中,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来执行它。

microTask与macroTask

在代码中,有这么一段注释:

62a916ef32c57c60ab9ff00fef4f9a7e.png

其大概的意思就是:在Vue 2.4之前的版本中,nextTick几乎都是基于microTask实现的(具体可以看文章nextTick一节),但是由于microTask的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,就会导致一些诡异的问题;但是如果全部都改成macroTask,对一些有重绘和动画的场景也会有性能的影响。所以最终nextTick采取的策略是默认走microTask,对于一些DOM的交互事件,如v-on绑定的事件回调处理函数的处理,会强制走macroTask。

具体做法就是,在Vue执行绑定的DOM事件时,默认会给回调的handler函数调用withMacroTask方法做一层包装,它保证整个回调函数的执行过程中,遇到数据状态的改变,这些改变而导致的视图更新(DOM更新)的任务都会被推到macroTask。

b2b5881998ac10129b147f1e4e7ecf7c.png

而对于macroTask的执行,Vue优先检测是否支持原生setImmediate(高版本IE和Edge支持),不支持的话再去检测是否支持原生MessageChannel,如果还不支持的话为setTimeout(fn, 0)。

最后,写一段demo来测试一下:

13848eba7488b3940c31b92041ce1b17.png

在Vue 2.5+中,这段代码的输出顺序是script、promise、nextTick,而Vue 2.4输出script、nextTick、promise。nextTick执行顺序的差异正好说明了上面的改变。

MessageChannel

在Vue 2.4版本以前使用的MutationObserver来模拟异步任务。而Vue 2.5版本以后,由于兼容性弃用了MutationObserver。

Vue 2.5+版本使用了MessageChannel来模拟macroTask。除了IE以外,messageChannel的兼容性还是比较可观的。

8957efe34e2e597833c7dbdb48f5b83b.png

可见,新建一个MessageChannel对象,该对象通过port1来检测信息,port2发送信息。通过port2的主动postMessage来触发port1的onmessage事件,进而把回调函数flushCallbacks作为macroTask参与事件循环。

MessageChannel VS setTimeout

为什么要优先MessageChannel创建macroTask而不是setTimeout?

HTML5中规定setTimeout的最小时间延迟是4ms,也就是说理想环境下异步回调最快也是4ms才能触发。

Vue使用这么多函数来模拟异步任务,其目的只有一个,就是让回调异步且尽早调用。而MessageChannel的延迟明显是小于setTimeout的。(这里有一个比对的测试https://stackoverflow.com/questions/18826570/settimeout0-vs-window-postmessage-vs-messageport-postmessage)

为什么要异步更新视图

看下面的代码:

9cc211e4d2bf38e341da2b24a18547e3.png

现在有这样的一种情况,mounted的时候test的值会被++循环执行1000次。 每次++时,都会根据响应式触发setter->Dep->Watcher->update->run。 如果这时候没有异步更新视图,那么每次++都会直接操作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属性来实现切换歌曲。

e75ae55e0f74942488d3d4225ee506be.png

毫无悬念,这样肯定是会报错的:

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();});

参考链接:

https://funteas.com/topic/5a8dc7c8f7f37aa60a177bb7

https://github.com/youngwind/…

https://github.com/answershut…

https://juejin.im/post/5a1af8…

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值