Object.defineProperty get收集依赖
src/core/observer/index.ts
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__) {
//...
} else {
dep.depend()
}
// ...
}
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
// ...
}
})
return dep
}
首先创建一个和key一一对应的dep, 一个key就对应着一个Dep;
下面代码中, Dep.target是全局唯一的watcher,在同一时间只有一个watcher被计算,如果存在这样一个watcher,调用dep.depend;
那么响应式数据的getter如何被触发呢?
watcher.get 触发响应式数据的gettter
代码中要用到响应式数据的地方有很多,如果模板中用到了,那就将模板抽象为一个渲染watcher,如果是用户自己写的watch监听,那就抽象为一个计算watcher;
在依赖收集阶段只收集这个Watcher类的实例,通知也通知它,它再去通知其他地方
src/core/observe/watcher.ts
export default class Watcher implements DepTarget {
vm?: Component | null
expression: string
cb: Function
id: number
deps: Array<Dep>
//本次求值所收集的Dep实例列表, 在每次求值完成之后都会被赋值给deps,然后被清空
newDeps: Array<Dep>
//上次求值所收集的Dep实例Id列表,用来避免两次求值之间的重复收集以及去除废弃的观察者
depIds: SimpleSet
//本次求值所收集的Dep实例Id列表,用来避免本次求值的重复收集,
//在每次求值完成之后都会被赋值给depIds,然后被清空
newDepIds: SimpleSet
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
if ((this.vm = vm) && isRenderWatcher) {
vm._watcher = this
}
this.cb = cb
this.id = ++uid // uid for batching
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = __DEV__ ? expOrFn.toString() : ''
// parse expression for getter
if (isFunction(expOrFn)) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy ? undefined : this.get()
}
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e: any) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
// ...
}
src/core/observe/dep.ts
Dep.target = null
const targetStack: Array<DepTarget | null | undefined> = []
export function pushTarget(target?: DepTarget | null) {
targetStack.push(target)
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
watcher.getter这个函数可能是生成DOM的渲染函数,也可能是watch选项中的回调函数
在开始vue渲染页面的时候,是通过渲染watcher来实现的,实例化渲染Watcher的时候,在构造器函数constructor中会调用this.get(),首先pushTarget(this),这里的this就是渲染watcher,把 Dep.target 赋值为当前的渲染 watcher 并压入targetStack中,执行 value = this.getter.call(vm, vm),暂时记住,这个getter函数执行就会读取模板中的响应式数据,也就会触发响应式数据的getter函数收集依赖
Dep 管理依赖Watcher
src/core/observe/dep.ts
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)
}
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
注意Dep的静态属性target ,全局唯一的依赖watcher,属性subs也是一个watcher数组;
根据上面的分析,Dep.target现在被赋值为渲染watcher, depend方法中,如果dep.target存在,就调用watcher的addDep方法 通过 pushTarget使Dep.target等于目前正在被收集的watcher,同时把watcher压入栈中,popTarget为了把Dep.target还原为上一个watcher,同时让watcher出栈,这样做使的嵌套的数据对象可以收集到准确的watcher
Watcher 被收集的依赖
watcher.addDep 收集依赖watcher
export default class Watcher implements DepTarget {
vm?: Component | null
expression: string
cb: Function
id: number
deps: Array<Dep>
//本次求值所收集的Dep实例列表, 在每次求值完成之后都会被赋值给deps,然后被清空
newDeps: Array<Dep>
//上次求值所收集的Dep实例Id列表,用来避免两次求值之间的重复收集以及去除废弃的观察者
depIds: SimpleSet
//本次求值所收集的Dep实例Id列表,用来避免本次求值的重复收集,
//在每次求值完成之后都会被赋值给depIds,然后被清空
newDepIds: SimpleSet
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
if ((this.vm = vm) && isRenderWatcher) {
vm._watcher = this
}
this.cb = cb
this.id = ++uid // uid for batching
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = __DEV__ ? expOrFn.toString() : ''
// parse expression for getter
if (isFunction(expOrFn)) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy ? undefined : this.get()
}
//...
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)
}
}
}
// ...
}
addDep:每个响应式对象的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this),watcher的addDep方法,收集dep和dep.id,同时dep.addSub也把当前watcher收集到subs数组中为了后面的触发更新,其中的逻辑判断,是为了dep不被重复收集;
在watcher.get()最后调用了this.cleanupDeps(),为什么还要清除dep呢?
watcher.cleanupDeps 清除依赖
export default class Watcher implements DepTarget {
// ...
cleanupDeps() {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp: any = 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
}
// ...
}
cleanupDeps:在执行this.get()收集完依赖的最后,会执行this.cleanupDeps(),遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅,然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换,并把 newDepIds 和 newDeps 清空。
比如在v-if这种情况下,如果为false了,v-if下的模板中的用到的响应式数据变化,也不应该触发渲染watcher更新,所以需要把以前的dep清空,重新收集
Object.defineProperty set触发更新
src/core/observer/index.ts
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean
) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
//...
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
childOb = !shallow && observe(newVal, false, mock)
if (__DEV__) {
// ...
} else {
dep.notify()
}
}
})
return dep
}
判断hasChanged(value, newVal),如果新旧值一样,就什么都不做;
observe(newVal, false, mock)如果newValue是对象,将返回Observer实例给childOb,同时将newValue也变为响应式
dep.notify 通知更新
如果shallow为false,也就是说要深度响应式,就调用observe(newVal, false, mock),如果newval是对象,它里面的属性也会变成响应式,
接下来就是dep.notify()通知所有的依赖
src/core/observe/dep.ts
export default class Dep {
// ...
notify(info?: DebuggerEventExtraInfo) {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
watcher.update
遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcher 的 update 方法
export default class Watcher implements DepTarget {
// ...
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
// ...
}
先按一般情况分析,直接走到 queueWatcher(this)
queueWatcher 把watcher装一起等更新
src/core/observe/schduler.ts
const queue: Array<Watcher> = []
let has: { [key: number]: true | undefined | null } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
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) {
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)
}
}
可以看到,每一次数据更新都把watcher往queue数组里面放,if (has[id] != null)不让watcher重复添加,例如:如果用户在data中定义了两个啊a,b两个响应式属性,都在模板中用到,然后同时更新a,b两个属性,这两个响应式数据的依赖watcher都是同一个渲染watcher,没必要收集两次到queue数组中,收集一次就好了
if (!flushing)条件,flushSchedulerQueue没有执行前,watcher队列queue没有正在刷新,就直接添加,else中表示正在刷新,这时数据又变化了,要往queue中添加相应依赖watcher,就需要判断此时queue中watcher执行到第几个了,并通过watcher.id从后往前找到位置插进去在本轮循环中立即执行
至于nextTick方法,暂时先知道在nextTick中调用flushSchedulerQueue方法
src/core/observe/schduler.ts
flushSchedulerQueue 给watcher排个序
const sortCompareFn = (a: Watcher, b: Watcher): number => {
if (a.post) {
if (!b.post) return 1
} else if (b.post) {
return -1
}
return a.id - b.id
}
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
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(sortCompareFn)
// 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()
}
}
queue.sort(sortCompareFn)按watcher的id排序:
1.组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。
2.用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。
3.如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。
watcher.run 更新页面或执行回调
export default class Watcher implements DepTarget {
// ...
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
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(
this.cb,
this.vm,
[value, oldValue],
this.vm,
info
)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// ...
}
如果是渲染watcher,const value = this.get(),当this.get()执行就会更新页面,如果是用户写的计算watcher, this.cb.call(this.vm, value, oldValue),this.cb就是watch中的回调,并且抛出最新值value和上一次的值oldValue