异步更新队列指的是当状态发生变化时,Vue异步执行DOM更新。我们来看个例子
当我们点击Next按钮后,并不能播放下一首音乐
原因就在于audio.play()是同步的,而这个时候DOM更新是异步的,src属性还没有被更新,结果播放的时候src属性为空,就报错了。解决办法就是在play的操作加上this.$nextTick()。
在Vue的官方文档中有这样的说明:
可能你还没有注意到,Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
之所以要采用异步方式更新DOM,是为了避免每次数据发生变化,就马上去重新渲染DOM,数据的状态可能会发生多次,只要计算最后一次状态的变化,这样可以大大减少性能的损耗。
Event Loop
Vue官方文档中提到在事件循环中执行DOM的刷新。我们先来了解下浏览器Event Loop机制。
浏览器Event Loop
javascript是一门单线程语言,JS的所有同步代码都在主线程(执行栈)中执行,当执行一个函数调用时,会创建一个新的执行环境并压到栈中开始执行函数中的代码,当函数中的代码执行完毕后将执行环境从栈中弹出,当栈空了,也就代表执行完毕。在主线程之外,还有一个任务队列(异步),事实上异步队列也分两种类型:微任务、宏任务。
浏览器Event Loop过程如图所示:
属于微任务(microtask)的事件有以下几种:
属于宏任务(macrotask)的事件有以下几种:
看下面的例子帮助我们理解这个过程
|
|
这是在浏览器中运行的结果。Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,所以js代码还能在Node中运行,此时Event Loop机制与浏览器略有不同。在这里我们暂时不探讨Node环境的机制。
Vue异步更新队列
vue异步更新队列事件循环的tick有可能是在当前的Tick微任务执行阶段执行,也可能是在下一个Tick执行,主要取决于nextTick函数到底是使用Promise/MutationObserver(现废弃采用MessageChannel)还是setTimeout。
在Vue 2.4之前的版本中,nextTick几乎都是基于microTask实现的,但是由于microTask的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,就会导致一些诡异的问题;但是如果全部都改成macroTask,对一些有重绘和动画的场景也会有性能的影响。所以最终nextTick采取的策略是默认走microTask,对于一些DOM的交互事件,如v-on绑定的事件回调处理函数的处理,会强制走macroTask。
应用场景
在操作DOM节点无效的时候,就要考虑操作的实际DOM节点是否存在,或者相应的DOM是否被更新完毕。比如说,在created钩子中涉及DOM节点的操作肯定是无效的,因为此时还没有完成相关DOM的挂载。解决的方法就是在nextTick函数中去处理DOM,这样才能保证DOM被成功挂载而有效操作。还有就是在数据变化之后要执行某个操作,而这个操作需要使用随数据改变而改变的DOM时,这个操作应该放进Vue.nextTick。