- 7.14 nextTick
- 7.14.1 事件循环机制
- 7.14.2 基本实现
- 7.14.3 使用场景
7.14 nextTick
在上一节的内容中,我们说到数据修改时会触发setter方法进行依赖的派发更新,而更新时会将每个watcher推到队列中,等待下一个tick到来时再执行DOM的渲染更新操作。这个就是异步更新的过程。为了说明异步更新的概念,需要牵扯到浏览器的事件循环机制和最优的渲染时机问题。由于这不是文章的主线,我只用简单的语言概述。
7.14.1 事件循环机制
- 完整的事件循环机制需要了解两种异步队列:
macro-task和micro-task macro-task常见的有setTimeout, setInterval, setImmediate, script脚本, I/O操作,UI渲染micro-task常见的有promise, process.nextTick, MutationObserver等- 完整事件循环流程为:4.1
micro-task空,macro-task队列只有script脚本,推出macro-task的script任务执行,脚本执行期间产生的macro-task,micro-task推到对应的队列中4.2 执行全部micro-task里的微任务事件4.3 执行DOM操作,渲染更新页面4.4 执行web worker等相关任务4.5 循环,取出macro-task中一个宏任务事件执行,重复4的操作。
从上面的流程中我们可以发现,最好的渲染过程发生在微任务队列的执行过程中,此时他离页面渲染过程最近,因此我们可以借助微任务队列来实现异步更新,它可以让复杂批量的运算操作运行在JS层面,而视图的渲染只关心最终的结果,这大大降低了性能的损耗。
举一个这一做法好处的例子: 由于Vue是数据驱动视图更新渲染,如果我们在一个操作中重复对一个响应式数据进行计算,例如 在一个循环中执行this.num ++一千次,由于响应式系统的存在,数据变化触发setter,setter触发依赖派发更新,更新调用run进行视图的重新渲染。这一次循环,视图渲染要执行一千次,很明显这是很浪费性能的,我们只需要关注最后第一千次在界面上更新的结果而已。所以利用异步更新显得格外重要。
7.14.2 基本实现
Vue用一个queue收集依赖的执行,在下次微任务执行的时候统一执行queue中Watcher的run操作,与此同时,相同id的watcher不会重复添加到queue中,因此也不会重复执行多次的视图渲染。我们看nextTick的实现。
// 原型上定义的方法Vue.prototype.$nextTick = function (fn) {return nextTick(fn, this)};// 构造函数上定义的方法Vue.nextTick = nextTick;// 实际的定义var callbacks = [];function nextTick (cb, ctx) {var _resolve;// callbacks是维护微任务的数组。callbacks.push(function () {if (cb) {try {cb.call(ctx);} catch (e) {handleError(e, ctx, 'nextTick');}} else if (_resolve) {_resolve(ctx);}});if (!pending) {pending = true;// 将维护的队列推到微任务队列中维护timerFunc();}// nextTick没有传递参数,且浏览器支持Promise,则返回一个promise对象if (!cb && typeof Promise !== 'undefined') {return new Promise(function (resolve) {_resolve = resolve;})}}
nextTick定义为一个函数,使用方式为Vue.nextTick( [callback, context] ),当callback经过nextTick封装后,callback会在下一个tick中执行调用。从实现上,callbacks是一个维护了需要在下一个tick中执行的任务的队列,它的每个元素都是需要执行的函数。pending是判断是否在等待执行微任务队列的标志。而timerFunc是真正将任务队列推到微任务队列中的函数。我们看timerFunc的实现。
1.如果浏览器执行Promise,那么默认以Promsie将执行过程推到微任务队列中。
var timerFunc;if (typeof Promise !== 'undefined' && isNative(Promise)) {var p = Promise.resolve();timerFunc = function () {p.then(flushCallbacks);// 手机端的兼容代码if (isIOS) { setTimeout(noop); }};// 使用微任务队列的标志isUsingMicroTask = true;}
flushCallbacks是异步更新的函数,他会取出callbacks数组的每一个任务,执行任务,具体定义如下:
function flushCallbacks () {pending = false;var copies = callbacks.slice(0);// 取出callbacks数组的每一个任务,执行任务callbacks.length = 0;for (var i = 0; i < copies.length; i++) {copies[i]();}}
2.不支持promise,支持MutataionObserver
else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) ||// PhantomJS and iOS 7.xMutationObserver.toString() === '[object MutationObserverConstructor]')) {var counter = 1;var observer = new MutationObserver(flushCallbacks);var textNode = document.createTextNode(String(counter));observer.observe(textNode, {characterData: true});timerFunc = function () {counter = (counter + 1) % 2;textNode.data = String(counter);};isUsingMicroTask = true;}
3.如果不支持微任务方法,则会使用宏任务方法,setImmediate会先被使用
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {// Fallback to setImmediate.// Techinically it leverages the (macro) task queue,// but it is still a better choice than setTimeout.timerFunc = function () {setImmediate(flushCallbacks);};}
4.所有方法都不适合,会使用宏任务方法中的setTimeout
else {timerFunc = function () {setTimeout(flushCallbacks, 0);};}
当nextTick不传递任何参数时,可以作为一个promise用,例如:
nextTick().then(() => {})
7.14.3 使用场景
说了这么多原理性的东西,回过头来看看nextTick的使用场景,由于异步更新的原理,我们在某一时间改变的数据并不会触发视图的更新,而是需要等下一个tick到来时才会更新视图,下面是一个典型场景:
<input v-if="show" type="text" ref="myInput">// jsdata() {show: false},mounted() {this.show = true;this.$refs.myInput.focus();// 报错}
数据改变时,视图并不会同时改变,因此需要使用nextTick
mounted() {this.show = true;this.$nextTick(function() {this.$refs.myInput.focus();// 正常})}
