现象:
在Vue开发的时候,data初始化一个对象没有定义任何属性,经过变量赋值的之后,不需要$set方法,该对象下面的属性就也能变成响应式属性
提问:分析下面代码,页面首先先显示什么?先点击changeTest1显示什么?然后点击chageTest2显示什么?
<body>
<div id="app">
<button @click="changeTest1">changeTest1</button>
<button @click="changeTest2">changeTest2</button>
<div>
{{ form.test1 }}-{{ form.test2 }}
</div>
</div>
<script>
let vm = new Vue({
el: "#app",
data: {
form: {},
},
mounted() {
this.form = { test1: 1};
this.form.test2 = 2;
},
methods: {
changeTest2() {
this.form.test2 = 'change2';
},
changeTest1() {
this.form.test1 = 'change1';
},
}
});
</script>
</body>
- 公布答案:
首先显示:1-2
, 点击changeTest1显示:change1-2
,点击changeTest2显示:change1-2
疑问:在data中的form对象没有定义test1和test2属性,test1可以改变视图,而test2却不能改变视图?
1): 打印一下form对象,可以看到test1有set,get修饰符,而test2没有get,set修饰符
2): 原来test1能够改变视图是因为被Vue用Object.defineProperties()处理,有get,set修饰符
,收集到渲染watch。
3): 而test2没有get,set修饰符
,没有收集到渲染watch。
4): 从这个表现就能知道test1可以更新视图,而test2不可以更新视图
疑问:为什么test1有get,set修饰符
1): 上面代码可以注意到在mounted生命周期那里,对this.form进行了赋值,所以触发了form的set修饰符,它会执行以下函数中的set函数
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
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
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
console.log(obj, key)
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()
}
})
}
2):可以看到newVal是{test1: 1}, 抛掉所有if-else的判断,会执行一行代码 childOb = !shallow && observe(newVal)
,shallow是undefined,然后执行observe函数,参数为{test1: 1}
看一下observe函数的功能
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
3): 会发现最终会执行ob = new Observer(value)
, 再看Observer函数
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
4): 最终发现ob = new Observer(value)
,就是把对象定义响应式的入口函数
5): 原来值{ test1: 1 },就这样被隐式
添加了set,get修饰符
总结:对this.form = { test1: 1 } 进行赋值的时候,就会触发到form的set函数。在set函数里面,会对newVal也就是{test1: 1}
走一次new Obsever(value)来添加set,get的修饰符,因此test1就变成了响应式属性
疑问:为什么test2没有set,get修饰符
1): 应为对象无法监听新增和删除,对this.form.test2 = 2的时候,没有触发form的set函数
,因而没有添加get,set修饰符
流程总结:
1): Vue在初始化阶段对data定义的对象form添加set,get修饰符
2): 执行render函数(生成vnode的过程),form取值一次,触发get函数,收集渲染watch
。form.test1取值一次为1,没有get修饰符。form.test2取值一次为2,没有get修饰符
3): 把vnode(虚拟dom)变成真实dom,页面显示: 1-2
4): 在mounted生命周期中对this.form = { test1: 1 }进行了赋值
,触发了form的set函数,然后对{ test1: 1 }添加get,set修饰符,test1变成了响应式属性
5): 然后执行dep.notify()
,触发渲染watch,执行render函数
6): 在生成vnode的时候(模板中的{{ form.test1 }} {{ form.test2 }}),需要form.test1,form.test2取值一次,这时候就触发了test1的get修饰符,收集该渲染watch
, 使得test1有了更新视图的能力,而test2没有get修饰符,无法收集改渲染watch,没有更新视图的能力
7): 点击chageTest1的时候,修改了test1的值,就会执行set修饰符
,执行dep.notify()去更新视图,页面显示: change1-2
8): 点击changeTest2的时候,修改了test2的值,由于test2没有get,set修饰符
,所有无法更新视图, 页面显示: change1-2
疑问:数组是怎么监听自身的改变的呢
1): 原来Vue对数组的push,pop,shift,unshfit,sort,reverse方法额外处理,例如:当数组新增的时候,能够给新增的项添加get,set修饰符,使得它们变成响应式属性,这就是对象和数组的区别
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
最后:
给Vue中data的值赋值对象或者数组,Vue内部会自动帮我们给该对象或数组添加get,set修饰符,并且为每一个属性收集渲染watch,使得它们有更新视图的能力
结束语:
文章大概说了Vue如何添加响应式属性,如何触发视图的更新,忽略了很多细节,比如渲染watch,AST抽象语法树,render函数,异步更新,虚拟dom变成真实dom等等。文章的目的是想说:什么情况属性会变成响应式属性,什么情况下没有响应式属性(当然手动调用$set可以变成响应式属性)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。