前言
通过前三章我们还有Watcher
(观察者)、Dep
(依赖收集容器)未解
Watcher
里有deps、depIds
存储dep
实例,这是个一(Watcher
也就是观察者,这里可指$watch
回调、渲染函数等)对多(dep
也就是依赖,这里可以想象成数据,eg:this.a
)关系
也就是一个Watcher
观察了多个数据,eg: render()
引用了多个data
Dep
里有subs
存储watcher
实例,这也是一(dep
也就是依赖,这里可以想象成数据,eg:this.a
)对多(Watcher
也就是观察者,这里可指$watch
回调、渲染函数等)关系
也就是一个data
用在了多个地方,eg: this.a
被render()
引用也被计算属性comB
引用
也就是一个数据在多处引用,而一个渲染函数也引用多个数据
正文
Watcher
我们知道它就是想方设法触发所依赖的数据的get
从而收集依赖,这就是核心
export default class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {}
get() {}
addDep(dep: Dep) {}
cleanupDeps() {}
update() {}
run() {}
getAndInvoke(cb: Function) {}
evaluate() {}
depend() {}
teardown() {}
}
理解一个函数就从参数看起就是,也就是从调用处看起
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
这里选择renderWatcher
就因为它够特殊,它接受五个参数:vm
(实例对象)、expOrFn
(被观察的目标,可以访问到待观察的数据即可)、cb
(数据变化触发的回调)、options
(选项)、isRenderWatcher
(是染renderWatcher
)
constructor
this.vm = vm;
// 若是renderWatcher,则赋值在_watcher上
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
首先将实例对象vm
赋值给this.vm
,这样子每个Watcher
实例都知道它属于哪个组件实例
然后判断下若是renderWatcher
(组件renderWatcher
)就将当前Watcher
实例赋值给vm._watcher
(initLifecycle
初始化),这样子比如销毁组件就可以取到该组件renderWatcher
实例对象(vm._watcher.teardown()
)
最后将当前Watcher
实例对象push
到vm._watchers
(initState
初始化)
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.computed = !!options.computed;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.computed = this.sync = false;
}
处理下options
,就是只接受五个参数:
-
options.deep
,用来标识是否深度观测 -
options.user
,用来标识是否是用户自定义还是内部定义,this.$watch
就默认传入options.user = true
-
options.computed
,用来标识当前观察者是否是计算属性观察者 -
options.sync
,用来标识数据变化之后是否同步求值且调用回调,默认是将其放进异步队列后统一触发 -
options.before
,钩子,就是数据变化之后,更新触发之前调用的(eg: beforeUpdate
)
this.cb = cb;
this.id = ++uid; // uid for batching
this.active = true;
this.dirty = this.computed; // for computed watchers
这个就dirty
提下,也就是它在计算属性下才为真,这个代表是否已求值,true
代表未求值
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
分为俩组用于存储当前的watcher
被哪些dep
收集了以及它们的id
,用于防止重复收集依赖
deps、depIds
是当前的依赖情况,newDeps、newDepIds
是页面重新渲染之后的情况也就是数据更新之后的依赖情况(初始时为空,cleanupDeps
导致的),也就是重新收集
// 传入的取值表达式,用于报错提示
this.expression =
process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
// parse expression for getter
// 将expression转成getter
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
// 将'a.b.c'转成取值函数
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = function () { };
process.env.NODE_ENV !== "production" &&
warn(
`Failed watching path: "${expOrFn}" ` +
"Watcher only accepts simple dot-delimited paths. " +
"For full control, use a function instead.",
vm
);
}
}
这个就是供Watcher
取值的源头。expression
就是取值表达式字符串,用于开发环境定位使用
expOrFn
可传入字符串、函数,所以需要序列化:
- 函数的话就无须处理
- 字符串的话(
a.b.c
),那么就得处理下
if (this.computed) {
this.value = undefined;
this.dep = new Dep();
} else {
this.value = this.get();
}
这里就是核心也就是触发求值,但是计算属性watcher
和普通watcher
不同,普通watcher
会立即求值,计算属性watcher
其实会在被引用时触发其存取描述符get
才求值
这就是
computed
惰性求值
收集依赖
很明显就是this.value = this.get()
触发的,这个除了触发求值还有取到值赋值给this.value
get() {
// 设置Dep.target
pushTarget(this)
let value
const vm = this.vm
try {
// 触发getter用于收集依赖
value = this.getter.call(vm, vm)
} catch (e) {
// ...
} finally {
// ...
}
return value
}
首先调用pushTarget(this)
(源码看下文Dep
),这个很明显就是设置Target
也就是赋值当前观察者了
然后就是求值了value = this.getter.call(vm, vm)
,因为这个有是用户自定义的,所以getter
不可预测,就得用try/catch
这个会触发get
属性访问器,即:
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;
}
这里之前章节基本都讲过,就差dep.depend()
,详解如下Dep
节,我们可知其调用Watcher.addDep
addDep(dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
// 本轮收集中还没订阅这个dep
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 还没有订阅这个需要订阅的dep
dep.addSub(this)
}
}
}
// 例子
new Vue({
data: {
a: 'a'
},
template: `
{{a}}{{a}}
`
}).$mount('#app')
数据观测之后有一堆
dep
,这些dep
是基本固定的,这就可以根据dep.id
来处理这个重复依赖问题
我们知道观测完数据之后会留下一堆的dep
,而Watcher
而言(比如renderWatcher
)只有一个(以Watcher
为视角),数据变化的话我们可以触发dep.notify()
。但是这个回调是在Watcher
里,我们怎么把这俩个关联起来?
这里其实就是处理重复依赖的核心逻辑,我们先判断当前Watcher
是否订阅了这个dep
(具体来说就是dep
对应的数据,可以使data、computed
)(也就是新的
渲染函数是否用了数据a
),最开始这个记录自然是空的,那么就将此dep.id
记录在newDepIds
表示这个记录了,dep
记录在newDeps
这里就把
Dep
记录在了Watcher
上
然后判断下当前的Watcher
是否订阅了这个dep(也就是当前
渲染函数是否用了数据a
),没有的话就将此观察者添加到该dep
里,即dep.addSub(this)
这里就把
Watcher
记录在了Dep
上。因为没有的话(!this.depIds.has(id)
)就说明这个收集订阅a
的观察者的容器(dep
)没有这个观察者,那么走到这步就是要收集这个观察者,所以就添加了
get() {
// ...
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
// 用户设置的话那么就会报错具体的语句
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
// 若是deep观测,那么久递归读取子属性值,已达到收集子属性依赖
traverse(value)
}
// 当前watcher完了之后就得置空(pop),轮到新的watcher
popTarget()
// 清理下订阅,比如之前订阅了a、b,现在订阅了a、c,那么得把b给清掉就这么个性能优化
this.cleanupDeps()
}
return value
}
回到watcher.get()
,若是报错的话判断下若是用户自定义就报下错
接下来判断下是否是深度观测,是的话就traverse(value)
(看下文,其实就是遍历访问每一个属性以达到触发其存取描述符get
的目的,从而收集依赖),然后popTarget()
,当前watcher
出栈,轮到下一个watcher
接下来就是很重要的this.cleanupDeps()
,这个其实就是每轮收集之后需要清理掉旧的已经不需要的依赖
cleanupDeps() {
let i = this.deps.length
// 循环遍历当前订阅过的deps
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
// 若是新的不订阅这个曾经订阅过得dep就得删除
// 这样子完成了dep的取消订阅
dep.removeSub(this)
}
}
// 就是更新下当前的订阅列表
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp // 这里得需要,不然this.depIds也会被clear,下同
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
首先将当前所依赖的依赖集遍历,然后判断新的Watcher
里是不是需要这个dep
,不需要的话就删掉就是了
然后就将newDepIds
(新的)赋给depIds
(当前的),然后得清空newDepIds、newDeps
俩变量。
- 从
addDep、cleanupDeps
可见newDepIds、newDeps
存的是本次求值收集的dep
,depIds、deps
存的是上一次,也就是当前。且每次最后会将newDepIds、newDeps
赋值给depIds、deps
,然后清空depIds
用于在多次求值时避免收集重复依赖,因为它存储的是当前的依赖集列表,不会被清空newDepIds
用于在一次求值时避免收集重复依赖,因为它每次求值之后都会被清空,它存储的是新的依赖集列表- 先把新的依赖情况给取到,然后就可以和当前的的比对从而去重
最后返回value
,这样子就可以被赋值给this.value = this.get()
触发依赖
很明显,触发依赖是设置属性值时触发其set
里的逻辑
set: function reactiveSetter(newVal) {
// ...
dep.notify()
}
很明显,这个就是触发依赖的关键(详见下文),而其最终导向watcher.update
update() {
if (this.computed) {
if (this.dep.subs.length === 0) {
this.dirty = true
} else {
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
这里我们先不管computed
,那么余下俩个就是同步与异步,我们先不管异步,其实和同步一样的是最后都是run
完成了更新操作,所以我们先看run
run() {
if (this.active) {
this.getAndInvoke(this.cb)
}
}
可见run
也只是判断了下当前观察者是否激活,激活的话就会调用getAndInvoke
方法,且将this.cb
为参数传入。这时候我们知道getAndInvoke
肯定就是更新变化操作的根源了
getAndInvoke(cb: Function) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
这个一开始就重新求值,这个也是为什么renderWatcher
传入的回调时noop
的原因,因为会重新求值,那么渲染函数自然也会被调用
然后就是一个if
语句,三个判断条件:
- 判断新旧值是否相等,不相等的话才执行回调
- 是对象的话也执行回调,因为对象的话引用不变但是数据内容可能变了
- 深度观测也执行回调,因为深度观测的话就是对象
接下来看if
语句内部逻辑,首先定义了oldValue
来存储旧值,将前面求得新值赋值给this.value
,给dirty
赋值false
代表已经求值了,然后就是个if
语句来区分下是否是用户自定义还是内部定义的watcher
。用户自定义的不可控,得用try/catch
包裹,且给适当的提示报错的话。最终回调调用就是cb.call(this.vm, value, oldValue)
,这个也就是回调函数的参数新旧值由来
Dep
这里就是收集依赖的容器,它暴露了相关方法
其实从某种情况而言你可以将其当做数据的一个影子,因为每一个
dep
都有其对应的一个响应式数据,computed
特殊点而已
Dep Class
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array;
constructor() {
this.id = uid++
this.subs = []
}
addSub(sub: Watcher) {
this.subs.push(sub)
}
removeSub(sub: Watcher) {
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()
}
}
}
看一个类自然先从constructor
看起,可见先初始化俩实例变量:id、subs
,id
是Dep
实例对象的唯一标识,subs
是存储依赖(观察者)的数组,还有一个静态属性target
用来存储当前处理的观察者实例
-
depend()
:首先判断Dep.target
,这是因为不是每处调用都和get
一样保证其有值。然后调用Dep.target.addDep(this)
,这个为什么不直接push
到subs
,这是因为需要做依赖重复处理 -
addSub
:将观察者存入subs
,也就是订阅 -
removeSub
:将观察者从subs
删除,也就是取消订阅 -
notify
:就是通知订阅者数据变更,首先取subs
副本,这是因为防止更新期间subs
变动,subs
存储的是watcher
,循环遍历调用其update()
即可,watcher.update()
与传入Watcher
的cb
有关联,之后再说
pushTarget、popTarget
// 当前处理的watcher
Dep.target = null
// 当前处理的watcher只能有一个,所以要是还没处理完当前的又设置Dep.target就得把后来的入栈
const targetStack = []
export function pushTarget(_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
// 完了之后出栈就能拿到后来的
export function popTarget() {
Dep.target = targetStack.pop()
}
首先定义Dep.target
用于存储当前处理的watcher
,因为当前处理的watcher
,所以若是有另外的watcher
待处理就得入栈排队,这就是targetStack
暴露俩个pushTarget、popTarget
方法,用于设置Dep.target
traverse
深度观测所用,也就是递归循环遍历触发属性get
const seenObjects = new Set()
export function traverse(val: any) {
_traverse(val, seenObjects)
// 完了之后清空seenObjects,以备下次使用
seenObjects.clear()
}
function _traverse(val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
// 因为深度观测,所以不能是非对象、不能被冻结、不能是VNode实例
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
// 这里是处理循环引用情况
// 实现val是对象且响应式,那么必然有__ob__属性
if (val.__ob__) {
// 我们需要一个标志来定位到这个val,就用其对应的dep的id即可
const depId = val.__ob__.dep.id
// 判定seenObjects里已经添加了这个,也就是已经处理过了,就return
if (seen.has(depId)) {
return
}
// 没处理的就添加上标志已经处理了
seen.add(depId)
}
// 因为数组可对象取值方法不一样,所以区分下,仅此而已。
// 目的都是val[i]、val[keys[i]]这俩触发getter尔
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
方法,这种很明显是得要递归的,那么_traverse
也就应运而生了。它调用_traverse
,传入的seenObjects
用于存储dep.id
来判断是否处理过
接来下看_traverse
。首先判断下若是非对象、被冻结、VNode
实例对象,那么自然不能深度观测,return
就是了
然后判断下是否有__ob__
,有的话那么必然是响应式的也就是无需处理的。因为可能有循环引用情况,那么就得把这个对象的dep.id
存着,这样子下次遍历到这个对象的时候就可以判断这个对象是否已经遍历过了。不然的话可能会无限循环
最后就是递归了,这个就得区分数组还是对象,因为俩个遍历方式不一样。目的其实都是为了触发get
,val[i]、val[keys[i]]
这个就是触发get
以达到收集依赖的目的