vue nextTick深入理解-vue性能优化、DOM更新时机、事件循环机制
一、定义[nextTick、事件循环]
nextTick 的由来:
由于 VUE 的数据驱动视图更新,是异步的,即修改数据的当下,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
nextTick 的触发时机:
在同一事件循环中的数据变化后,DOM 完成更新,立即执行 nextTick(callback) 内的回调。
应用场景:
需要在视图更新之后,基于新的视图进行操作。
以上出现了事件循环的概念,其涉及到 JS 的运行机制,包括主线程的执行栈、异步队列、异步 API、事件循环的协作,此处不展开之后再总结。大致理解:主线程完成同步环境执行,查询任务队列,提取队首的任务,放入主线程中执行;执行完毕,再重复该操作,该过程称为事件循环。而主线程的每次读取任务队列操作,是一个事件循环的开始。异步 callback 不可能处在同一事件循环中。
简单总结事件循环:
同步代码执行 -> 查找异步队列,推入执行栈,执行 callback1[事件循环 1] -> 查找异步队列,推入执行栈,执行 callback2[事件循环 2]...
即每个异步 callback,最终都会形成自己独立的一个事件循环。
结合 nextTick 的由来,可以推出每个事件循环中,nextTick 触发的时机:
同一事件循环中的代码执行完毕 -> DOM 更新 -> nextTick callback 触发
tips:本文的任务队列、消息队列、异步队列指同一个东西,均指 macrotask queue。
二、实例理解 nextTick 的使用,并给出在页面渲染上的优化巧用
(tips:代码的正确阅读方式:看 template 组成、跳过 script 代码、看代码后面的用例设计、看之后的代码分析、同时结合回头结合 script 代码理解)
<template> <div> <ul> <li v-for="item in list1">{{item}}</li> </ul> <ul> <li v-for="item in list2">{{item}}</li> </ul> <ol> <li v-for="item in list3">{{item}}</li> </ol> <ol> <li v-for="item in list4">{{item}}</li> </ol> <ol> <li v-for="item in list5">{{item}}</li> </ol> </div> </template> <script type="text/javascript"> export default {data() { return {list1: [], list2: [], list3: [], list4: [], list5: []} }, created() { this.composeList12() this.composeList34() this.composeList5() this.$nextTick(function() { // DOM 更新了 console.log('finished test' + new Date().toString()) console.log(document.querySelectorAll('li').length) }) }, methods: {composeList12() { let me = this let count = 10000<span style="color: rgba(0, 0, 255, 1)">for</span> (let i = 0; i < count; i++<span style="color: rgba(0, 0, 0, 1)">) { Vue.set(me.list1, i, </span>'I am a 测试信息~~啦啦啦' +<span style="color: rgba(0, 0, 0, 1)"> i) } <span style="background-color: rgba(227, 227, 227, 1)">console.log(</span></span><span style="background-color: rgba(227, 227, 227, 1)">'finished list1 ' + <span style="color: rgba(0, 0, 255, 1)">new</span></span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)"> Date().toString()</span>) </span><span style="color: rgba(0, 0, 255, 1)">for</span> (let i = 0; i < count; i++<span style="color: rgba(0, 0, 0, 1)">) { Vue.set(me.list2, i, </span>'I am a 测试信息~~啦啦啦' +<span style="color: rgba(0, 0, 0, 1)"> i) } <span style="background-color: rgba(227, 227, 227, 1)">console.log(</span></span><span style="background-color: rgba(227, 227, 227, 1)">'finished list2 ' + <span style="color: rgba(0, 0, 255, 1)">new</span></span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)"> Date().toString()</span>) </span><span style="color: rgba(0, 0, 255, 1)">this</span>.$nextTick(<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">() { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> DOM 更新了</span> <span style="background-color: rgba(227, 227, 227, 1)">console.log('finished tick1&2 ' + <span style="color: rgba(0, 0, 255, 1)">new</span></span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)"> Date().toString()</span>) <span style="background-color: rgba(227, 227, 227, 1)">console.log(document.querySelectorAll(</span></span><span style="background-color: rgba(227, 227, 227, 1)">'li'</span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)">).length</span>) }) }, composeList34() { let me </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)"> let count </span>= 10000 <span style="color: rgba(0, 0, 255, 1)">for</span> (let i = 0; i < count; i++<span style="color: rgba(0, 0, 0, 1)">) { Vue.set(me.list3, i, </span>'I am a 测试信息~~啦啦啦' +<span style="color: rgba(0, 0, 0, 1)"> i) } <span style="background-color: rgba(227, 227, 227, 1)">console.log(</span></span><span style="background-color: rgba(227, 227, 227, 1)">'finished list3 ' + <span style="color: rgba(0, 0, 255, 1)">new</span></span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)"> Date().toString()</span>) </span><span style="color: rgba(0, 0, 255, 1)">this</span>.$nextTick(<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">() { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> DOM 更新了</span> <span style="background-color: rgba(227, 227, 227, 1)">console.log('finished tick3 ' + <span style="color: rgba(0, 0, 255, 1)">new</span></span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)"> Date().toString()</span>) <span style="background-color: rgba(227, 227, 227, 1)">console.log(document.querySelectorAll(</span></span><span style="background-color: rgba(227, 227, 227, 1)">'li'</span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)">).length</span>) }) setTimeout(me.setTimeout1, </span>0<span style="color: rgba(0, 0, 0, 1)">) }, setTimeout1() { let me </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)"> let count </span>= 10000 <span style="color: rgba(0, 0, 255, 1)">for</span> (let i = 0; i < count; i++<span style="color: rgba(0, 0, 0, 1)">) { Vue.set(me.list4, i, </span>'I am a 测试信息~~啦啦啦' +<span style="color: rgba(0, 0, 0, 1)"> i) } <span style="background-color: rgba(227, 227, 227, 1)">console.log(</span></span><span style="background-color: rgba(227, 227, 227, 1)">'finished list4 ' + <span style="color: rgba(0, 0, 255, 1)">new</span></span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)"> Date().toString()</span>) me.$nextTick(</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">() { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> DOM 更新了</span> <span style="background-color: rgba(227, 227, 227, 1)">console.log('finished tick4 ' + <span style="color: rgba(0, 0, 255, 1)">new</span></span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)"> Date().toString()</span>) <span style="background-color: rgba(227, 227, 227, 1)">console.log(document.querySelectorAll(</span></span><span style="background-color: rgba(227, 227, 227, 1)">'li'</span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)">).length</span>) }) }, composeList5() { let me </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)"> let count </span>= 10000 <span style="color: rgba(0, 0, 255, 1)">this</span>.$nextTick(<span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">() { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> DOM 更新了</span> <span style="background-color: rgba(227, 227, 227, 1)">console.log('finished tick5-1 ' + <span style="color: rgba(0, 0, 255, 1)">new</span></span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)"> Date().toString()</span>) <span style="background-color: rgba(227, 227, 227, 1)">console.log(document.querySelectorAll(</span></span><span style="background-color: rgba(227, 227, 227, 1)">'li'</span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)">).length</span>) }) setTimeout(me.setTimeout2, </span>0<span style="color: rgba(0, 0, 0, 1)">) }, setTimeout2() { let me </span>= <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)"> let count </span>= 10000 <span style="color: rgba(0, 0, 255, 1)">for</span> (let i = 0; i < count; i++<span style="color: rgba(0, 0, 0, 1)">) { Vue.set(me.list5, i, </span>'I am a 测试信息~~啦啦啦' +<span style="color: rgba(0, 0, 0, 1)"> i) } <span style="background-color: rgba(227, 227, 227, 1)">console.log(</span></span><span style="background-color: rgba(227, 227, 227, 1)">'finished list5 ' + <span style="color: rgba(0, 0, 255, 1)">new</span></span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)"> Date().toString()</span>) me.$nextTick(</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">() { </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> DOM 更新了</span> <span style="background-color: rgba(227, 227, 227, 1)">console.log('finished tick5 ' + <span style="color: rgba(0, 0, 255, 1)">new</span></span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)"> Date().toString()</span>) <span style="background-color: rgba(227, 227, 227, 1)">console.log(document.querySelectorAll(</span></span><span style="background-color: rgba(227, 227, 227, 1)">'li'</span><span style="color: rgba(0, 0, 0, 1)"><span style="background-color: rgba(227, 227, 227, 1)">).length</span>) }) } }
}
</script>
2.1、用例设计
用例 1:通过 list1、2、3 验证,处在同步代码中的 DOM 更新情况及 nextTick 的触发时机;
用例 2:通过 list3、list4 验证,同步代码及异步代码中 Dom 更新及 nextTick 触发的区别;
用例 3:通过 list4、list5 对比验证,多个异步代码中 nextTick 触发的区别;
用例 4:通过在视图更新后获取 DOM 中 <li> 的数量,判断 nextTick 序列渲染的时间点。
2.2、代码分析
函数执行步骤:
事件循环 1:
step1: this.composeList12() -> update list1, update list2 -> 绑定 tick’1&2’
step2: this.composeList34() -> update list3, 设置异步 1setTimeout1 -> 绑定 tick’3’
step3: this.composeList5() -> 绑定 tick’5-1’ -> 设置异步 2setTimeout2
step4: 绑定 tick’test’
事件循环 2:
将 setTimeout1 的 callback 推入执行栈 -> update list4 -> 绑定 tick’4’
事件循环 3:
将 setTimeout2 的 callback 推入执行栈 -> update list5 -> 绑定 tick’5’
2.3、推断输出消息
由于同一事件循环中的 tick 按执行顺序,因此消息输出为即:
[同步环境]update list1 -> update list2 -> update list3 -> tick‘1&2’ -> tick‘3’ -> tick’5-1’ -> tick’test'
[事件循环 1]->update list4 -> tick’4’
[事件循环 2] ->update list5 -> tick’5’
2.4、实际运行结果如下图
该 demo 中,设置了 5 个 size 为 10000 的数组,从而能从时间及消息输出两个维度来了解 nextTick 的执行情况。另外,额外增加了一个参数,即更新后的视图中 <li> 的数量,从这个数量,可以考察出同一事件循环中的 nextTick 执行情况。由运行结果图可以看出实际的输出与推导的输出结果相符合。
2.5、总结
从用例 1 得出:
a、在同一事件循环中,只有所有的数据更新完毕,才会调用 nextTick;
b、仅在同步执行环境数据完全更新完毕,DOM 才开始渲染,页面才开始展现;
c、在同一事件循环中,如果存在多个 nextTick,将会按最初的执行顺序进行调用;
从用例 1+ 用例 4 得出:
d、从同步执行环境中的四个 tick 对应的‘li’数量均为 30000 可看出,同一事件循环中,nextTick 所在的视图是相同的;
从用例 2 得出:
e、只有同步环境执行完毕,DOM 渲染完毕之后,才会处理异步 callback
从用例 3 得出:
f、每个异步 callback 最后都会处在一个独立的事件循环中,对应自己独立的 nextTick;
从用例 1 结论中可得出:
g、这个事件环境中的数据变化完成,在进行渲染[视图更新],可以避免 DOM 的频繁变动,从而避免了因此带来的浏览器卡顿,大幅度提升性能;
从 b 可以得出:
h、在首屏渲染、用户交互过程中,要巧用同步环境及异步环境;首屏展现的内容,尽量保证在同步环境中完成;其他内容,拆分到异步中,从而保证性能、体验。
tips:
1、可产生异步 callback 的有:promise(microtask queue)、setTimeout、MutationObserver、DOM 事件、Ajax 等;
2、 vue DOM 的视图更新实现,,使用到了 ES6 的 Promise 及 HTML5 的MutationObserver,当环境不支持时,使用 setTimeout(fn, 0) 替代。上述的三种方法,均为异步 API。其中 MutationObserver 类似事件,又有所区别;事件是同步触发,其为异步触发,即 DOM 发生变化之后,不会立刻触发,等当前所有的 DOM 操作都结束后触发。关于异步 API、事件循环将在以后补充。
事件循环、任务队列详解:http://www.cnblogs.com/hity-tt/p/6733062.html