引言
看过多遍 vue.js 文档,但是对一些知识点始终似懂非懂。于是便有了重读 Vue.js 文档的计划。此篇对 Vue 中 nextTick 进行剖析并结合源码,希望对您有所帮助。
聊聊变化侦测
前端三大框架中,React、Angular的变化侦测都有一个共同点,就是都不知道哪些状态 (state) 发生了改变,于是就需要进行比较暴力的比对。React 采用了虚拟 DOM 比对,Angular 采用了脏检查机制。而 Vue在一定程度上知道哪些状态发生了改变,哪些节点依赖了这个状态,从而对这些节点进行更新操作,事实上 Vue1.0 就是这么干的。
Vue1.0 实现的好处是简单、粗暴。但也为此付出了一定的代价。原因是对每个状态进行侦测,每个绑定都会对应一个 watcher实例,来观察状态的变化,粒度太细。对一个大型项目来说,会造成很大的开销。
Vue2.0 既没有采用类似React、Angular一样的粗粒度侦测,也没有继续采用Vue1.0 的细粒度侦测,而是采取了则中方案。引入了虚拟DOM, 每个组件对应一个 watcher实例。即便组件中有多个节点依赖于某个状态,也只会生成单个 watcher 实例观察该状态。当该状态改变时,只能通知到该组件,然后通过虚拟DOM的 diff 算法去比对、更新、渲染。如果每次都同步更新视图,会非常耗性能的,于是异步更新是大势所趋了。
异步更新队列
Vue 在更新 DOM 时是异步执行的,当侦测到状态发生变化,Vue会开启一个状态更新队列,将此事件循环中的所有数据变更缓冲起来,在下一个的事件循环 tick 中,刷新队列并执行去重后的更新操作。即便一个 watcher 被多次触发,也只会被推入队列中一次。
<template>
<div class="example">
{{ count }}
</div>
</template>
<script>
export default {
data () {
return {
count: 0
}
},
mounted () {
this.count++;
this.count++;
this.count++;
},
watch: {
count () {
console.log(this.count); // 只会执行一次,结果为 3
}
}
}
多次改变count值,本应监听到多次变化,可只监听到了最后一个操作的结果。
Vue.js 异步更新策略实现
let timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) { // native Promise 存在
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks); // flushCallbacks 作用是将回调队列清空,遍历并执行每个回调。
};
} else if (!isIE && typeof MutationObserver !== 'undefined' && (isNative(MutationObserver)
|| MutationObserver.toString() === '[object MutationObserverConstructor]')) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} 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 = () => {
setImmediate(flushCallbacks);
};
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
// 以上代码就是在不同环境下,timerFunc 的不同实现
Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。而之所以优先采用microtask,然后macrotask,是因为想让回调异步且尽早调用。setTimeout(fn, 0)最快为4ms,而setImmediate 延迟小于 setTimeout。
nextTick
function nextTick(cb, ctx) { // cb 回调函数,ctx 回调执行上下文,均为可选参数
let _resolve;
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true; // 标记是否已经向任务队列添加了一个任务,这就是多次触发一个watcher,只会被推入更新队列一次的原因
timerFunc();
}
// 如果未提供回调,且存在 Promise,即返回一个 Promise
// 于是支持 this.$nextTick().then(function () {});
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve;
});
}
}
nextTick 的使用情景
当改变一个值时,如 this.message = 'World',虽然 data 里的状态会立刻改变,但该状态的更新并不会立即反映到DOM上。多数情况我们不需要关心这个过程,但是如果想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。此时,我们就可以使用 nextTick(callback),这样回调函数将在 DOM 更新完成后被调用。
<template>
<div class="example" ref="example">
{{ message }}
</div>
</template>
<script>
export default {
data () {
return {
message: 'Hello'
}
},
mounted () {
this.message = 'World';
console.log(this.$refs.example.innerText); // Hello
this.$nextTick(function () {
console.log(this.$refs.example.innerText); // World
});
}
}
改变了message 的值之后,界面上message 值并没有立即发生改变。而是在下一个 tick 才改变。
总结
以上是个人对 nextTick 的一些理解,希望能对大家有所帮助。如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞。您的鼓励,是对笔者的最大支持。