前言
有人说:是为了提高性能,对,根本上也是这么个道理 ;那到底是如何做的呢 ?
其实在vue
中,响应式数据是组件级的,也就是说,每一次的更新都是渲染整个组件,如果是同步的话,根据我们前边理解的响应式数据原理,一旦修改了data属性,便会触发对应的 watcher
,然后调用对应 watcher
下的 update
方法更新视图,那么结果显而易见,太频繁了 !如下代码:
// 省略多余模板语法
data () {
a:1,
b:2,
c:3
}
//如果我们按照同步的逻辑,修改data属性,this.a = 10; this.b = 20; this.c = 30;
//就会调用三次update渲染视图,岂不是很耗性能 ?而且体验也不好。
所以 vue 采用的是异步渲染
接下来,我们来了解一下 ;前边也有讲过响应式数据原理,不了解的童鞋可以回过头去看看Go,这里我就接着数据更新方法update
开始;
src/croe/observer/watcher.js
166 行,这里的更新先不考虑计算属性和同步,我们直接看向queueWatcher
update () {
/* istanbul ignore else */
if (this.lazy) { // 计算属性 依赖的数据发生变化了 会让计算属性的watcher的dirty变成true
this.dirty = true
} else if (this.sync) { // 同步watcher
this.run()
} else {
queueWatcher(this) // 将要更新的 watcher 放入队列
}
}
src/core/observer.scheduler.js
164行,queueWatcher
方法;实现一个watcher
队列 ,每一次的update
都放入到队列中,然后进行统一异步处理 。 看代码:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id // 过滤 watcher,多个data属性依赖同一个watcher ,一个组件只有一个watcher
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher) // 将watcher放到队列中
} else { // 通过对 id 的判断,这里的 id 是自加1,可查看 watcher.js 源码,
// 如果已经刷新了,则赋值当前的id , 如果id超过了,将运行如下代码
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) { // 如果不等了,则进行刷新
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue() // 该方法做了刷新前的 beforUpdate 方法调用,然后 watcher.run()
return
}
nextTick(flushSchedulerQueue) // 在下一次tick中刷新 watcher 队列 (借用nextTick)
//(包含同一watcher的多个data属性),
// 这里的nextTick 就是我们的常用api => this.$nextTick()
}
}
}
好了,通过源码简单的分析,明白为啥 vue
为啥采用异步更新了吧,原因很简单,因为vue
是组件级更新视图,每一次update
都要渲染整个组件,为了提高性能,采用了队列的形式,存储同一个watcher
的所有data属性变化,然后统一调用nextTick
方法进行更新渲染(有且只调用一次)。
问题来了,nextTick
方法是异步的 ,那么它又是如何实现的异步更新呢 ?来看张图
从图来看,调用了 nextTick
之后,将watcher
队列回调函数暂时存入了一个数组callbacks
中,然后才依次调用 timeFun()
方法执行,而真正让watcher
异步的关键就在这儿,我们通过代码来看一下:
首先进入 nextTick
函数 src/core/util/next-tick.js
87 行
export function nextTick (cb?: Function, ctx?: Object) {
// flushSchedulerQueue 会使用 nextTick 保证当前视图渲染完成
let _resolve
callbacks.push(() => { // 暂存 watcher 队列
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) { // 状态改变后,调用 timerFun() 方法
pending = true
timerFunc() // 重点,重点,重点! 我们进去看一下
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
timerFunc
方法 src/core/util/next-tick.js
33 行 ,看如下代码,会Js的童鞋应该就可以看出来是什么东西;它对当前的环境进行了判断,如果支持promise
就用 promise
依次往下: MutationObserver , setImmediate , setTimeout
这四个分别都是异步解决方案,除了 setTimeout
是宏观任务以外,其它三个都是微观任务;
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks) // then 里边执行 flushCallbacks
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
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)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks) // setImmediate 回调里边
}
} else {
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0) // setTimeout 回调里边
}
}
总结
nextTick 方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用 nextTick 会将方法存入
队列中,通过这个异步方法清空当前队列。 所以这个 nextTick 方法就是异步方法 。
而我们平常使用的api :vue.nextTick()
也是如此 .