原文 更好的阅读体验html
当你把一个普通的 JavaScript 对象传给 Vue 实例的
data
选项,Vue 将遍历此对象全部的属性,并使用 Object.defineProperty 把这些属性所有转为 getter/setter。Object.defineProperty 是 ES5 中一个没法 shim 的特性,这也就是为何 Vue 不支持 IE8 以及更低版本浏览器vue
以上摘自 深刻响应式原理react
那么,把这些属性所有转为 getter/setter 具体是怎样一个过程呢?本文不深刻具体,简单大体了解其过程,旨在总体把握,理解其主要思路git
假设代码以下:github
const vm = new Vue({
el: '#app',
data: {
msg: 'hello world'
}
})
复制代码
data 选项能够接收一个对象或者方法,这里以对象为例(其实最后都会转为对象)express
首先,这个对象的全部键值对都会被挂载在 vm._data
上(此外 vm._data
对象上还有个 __ob__
key,暂时能够忽视),这样咱们便能用 vm._data.msg
访问到数据api
可是一般咱们是用 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.msg 其实就是访问 vm._data.msg。若是直接在开发环境对 vm.
data = xxx
这样的赋值,而不是
vm.$data.msg = xxx` 这样的赋值,后者是没问题的)app
至此,咱们理解了为何能用 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 响应式核心就是,getter 的时候会收集依赖,setter 的时候会触发依赖更新
咱们仍是以上面的 computed msg2 为例,当咱们第一次去取值 msg2 时(注意,必须是取值行为,能够是在 template,也能够是程序中),势必须要去取值 this.msg
,这就会触发 msg 的 getter,此时咱们就能够肯定 msg2 依赖于 msg
msg 能够被哪些东西依赖呢?目前看来有三
咱们能够打印 vm._watchers
查看,是一个 Watcher 实例数组,直接看实例的 expression 值,其实就是触发这个表达式的时候,会触发 msg 的 getter
而这个表达式就对应上述的三种状况,由于 msg 改变的时候,这些表达式须要从新求值,因此这些依赖项都要保存起来,因此源码中定于了这个 Watcher 类
A watcher parses an expression, collects dependencies, and fires callback when the expression value changes. This is used for both the $watch() api and directives.
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 的响应式原理
若是看到最后,不妨关注个人公众号「码农随手记」