浅析 vue 异步更新策略和 nextTick 源码
准备工作
去 github 上把 vue2 的仓库 clone 下来。下载依赖,给 dev 脚本加上--sourcemap 参数。
然后执行dev脚本。
再然后我们可以打开一个测试案例,然后进行调试。比如 todoMVC。
vue\examples\classic\todomvc\index.html注意要引入 dist 目录下的 vue.js 文件,因为这个文件是加上--sourcemap 参数才有的,可以进行源码映射方便调试。
简单的代码 这是我经过简化 todoMVC 后的代码。。。
<!doctype html>
<section class="todoapp">
{{count}}
</section>
<script src="../../../dist/vue.js"></script>
<script src="app.js"></script>
</body>
</html>同目录下的 app.js
// app Vue instance
var app = new Vue({
data: {
count: 0
},
mounted() {
this.count = 1 // 等下在浏览器调试时,这行代码打上断点。
},
})
// mount
app.$mount('.todoapp')开始调试
当在 mounted 钩子里给 count 赋值为 1 的时候,会触发 set 函数
export function proxy(target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter(val) {
// ===================
this[sourceKey][key] = val
// ===================
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}进入 set 函数,看看里面是怎么做的。
我的理解是 dep 相当于一个响应式数据,watcher 相当于响应式数据在模板上的监视者。一个 dep 对应一个或多个 watcher。当然一个 watcher 也只对应视图模板上的某一个地方。
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean
) {
const dep = new Dep()
// ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend()
}
if (childOb) {
childOb.dep.depend()
if (isArray(value)) {
dependArray(value)
}
}
}
return isRef(value) && !shallow ? value.value : value
},
// 执行这里的 set 函数
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
// 将新值赋值给 dep 的 val 属性上
val = newVal
}
childOb = !shallow && observe(newVal, false, mock)
if (__DEV__) {
// 响应式数据去通知它的 watcher 们更新视图了。在这里打断点进入代码看看。
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
dep.notify()
}
}
})
return dep
}dep.notify 函数内部:
export default class Dep {
static target?: DepTarget | null
id: number
subs: Array<DepTarget>
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub: DepTarget) {
this.subs.push(sub)
}
removeSub(sub: DepTarget) {
remove(this.subs, sub)
}
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this)
if (__DEV__ && info && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
...info
})
}
}
}
// 响应式数据去通知它的 watcher 们更新视图了,但不是立即同步执行更新 (run 方法),而是执行 update 方法
notify(info?: DebuggerEventExtraInfo) {
// subs 是一个 watcher 数组,这里复制一个副本
const subs = this.subs.slice()
if (__DEV__ && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
// for 循环,执行 watcher 的 update 函数,可以简单的理解为更新视图的函数。
for (let i = 0, l = subs.length; i < l; i++) {
if (__DEV__ && info) {
const sub = subs[i]
// 触发将要更新的钩子函数
sub.onTrigger &&
sub.onTrigger({
effect: subs[i],
...info
})
}
// 依次执行数组中 watcher 的 update 方法。
// 重点就在这里了,update 方法内部并不是同步执行的,而是异步执行,那它到底是怎么实现的呢?
subs[i].update()
}
}
}subs[i].update() 执行方法内部:
即会去执行 watcher 实例的 update 方法。
export default class Watcher implements DepTarget {
// ...
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
//
}
get() {
// ...
}
// ...
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 会去执行这个方法,把 watcher 放入队列中。传入参数为 watcher 实例 this。
queueWatcher(this)
}
}
run() {
if (this.active) {
// 真正的更新视图
// ...
}
}
// ...
}queueWatcher 方法内部:
可以看到,
接收获取到 watcher 实例,
const queue: Array<Watcher> = []
let waiting = false
// ...
// 这个方法的作用就是将要更新的 watcher 放入定义的 queue 数组中,
// 并且执行一次 nextTick 方法。这应该是异步更新的关键。
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
if (has[id] != null) {
return
}
if (watcher === Dep.target && watcher.noRecurse) {
return
}
has[id] = true
if (!flushing) { // 没有执行 flushSchedulerQueue 方法时
// 往 queue 数组中 push watcher
queue.push(watcher)
} else {
// 动态往数组中添加 watcher
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// wait 默认为 false
if (!waiting) {
// 然后就置为 true 了。
waiting = true
if (__DEV__ && !config.async) {
flushSchedulerQueue()
return
}
// 在这里会执行 nextTick 方法。没错,源码内部也使用了,也给我们暴露可以使用。
// 传入参数是一个名为 flushSchedulerQueue 的函数。
nextTick(flushSchedulerQueue)
}
}
// ==========================
// flushSchedulerQueue 方法定义
// ==========================
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
queue.sort(sortCompareFn)
// ===========================触发 watcher 的 run 方法========================
// 同步 for 顺序循环 queue 数组,执行 watcher 的 run 方法,触发视图更新。
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
if (__DEV__ && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' +
(watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`),
watcher.vm
)
break
}
}
}
// ===========================触发 watcher 的 run 方法 end========================
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
function resetSchedulerState() {
index = queue.length = activatedChildren.length = 0
has = {}
if (__DEV__) {
circular = {}
}
waiting = flushing = false
}在 nextTick 所在的文件,会有一个数组来存放传入 nextTick 的函数入参。
nextTick 的作用就是把参数 push 进名为 callbacks 的数组中,并且执行一次 timeFunc 方法。
而 timeFunc 的作用就是使用 Promise.then 来异步执行 flushCallbacks 方法,把存入 callbacks 中的数组统统 flush 掉 (执行一遍)
看到这里,是不是感觉和很多个 watcher 放入 queue 数组有点相像?
// 这个文件里会有一个数组,专门保存传入 nextTick 的回调参数
const callbacks: Array<Function> = []
// ...
let pending = false
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
let _resolve
// 把回调参数 push 进 callbacks 数组中。
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e: any) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
// 然后执行在同一文件里的 timerFunc 方法。
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
// 因为有些浏览器不执行 Promise,所以需要做处理。。ie,你知道的。。这里删减一些代码,太长了
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
// 原来 timerFunc 方法就是使用 Promise 的微任务,异步执行 flushCallbacks 方法。
timerFunc = () => {
p.then(flushCallbacks)
}
} else if (
// ...
) {
// ...
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// ...
} else {
// ...
}
// 执行这里
// callbacks 数组中存的是传入 nextTick 参数的函数。
// for 循环执行。
function flushCallbacks() {
pending = false
// 保存一个副本
const copies = callbacks.slice(0)
// 将原本的函数数组清空、初始化。
callbacks.length = 0
// 循环执行副本的每一个函数。
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}总结
当 vue 的响应式数据更新时,会去触发对应的 set 方法,进而执行 dep.notify 方法,通知所有的 watcher 去更新视图。
每个 watcher 会执行 update 方法,将自己的实例 this 作为参数调用 queueWatcher(this) 方法,然后把自己添加到队列数组queue中。
queueWatcher 方法第一次还会调用 nextTick(flushSchedulerQueue) 方法。
flushSchedulerQueue 会for 循环依次执行 queue 中保存的 watcher 的 run 方发。
而 nextTick 方法也是将传入的函数参数添加到一个数组callbacks中。
并且第一次还会执行 timeFunc 方法,在兼容的情况下也就是执行 Promise.then(flushCallbacks) 方法。
flushCallbacks() 会for 循环依次执行 callbacks 中保存的函数。