引言
看过多遍 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 的一些理解,希望能对大家有所帮助。如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞。您的鼓励,是对笔者的最大支持。