vue收集依赖机制
- 通过Object.definedproperty来设置观察属性的setter和getter
- 通过getter收集依赖,通过setter触发依赖更新
与收集依赖相关的三个类
- Observer,观察对象,其变化时会通知观察者
- Watcher,观察者,观察观察对象,依赖收集对象,观察对象变化时会被通知,同时调用其对象上的更新方法
- Dep,专门用于管理依赖的对象,其上面有添加删除以及通知依赖的方法,并保存一个依赖队列
Observer
- 以vue中的data为例,Object.definedproperty发生在new Vue => Vue => initState => initData => observe => new Observer
- 简略源码如下:
class Observer {
constructor (value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
- def函数即在实例化Observer时,将他绑定到入参value上的__ob__上面,这一步是为了后面可以直接通过__ob__访问到对应的
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
- 由于数组和对象不同,数组无法通过Object.definedproperty设置getter和setter,所以数组要通过别的方法,即重写原型方法的方式去拦截对数组改动的方法,实现响应,数组的处理放到后面讨论
- walk方法,用于对对象中的每个属性进行处理,defineReactive方法中将会使用Object.definedproperty,将value变成响应式的
function defineReactive (obj, key, val, shallow) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
- 如果当前key对应的属性是不可配置的,则退出
- shallow用于判断是否将子属性也转换成响应式的,一般都是false
- 在设置key对应属性的getter中,如果当前Dep类下面target属性有值,则调用实例化后的dep上的depend方法,他会把target的值保留下来,同时如果子属性也是对象,且已经被Observer化后,由于Observer时会在其实例上保留dep属性,故这里可以直接调用childOb.dep.depend(),这样一来假设当前key对应的属性是一个obj,其子属性也会同时被观察,即子属性发生变化时也会通知依赖更新;
- 而如果当前key对应的值是一个数组,为了也能实现深度监听,调用了dependArray方法:
function dependArray (value: Array) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
- 这里dependArray会递归遍历每一个元素,由于之前已经observe了这个val,所以这里的元素理论上已经有__ob__属性,可以通过他调用dep的depend进行依赖;
- 回到Observer中,在设置key对应属性的stter时,判断了一遍其新旧值是否不同,不同的时候才通知依赖更新,减少无谓更新;另外将新设的值用ovserve方法处理,这样假设新设的值是一个对象,其子属性也能被观察,触发依赖更新;最后调用dep的notify方法通知依赖更新;
- 数组的依赖放在后面讨论
- 另外,只有getter没有setter的时候收集依赖无效,见下面代码
const data = {}
let _a = 1
Object.defineProperty(data, 'getterProp', {
enumerable: true,
configurable: true,
get: () => {
return {
a: _a
}
}
})
- 由于data只有getter,其被defineReactive处理后,getter出来的永远是{a: _a},而setter中假设没有
if (getter && !setter) return
判断,则通过data.getterProp = { a: 2 }
将会触发依赖更新,同时将{a: 2}通过observe处理,但由于setter中并不会改变_a的值,所以getter出来的依旧是{a: 1},即依赖更新实际并没有意义,同时新产生的{a: 2}虽然被观察了,但永远不会被依赖,所以加了仅有getter时跳出处理的逻辑判断
Dep
let uid = 0
class Dep {
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
- Dep类中保存着subs数组,这个subs数组其实就是watcher,观察者队列,后面分析watcher会说到;同时通过this.id标记当前Dep实例;
- removeSub用于从数组中移除对应元素,实际通过array.splice进行操作;
- Observer中调用的dep.depend方法实际调用的是Dep.target上面的addDep方法,并将当前实例的上下文作为参数传了进去,这里的Dep.target实际是Watcher实例,将在后面分析watcher时提及;
- notify,就是用来通知所有收集到的依赖,通过遍历subs数组实现,这里也可以看出,subs中的每个元素,即watcher中,都有一个update方法;
Watcher
let uid = 0
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
this.value = this.lazy ? undefined : this.get()
}
// Evaluate the getter, and re-collect dependencies.
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
throw e
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// Evaluate the value of the watcher.
// This only gets called for lazy watchers.
evaluate () {
this.value = this.get()
this.dirty = false
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
- watcher实例化时入参有:vm,组件实例;expOrFn,表达式,即watcher中的表达式,或computed里的getter,或渲染组件的渲染方法;cb,回调函数,在watcher被通知变化,执行update之后执行;options,各种设置;isRenderWatcher,是否是render watcher,即是否是模版产生的watcher,由于模版中也会有数据依赖,故模版也有对应的watcher,在mounteComponent阶段生成,同时isRenderWatcher为true的时候,会把watcher实例挂在vm的_watcher上;
- options中的各种设置有:deep,深度监听,如watch中传入deep为true,则表达式中的对象属性或数组元素都会被监听,而不仅仅是监听其引用变化;sync,同步更新,监听到变化马上变化,否则采取batch update策略(batch update,通过事件循环实现,使用微或宏任务,将update方法放到事件循环末尾执行,之前所有的变化都通过一个队列收集起来);user,标记该watcher是否是用户自定义的,为true时会执行cb,系统自动标记了,无需用户手动设置;before,render watcher时会传进来,会在watcher的更新前调用;lazy,懒监听,系统自动添加在computed中,即只要其依赖的变量没发现变化的话,都不会重新计算该computed值,而是一直使用缓存的值;
- 实例化的最后一步是触发get函数,如果是lazy的话,就不执行,value=undefined
- get函数中一头一尾有两个方法pushTarget和popTarget,其是Dep文件里定义的两个方法:
const targetStack = []
function pushTarget (_target) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
function popTarget () {
Dep.target = targetStack.pop()
}
- 这里即把当前的watcher实例添加到了Dep.target中,其后执行this.getter.call的时候,会读取别的变量,触发之前observe的时候定义的get拦截器,这时依赖收集就被触发了;
- 假设在pushTarget触发的时候,Dep.target已经保存了别的值,先把该值压入stack中,并重置Dep.target,在popTarget再把之前压入stack的watcher拿出来,重新赋给Dep.target,从而保证在同一时间,有且只有一个watcher在Dep.target上
- 如果设置了deep,则会执行traverse方法:
const seenObjects = new Set()
function traverse (val) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val, seen) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
- traverse方法首先进行了一些判断,把无需深度监听的情况排除了,然后根据__ob__判断是否是响应式的数据,如果是则判断是否已经处理过依赖了,如果有就跳出逻辑
- 接着判断是否是数组,数组的话,递归每个元素;不是数组的话,递归每个key,注意这里通过val[i]或val[keys[i]],实际上是调用了响应数据的get方法,也就是已经触发了依赖收集
- 回到Watcher类里,get的最后一步是执行cleanupDeps,这是因为变化过程中,可能有些依赖已经不存在了,那就没必要再去观察这些变量,故用一个newDeps和newDepIds的队列记录最新的依赖集合,然后对比现有的dpes队列,进行一遍清洗;
- dep.removeSub(this)将Observer里的依赖关系删除,this.deps = this.newDeps修改Watcher中的依赖关系
- addDep方法,实际即Observer中定义的get方法中调用的dep.depend最终调用的方法,他会把watcher的实例push进入参(即Observer上的dep)队列中,同时也把observer的dep添加到自己的newdeps队列中,从而双方上面都有双方的实例引用,依赖关系从而建立;
- depend方法,搜了下源码只有在初始化computed里面用到,相关代码如下:
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
- createComputedGetter会用于劫持computed属性的get方法,这里的watcher是computed的watcher,但Dep.target并不是computed的watcher,而是调用了computed属性的watcher,打个比方可能是渲染函数的watcher,如上所说,pushTarget会将watcher压入栈,computed压入栈的时候是调用get的时候,而由于computed属性是懒更新的,new watcher的时候并不会调用get方法,其get被调用是在evaluate方法里,那么按照上面代码逻辑,evaluate调用完毕或直接跳过了evaluate方法,这时候的Dep.target都不是computed的watcher,而是调用他的watcher,所以这里的逻辑是在computed依赖的Observer的dep上面添加调用computed的watcher,这样当依赖属性变化,被comouted依赖的Observer也会马上收到通知,触发notify方法
- Dep的notify最终触发的是Watcher的update方法里面判断了是否懒更新,即lazy值,如果是懒更新,把其dirty标志置为true,意即,这个watcher已经被更新了,需要重新计算,但此时并不会马上计算,也不会用queueWatcher把他保存在待执行队列中;而如果设置了sync同步标志,则马上执行run方法,run方法实际就是调用了一遍get方法,重新获取新值,并在最后调用cb方法;而除此之外的情况都会采用queueWatcher将变化保存在队列中,待后续进行,这也是vue批量更新的原因
- queueWatcher后续再提及,先看evaluate方法,之前有提到,evaluate发生在computed的get方法上,结合update方法来看,也就是computed的依赖发生变化时,调用了update方法,但computed本身并不会马上被更新,而是通过一个dirty标签表明computed的值需要被更新,然后最终的更新是发生在读取computed值的时候,通过判断dirty值,调用了evaluate方法,更新了值,并将dirty重新置为false
- teardown用于解除当前watcher实例的所有依赖关系,首先将自己从vue实例上面删除,然后依次调用其上保存的dep队列中的removeSub,即从其观察的Observer上面dep队列中删除自己
- 回到queueWatcher,由于更新需要消耗性能,且工程中更新通常是大规模且高频地发生,所以需要有批量更新的策略,queueWatche就是用来实现这个目的的,queueWatcher代码如下:
let waiting = false
function queueWatcher (watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
- queueWatcher先判断这一轮更新是否已经记录过当前的watcher,只有未记录过的才需要处理,处理过程首先记录标记当前watcher已经被添加到队列中,然后判断是否正在执行更新,flushing代表目前正在执行更新,如果不是正在执行更新,则将watcher推入queue队列;而如果已经正在执行更新,将watcher插入队列中;index是当前queue执行到的位置,从队列末尾开始向前取出队列中的watcher做对比,如果队列中的watcher ID比当前watcher ID大,则继续往前寻找,知道找到比队列ID大的位置,将当前watcher插入其后,而如果直到遍历到当前队列执行位置都没有找到这个值,则直接把当前watcher插入下一个位置,即处理完队列正在处理的watcher后,就会处理当前watcher
- 这里可能会有疑问,为什么不直接把当前watcher插入下一个要处理的queue位置就好,而要遍历判断watcher.id来判断插入位置,这是因为watcher的生成是有先后顺序的,而被依赖的对象有可能是data中的一个值,也可能是一个视图,如果是后者,其本身也有watcher,也依赖其他可观察对象,而如果这个视图里有一个组件,并接收一些props,则必须保证视图的更新要早于组件,才能保证数据的准确,而watcher的生成顺序是被依赖的先于依赖的生成,所以要通过id的大小判断插入的位置
- 最后执行flushSchedulerQueue前会通过waiting标志来确保一次排队只会触发一次flushSchedulerQueue
- flushSchedulerQueue代码如下:
function flushSchedulerQueue () {
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
}
}
function resetSchedulerState () {
index = queue.length = activatedChildren.length = 0
has = {}
waiting = flushing = false
}
- 这里可以看到flushSchedulerQueue中对队列进行了排序,让watcher按id从小到大排列,所以queueWatcher中针对正在处理flushing的队列插入watcher的逻辑才会成立
- 执行更新前,会调用before方法,对于render watcher,定义的时候,before方法里写的是调用勾子函数beforeupdate
- flush方法会执行执行队列里每一个watcher的run方法,即重新执行了取值操作,render watcher的get是其render方法。从而更新了值或视图
- 最后执行resetSchedulerState,将各种标志复位
- flushSchedulerQueue是作为一个参数传给nexttick的,nexttick的作用就是把回调放到事件循环的末尾去做,从而实现batch update
let pending = false
const callbacks = []
function nextTick (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
timerFunc()
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
- 可以看到nexttick主要是通过将cb推入callback队列,并调用timerFunc来处理的,timerFunc代码如下
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
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)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
- 这一段主要是各种兼容性代码,简单地讲就是优先将callback队列的执行放到微任务队列中等待,不支持微任务时放到宏任务队列中等待,从而保证你的更新是发生在同步任务执行完成之后才执行
数组的依赖收集
- 数组的依赖收集,主要是Observer实例化时做的处理
- 实例化数组Observer的时候,调用了copyAugment(value, arrayMethods, arrayKeys)/protoAugment(value, arrayMethods)方法
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result
})
})
function protoAugment (target, src) {
target.__proto__ = src
}
function copyAugment (target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
- 因为value是数组,这两个方法其实就是改写了数组的原型,原本数组实例的原型指向Array.prototype,现在指向了arrayMethods,且arrayMethods的原型指向Array.prototype
- 同时在arrayMethods上,针对会对数组造成变化的方法,定义了拦截器,当使用这些方法的时候,获取当前实例上的__ob__属性,即Observer实例,调用他们的通知方法,触发更新;
- 注意这里的inserted变量,是针对会对数组进行插值的方法,获取他们插入的变量,调用observeArray,将他们转化为可观察对象
- observeArray不止在插值时调用,拦截了数组原型方法后也有调用,代码如下,只是简单地观察其中的每个元素
observeArray (items: Array) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
- 至此,array的Observer就定义好了,其具备了一项功能,那就是当使用事先拦截的那几个会改变数组的方法时,如splice,会触发其对应的Observer上的通知方法,通知依赖更新
- 触发功能有了,触发的对象是怎么收集的呢,因为无法通过defineProperty来拦截get方法,从而触发Observer的dep.depend,需要另外的机制来处理依赖的收集,这个方法就是defineReactive上面的dependArray方法,因为可监听的数组是定义在data上的,而data是一个对象,会在init阶段被observe处理,处理过程中就会调用到defineReactive方法,在其拦截get方法时,get方法中判断该属性是数组的话,就会触发dependArray方法,里面递归遍历每个元素,如果是响应式的,就调用dep.depend方法,这里就触发了依赖的收集
- this.array或this.array[i]方法都会触发array对应的get方法,然后触发dependArray,最后触发依赖收集