本文主要分析 Vue 响应式对象的创建,以 data 为例,Vue 内部是怎么处理的。
data 处理:
1、对 vm._data 设置 getter / setter 拦截,通过 Dep 管理 Watcher
2、vm.key 访问实际是 vm._data.key
function initData(vm: Component) {
let data = vm.$options.data
// 处理后的 data 给 vm._data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// ... 校验 data 属性
// 代理 vm.key -> vm._data.key, 注意 $,_开头不会代理
proxy(vm, `_data`, key)
// 将 data(vm._data) 响应式
observe(data, true /* asRootData */)
}
// 我们访问 this.msg -> this._data.msg -> this._data.msg 的 getter
proxy 函数作用将
target.key -> target[sourceKey][key]
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
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)
}
实例是怎么能访问到 data 中的数据?
data: {msg: 'dxx'},为什么页面能直接 this.msg 访问到?
因为 Vue 内部会 proxy(vm, '_data', 'msg'),将 msg 放到 vm 实例中,所以页面能访问 this.msg
若 msg 改为 _msg 会怎么样呢? 页面如何访问
改为$,_ 开头 proxy 不会执行,页面中通过 this._data._msg 或 this.$data._msg;
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
if (process.env.NODE_ENV !== 'production') {
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
可以看到访问 $data $props 下的属性其实是 _data _props
而修改$data={} 会给警告 $props 也不能修改
observe 将对象变成响应式
var vm = new Vue({
el: '#app',
template: `
<div>
{{msg}}
</div>
`,
data() {
return {
msg: {
age: 10,
}
}
},
})
流程:
data 是个对象 {msg:{age: 10}} 记为 root,msg 的值是个对象 {age:10} 记为 a,age 的值是个基本类型 10,记为 b。
observe(root)
开始
root.__ob__ = Observer1 {value: root, dep1, vmCount=0}
defineReactive(root, 'msg') msg get/set 闭包引用 new Dep2
observe(a)
开始
a.__ob__ = Observer2 {value: a, dep3, vmCount=0}
defineReactive(a, 'age') age get/set 闭包引用 new Dep4
observe(b) return
设置 a.age get/set Dep4, childOb没有
return Observer2
结束
设置 root.msg get/set Dep2, childOb 为 Observer2
return Observer1 (vmCount++)
结束
上面 walk 没有加进去,因为对象里面只有一个属性 key;若多个属性 key 会循环调用 defineReactive ;
defineReactive 设置 obj.key 的 get/set,并通过 Dep 管理依赖,get 时收集依赖,set 对值处理,通知收集的依赖处理更新
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 每个属性都有 dep
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
// 注意参数是 2 个,且 getter 没有值,或有 setter
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// shallow true 的话不进行深度处理 $attrs $listeners 不进行深层次设置(shallow=true)
let childOb = !shallow && observe(val)
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 提供 set 处理
var vm = new Vue({
el: '#app',
template: `
<div @click="change">
{{msg}}
</div>
`,
data() {
return {
msg: {
age: 10
}
}
},
methods: {
change() {
// this.msg.name = 'dxx'; 页面不会更新
this.$set(this.msg, 'name', 'dxx')
}
}
})
初始化后对象结构
data {
msg(dep2): {
age(dep4): 10,
__ob__: Observer2 (dep3)
},
__ob__: Observer1 (dep1)
}
dep1 dep3 由 Observer 实例时生成
dep2 dep4 由 defineReactive 是对属性拦截时设置 闭包引用
页面仅有一个渲染 Watcher
当页面渲染 msg 时,this.msg -> this._data.msg -> msg.get dep2[Watcher], childOb为 Observer2, dep3[Watcher] dep4[Watcher]
Vue.set 函数源码:
function set (target: Array<any> | Object, key: any, val: any): any {
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 时的对象需要响应式(defineReactiv设置get/set);对 this.$set(this.$data, 'aa', 'aaa'); 是无效的 data 不能收集依赖 ×
// 非实例 vm ;避免覆盖 this.$set(this, 'aa', 'aaa');×
页面点击时,target 为 msg 对象,key 为 name,val 为 dxx ;msg.__ob__ 为 Observer1; ob.value就是 msg 对象,defineReactive 执行后name(dep5): 'dxx',要重新渲染 msg 对象,通过 msg.__ob__.dep.notify() 渲染 Watcher 执行。data 重新收集依赖。
当对象属性定义 getter 时,data 拦截会怎么设置?
const data = {}
Object.defineProperty(data, 'getterProp', {
enumerable: true,
configurable: true,
get: () => {
return {
msg: 1
}
}
})
var vm = new Vue({
el: '#app',
template: `
<div @click="change">
{{getterProp}}
</div>
`,
data() {
return data
},
methods: {
change() {
this.$set(this.getterProp, 'ns', Math.random());
}
}
})
初始化后的数据结构:
开始 data {
getterProp(get): fn
}
执行到 defineReactive 时,因为 getter 有值,没有 setter,所以 val=undefined,childOb = undefined
然后重新设置 getterProp 的 get/set
当页面渲染 getterProp 时,仅 getterProp 的 dep 收集,返回 value {msg: 1}
页面点击
此时 this.getterProp -> {msg: 1} 它的 __ob__ 不存在,页面不会更新。
数组的处理
var vm = new Vue({
el: '#app',
template: `
<div @click="change">
{{msg}}
</div>
`,
data() {
return {
msg: [1]
}
},
methods: {
change() {
this.msg.push(2)
}
}
})
初始后
data {
msg(dep2): [
1,
__ob__: Observer2 (dep3)
]
__ob__: Observer(dep1)
}
数组的处理,内部重写了 push pop unshift shift splice sort reverse ,主要是添加新元素要将新元素先 observe,然后 notify 通知更新,通知的关键就是上面的 dep3。数组还有注意点就是收集时,相关所有子元素通过 dependArray 都会收集依赖
var vm = new Vue({
el: '#app',
template: `
<div @click="change">
{{msg}}
</div>
`,
data() {
return {
msg: [1,{name: 'dxx'},[2]]
}
},
methods: {
change() {
this.msg.push(2)
}
}
})
这个加载后的数据结构:
data {
msg(dep2):[
1,
{
name(dep5): 'dxx',
__ob__: Observer3(dep4)
},
[
2,
__ob__: Observer4(dep6)
],
__ob__: Observer2(dep3)
],
__ob__: Observer1(dep1)
}
Watcher 的收集
dep2 dep3 dep4 dep6 dep5
总结
本文分析 Vue 对 data 的处理,通过 observe 处理,将它们转为响应式对象。Watcher 管理页面渲染,Dep 管理 Watcher,而对象中每个属性都有自己的 Dep。Dep、 Wathcer 关系是由属性的 getter / setter 触发。