前面我们在 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
的实现看起来并不复杂,但是其中的弯弯绕绕还是比较绕人的。本篇的分析其实是比较全面的,但是因为表达原因可能有些地方比较难看懂,大家可以一起交流探讨,错误的地方希望指正。下篇将继续分析响应式原理的其它细节。