提问
有一个小伙伴和我分享了一道面试题:哪些情况改变data的值,视图不会发生变化?想必我们的项目中都会碰到过这样的问题,我写了一个小demo,data有对象,有字符串数组,有数组对象,又在method里写了一些按钮可以修改对象的值,请问以下哪些修改方式能够直接触发视图更新?
<template>
<div>
<div class="objectString">
<span>{{objectString.id}}</span>
<button @click="ObjectString_change">change</button>
</div>
<div class="arrayNumber">
<span v-for="item in arrayNumber" :key="item">{{item}}</span>
<button @click="ArrayNumber_change">change</button>
</div>
<div class="arrayString">
<span v-for="item in arrayString" :key="item">{{item}}</span>
<button @click="ArrayString_change">change</button>
</div>
<div class="arrayObject">
<span v-for="item in arrayObject" :key="item.id">{{item.id}}</span>
<button @click="ArrayObject_change">change</button>
</div>
<div class="arrayObject">
<span v-for="item in arrayObject2" :key="item.id">{{item.id}}</span>
<button @click="ArrayObject2_change">change</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
objectString:{ id:0 },
arrayNumber:[0,1],
arrayString:["a","b"],
arrayObject:[{ id:0 },{ id:1 }],
arrayObject2:[{ id:0 },{ id:1 }],
}
},
methods: {
ObjectString_change(){
this.objectString['id'] = 99
},
ArrayNumber_change(){
this.arrayNumber[0] = 99
},
ArrayString_change(){
this.arrayString[0] = 99
},
ArrayObject_change(){
this.arrayObject[0].id = 99
},
ArrayObject2_change(){
this.arrayObject2[0] = { id:99 }
},
}
};
</script>
<style>
span{
color:blue;
padding-right:20px;
}
</style>
答案是只要是通过数组索引修改都不能触发视图更新( data[0] ),通过修改对象的property都能触发更新( data['id'] )。其实官网已经给出了答案,但我还是想深究一下为什么。
引用Vue官网的解释
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
为了解决第一类问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue
相同的效果,同时也将在响应式系统内触发状态更新:
1.Vue.set(vm.items, indexOfItem, newValue)
2.vm.items.splice(indexOfItem, 1, newValue)
知识储备
Object.defineProperty
我们都知道Vue能双向绑定的关键点是:对data上所有property全部 Object.defineProperty
一遍,在data值发生改变的时候会触发 setter 函数内的代码。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。那究竟是谁做了 Object.defineProperty
这件事,为何修改数组下标不会触发,我们只能去GitHub搜索vue的源码一探究竟了。
监听数组
在此之前,了解[Object.defineProperty](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)
的基础知识是非常重要的,来看一个监听数组的例子:
let arr = [0,1,2];
/*
key:"0",value:0,
key:"1",value:1,
key:"2",value:2,
*/
for(key in arr){
observer(key)
}
function observer(key){
Object.defineProperty(arr,key,{
set(){
console.log("set the value")
},
get(){
console.log("get the value")
}
})
}
浏览器运行一下,在控制台输入arr[0]=100触发set,输入arr[0]触发get。说明数组通过下标修改是可以被监听的到的!
Vue源码窥探
我带着“为什么vue不能监听”的疑问阅读了vue源码后发现:原因是Vue根本没有 defineProperty 数组的key!
//vue源码
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
export function observe (value: any, asRootData: ?boolean): Observer | void {
//不是object类型跳出函数
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
}
结论
对比我们写的例子可以发现:我们是遍历数组的 key 逐个defineProperty,而他是遍历数组的 value 逐个 defineProperty(而且只有在value类型为 Object 的情况下才会继续),所以通过修改数组的索引 arr[0]=100 自然就不能触发set函数。至于不监听是因为当数组长度过长时,性能的代价与用户体验收益不成正比。
Vue对此提供一种解决方案,通过数组上的 push 等方法触发视图的更新,Vue 做了什么大伙应该能猜得到,它在 vue/observer/array.js 重写了 push 等方法挂载到了 Array.prototype 上。
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
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
})
})
通过Observer 构造器挂载到实例上。
//vue/src/core/observer/index.js
import { arrayMethods } from './array'
export class Observer {
constructor (value: any) {
//...省略一些代码
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
}
}
//省略一些方法
}
function protoAugment (target, src: Object) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
拓展阅读
接下来想知道更多细节的小伙伴可以跟我把整个流程走一遍。
- 创建vue实例
vue/src/core/instance/index.js
- 初始化state
data就是我们常说的要写成函数的形式,返回值传入observe函数。
//vue/src/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data={}
//...此处隐藏一些无关代码
}
//...此处隐藏一些无关代码,代理data到实例上
// observe data
observe(data, true /* asRootData */)
}
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
- function observe() 和 class Observer
判断data对象原型链上有没有__ob__属性,一上来肯定是没有的,走new Observer构造器,value值是data对象。
Observer构造器会递归地defineProperty数据,如果是对象依次将 key 传入defineReactive(),是数组则遍历数组的值,值是个对象接着走Observer构造器的老路,不是对象就over,递归用的妙啊。
//vue/src/core/observer/index.js
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)
}
//...此处隐藏一些无关代码
return ob
}
//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])
}
}
}
//def函数
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
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 () {
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()
}
})
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。