以上摘自 深入响应式原理
那么,把这些属性全部转为 getter/setter 具体是怎样一个过程呢?本文不深入具体,简单大致了解其过程,旨在整体把握,理解其主要思路
假设代码如下:
const vm = new Vue({
el: '#app',
data: {
msg: 'hello world'
}
})
data 选项可以接收一个对象或者方法,这里以对象为例(其实最后都会转为对象)
首先,这个对象的所有键值对都会被挂载在 vm._data
上(此外 vm._data
对象上还有个 __ob__
key,暂时可以忽视),这样我们便能用 vm._data.msg
访问到数据
但是通常我们是用 vm.msg
这样访问数据,如何做到的呢?其实就是做了个代理,将 data 键值对中的 vm[key] 的访问都代理到 vm._data[key] 上
proxy(vm, `_data`, key)
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
通常 vm._data
(下划线变量)用作内部程序,对外暴露的 API 是 vm.$data,其实这两者也是一个东西,也是做了个代理,代码大概这样:
const dataDef = {}
dataDef.get = function () { return this._data }
Object.defineProperty(Vue.prototype, '$data', dataDef)
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
}
简单理解就是访问 vm.$data 的时候,就会代理到 vm._data,所以访问 vm.$data.msg 其实就是访问 vm._data.msg。如果直接在开发环境对 vm.$data 赋值会有个警告(是 vm.$data = xxx
这样的赋值,而不是 vm.$data.msg = xxx
这样的赋值,后者是没问题的)
至此,我们理解了为什么能用 vm.msg
、vm._data.msg
以及 vm.$data.msg
三种方式获取/改变数据,最原始的数据是 vm._data.msg
,而另外两者即代理了 _data 的数据,vm.$data.msg
即为 Vue 向外提供的 API,一般情况下开发我们直接用 vm.msg
这样比较多,也方便,如果要获取整个 data,程序中需要用 this.$data
,而不是 this.data
接下来说 getter/setter
将 demo 稍微添点东西:
const vm = new Vue({
el: '#app',
data: {
msg: 'hello world'
},
computed: {
msg2() {
return this.msg + '123'
}
}
})
msg2 是依赖于 msg 的,当 msg 改变的时候,msg2 的值需要自动更新,msg 的改变可以在 vm._data.msg
的 setter 中监听到,但是怎么知道 msg2 是依赖于 msg 的呢?
直观地我们可以想到,遍历所有 computed 对象的键值对,然后进行分析,理论上似乎可行,但是我寻思着这可能需要解析 AST 啊,或者正则去匹配,看看是否用到了 this.msg
,也可能是 this.$data.msg
啊,还可能是 this._data.msg
,而且还要遍历 data
中的所有 key,这看起来也太麻烦了吧,而且,如果程序中没有用到 msg2,那不是多此一举了?
事实上,Vue 初始化的时候会对 vm._data
的每个键值对设置 getter/setter,大概代码如下:
// obj 即为 vm._data
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
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
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
Vue 响应式核心就是,setter 的时候会收集依赖,getter 的时候会触发依赖更新
我们还是以上面的 computed msg2 为例,当我们第一次去取值 msg2 时(注意,必须是取值行为,可以是在 template,也可以是程序中),势必需要去取值 this.msg
,这就会触发 msg 的 getter,此时我们就可以确定 msg2 依赖于 msg
msg 可以被哪些东西依赖呢?目前看来有三
- template 模版中
- computed
- watch
我们可以打印 vm._watchers
查看,是一个 Watcher 实例数组,直接看实例的 expression 值,其实就是触发这个表达式的时候,会触发 msg 的 getter
而这个表达式就对应上述的三种情况,因为 msg 改变的时候,这些表达式需要重新求值,所以这些依赖项都要保存起来,所以源码中定于了这个 Watcher 类
watcher.deps 数组表示该 watcher 的依赖项,值为 Dep 实例,可以理解成和 Watcher 实例的表达式有关的 data 数据。注意,deps 数组可能是空,对于 template 而言,可以是 template 中不依赖于 data,对于 computed 而言,可以是这个 computed 数据还没被获取(比如我定义了 msg2,但是程序中没有用,这时 deps 为空,这表明我如果改变了 msg,但是不需要通知到 msg2,因为 msg2 根本没用到嘛,但是我在控制台输入 vm.msg2,从而触发了 msg 的 getter,继而进行了依赖收集,这时 deps 就不为空了,这表明我已经使用了 msg2,下次 msg 更新时需要通知到 msg2 进行改变)
而对于 watch 而言,我试了下任何情况下 deps 都不为空,这需要进一步查看源码确认
deps 数组元素是 Dep 实例,该实例有个 subs 属性,是 Watcher 实例数组,表示依赖于这个 Dep 的项目
Watcher 和 Dep 比较难理解,可以暂时这样理解,Dep 和 data 挂钩,每一个 Dep 实例就对应 data 的一个键值对,Watcher 实例则依赖于 Dep,那么有三个情况会依赖,也就是以上三种(想想是不是这样,当数据更新的时候,是不是只有这三处需要同时更新,或者同时响应)
总结下:我们会对 data
中所有键值对设置 getter/setter,getter 的时候我们会收集依赖(依赖项为上面三项,并不是任何情况下都会收集依赖,比如在钩子中打印 msg,这时候就没依赖,所以源码中这里还有复杂判断),setter 的时候我们会将收集的依赖项触发,从而更新数据,理解了这些,就能初步理解 Vue 的响应式原理