前面我们在 vue2源码系列-响应式原理 中介绍了 vue 中的整个响应式实现及流程,其中跳过了某些细节性的代码,现在我们再去好好学习研究一番
入口
我们在 defineReactive 函数里发现这么一段代码
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
}
//...
}
我们知道在 dep.depend() 中会将当前订阅实例 watcher 添加进属性的 dep 中。那下面的 childOb.dep.depend() 又是干嘛的呢?
Vue.set函数
答案在于 Vue.set,我们来看看其实现吧
依旧从入口开始,在 src/core/global-api/index.js 中定义了静态属性 set
Vue.set = set
set 的定义在 src/core/observer/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
// 如果是数组 直接调用splice方法
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 如果是已经存在的属性直接返回
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 某些不允许开发者添加属性的对象
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 非监测属性直接返回
if (!ob) {
target[key] = val
return val
}
// 监测新值
defineReactive(ob.value, key, val)
// 通知更新
ob.dep.notify()
return val
}
咋一看 set 函数并不复杂,无非是判断一些不同的情况。如果是真正符合条件参数的再为其添加属性,监测新值,同时再调用下 dep 的通知。
但其复杂之处就在于 ob.dep.notify()。之前我们学习vue响应式的时候说到当值更新时会触发属性 set 函数并调用属性对应的 dep.notify,而这里调用的是属性值的 dep -> ob.dep, 其定义在 Observer 类中
// Observer contructor
this.dep = new Dep()
我们有必要梳理下 Vue.set 的实现流程
Vue.set实现流程
假设在模板中有这么一段
<span>{{deep.name}}</span>
data() {
return {
deep: {}
}
}
- 在
defineReactive函数中会为属性值{}生成新的监测实例并返回
let childOb = !shallow && observe(val)
其中 childOb 在 new Observer() 中为其添加了 dep 属性指向新的 dep 实例
-
劫持属性
deep的get函数 -
渲染函数调用
this.deep.name触发deep的get函数,将当前订阅者添加进了childOb的dep的订阅者中
if (childOb) {
childOb.dep.depend()
// 数组的情况我们待会再讲
if (Array.isArray(value)) {
dependArray(value)
}
}
注意我们没有劫持deep.name的get,因为此时并deep是个空对象,没有任何属性值,更不会遍历属性为其添加get
-
开发者调用
Vue.set(deep, 'name', 'xxx')添加name属性 -
在
Vue.set函数中调用ob.dep.notify()通知更新
这里的 ob 实际指向了 deep 的值 {},之前我们在 ① 为其添加了 dep 属性,在 ③ 中添加了和 deep 相同的 watcher 实例。所以 ob.dep.notify() 通知了 watcher 更新,和 deep 的 set 中 dep.notify() 本质是一样的。
为数组元素添加属性
继续来看这段还未破解的代码
// defineReactive -> get
if (Array.isArray(value)) {
dependArray(value)
}
为什么要有这段代码呢?我们来看看这么个情况
<span>{{deep[0].name}}</span>
data() {
return {
deep: [{}]
}
}
Vue.set(this.deep[0], 'name', 'xxx')
我们为 deep[0] 赋值新属性 name,按照我们之前的学习成果来看看是否会触发更新
- 是否会触发属性的
dep.notify()?
我们现在相当于触发 deep[0].name 的 set 函数,但是之前没有 name 属性,想必也不会触发自定义 set 了,所以不会更新
- 是否会在
Vue.set中触发ob.dep.notify()?
答案是会的,但是我们应该看看此时的 ob 是?
ob 是 deep[0] 的值 {},但是按照 childOb.dep.depend() 来看,其是在遍历 deep 的属性值函数 defineReactive 中为其添加订阅的。
根据 vue 实现原理,我们是不会对数组进行属性遍历的
// Observe
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 在这边将遍历value进行observe 不会走walk进行defineReactive
this.observeArray(value)
} else {
this.walk(value)
}
所以虽然调用了 ob.dep.notify(),但实际 ob.dep 并没有添加渲染函数的 {{deep[0].name}} 对应的 watcher。但是通过神来一笔
// defineReactive -> get
if (Array.isArray(value)) {
dependArray(value)
}
这样在 defineReactive(vm._data, deep, [{}]) 的时候就会触发 dependArray(value) 完成数组元素 dep 对应 watcher 实例的订阅
dependArray
我们也来稍微看看 dependArray 的实现
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
// 编辑递归数组元素完成e.__ob__.dep.depend() 实现上上...级属性的 get 中订阅实例的订阅
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
总结
Vue.set 的实现看起来并不复杂,但是其中的弯弯绕绕还是比较绕人的。本篇的分析其实是比较全面的,但是因为表达原因可能有些地方比较难看懂,大家可以一起交流探讨,错误的地方希望指正。下篇将继续分析响应式原理的其它细节。