@frank-shaw
2019-11-30T10:18:38.000000Z
字数 2837
阅读 1048
vue
数据响应式
数组响应式
回答这个问题,实际上回答的是:Vue的数据响应式原理有什么限制?理解了Vue的数据响应式原理的限制,就可以很好回答这个问题。
在实际使用Vue的过程中,对于响应式Data的声明,必须将需要响应的各个属性都逐一在声明阶段写清楚。如果对于一个响应式的Object,动态增加了某一个属性,那么Vue是无法为该新增属性做响应式处理的。举个栗子:
export default {
data(){
return {
foo : 1,
bar : {tau : "test"}
}
}
//给data动态增加属性newFoo, 页面无法响应式
this.data.newFoo = "123"
//给bar动态增加属性tau2, 页面无法响应
this.data.bar.tau2 = "tell me"
理解了这个限制,我们再来看看数组的情况。我们会经常对数组进行的操作是怎样的呢?
var array1 = ['t1','t2','t3']
//动态修改元素
array1[0] = 'tt1'
//动态增加元素
array1[4] = 't5'
//push方法增加数组元素
array1.push("1234")
//动态改变元素的长度
array1.length = 100
这些常规操作,如果只是通过常规的拦截key、value的方式来进行数据响应式,那么明显无法完全覆盖。并且成本太高,设想一下:如果用户直接写一个array1[1000] = 'haha'
,Vue是否要为其1000个子元素(极有可能是都为null)都响应化处理呢?
另外,值得注意的是,之所以对象可以在data声明的时候写死,是因为页面操作中的对象属性基本上可以在编写的时候确定。但页面中数组的变化就不一样,其变化基本上无法预测。而JS语言中的数组对其子元素的属性不限制,于是更加凌乱。
那么,Vue是采用了什么方式来进行数组的数据响应化实现呢?
我们来接着上节课的内容,看源码。
查看/src/observer/array.js
,可以知道Vue对数组的七个方法进行了拦截处理:
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)
//额外通知变更,当然,只有这7个方法才会有这个待遇
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的时候,如何处理数组:
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
//值得留意的是:Observer对象在一个Vue实例中是存在多个的,取决于data数据中嵌套了几个Object对象或数组对象
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 {
//如果无法使用原型,那么通过defineProperty的方式将变异方法赋予响应化数组
copyAugment(value, arrayMethods, arrayKeys)
}
//接着,对数组子元素,进行新一轮的observe数据响应化的过程
this.observeArray(value)
} else {
//如果是对象
this.walk(value)
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
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])
}
}
这个过程并不难看懂。至此,我们可以回答这个问题:
data: {
obj: {foo: 'foo'}
bar: ['tua', 'tea']
}
//下面的这个操作,是否会触发数据响应化过程呢?
this.bar[0] = 'testNew';
我们了解了Vue数据响应化的这些限制,但我们如果又想要动态更新数组的属性,或者动态增加已有对象的新属性,该怎么办呢? Vue给我们提供了可用的api: Vue.set()
与Vue.delete()
方法。问题不是如何使用该api,而是想要了解其内部源码是怎样的?
想了解更多,可以通过