众所周知Vue实现响应式是基于Object.definePropertie,这个方法是Object上,那么数组是怎么实现的呢?
初始化
数组和对象的初始化是不一样的,Vue源码中会通过Array.isArray来区分当前变量是不是数组,然后调用不同的方法,本次只简单讲一下数组,由于篇幅有限我们直接讲核心源码,实际上整个流程是initState => initData => observe => Observer
export class Observer {
constructor (value: any) {
this.value = value
// 此处也是重点
this.dep = new Dep()
this.vmCount = 0
// 给所有的属性身上都新增一个变量__ob__
def(value, '__ob__', this)
// 如果判定为数组的话调用数组专有的方法
if (Array.isArray(value)) {
// hasProto= "__proto__" in hasProto
const augment = hasProto
? protoAugment
: copyAugment
// 此处也是关键,这个是把所有Vue封装的数组变异方法覆盖到数组的__proto__上, 请记住是整体覆盖__proto__
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
}
数组一般分为两种json数组或者普通数据类型的数组,如下
var arr = [{id: 1}]
var arr = [1, 2, 3]
如果是第一种的话,Vue会继续递归,直到给每个对象都加上__ob__,如果是第二种的话,给在这里这个数组加上__ob__就完事了
__ob__是什么东西?
查看上面的代码可以发现__ob__是Observer的对象的copy,重点是new Dep那里,因为dep对象是管理所有watcher的地方,他是Vue实现某个数据改变更新视图/调用对应的回调函数的关键
arrayMethods
上面有一处代码是把所有的Vue封装的数组方法覆盖到当前数组的原型上__proto__,那么Vue具体封装了那么数组方法呢? Observer/array.js
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
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)
// 调用所有该值对应的watcher
ob.dep.notify()
return result
})
})
通过分析上面代码可以看到Vue封装了[push, splice, ...]等方法,在最后会调用该值__ob__的notify方法,也就是数组响应式的本质
其他情况
<template>
<ul><li v-for="item in arr">{{item.id}}</li></ul>
<ul><li v-for="item in arr2">item</li></ul>
</template>
{
data () {
return {
arr: [{id: 1}, {id: 2}, {id: 3}],
arr2: [1, 2, 3]
}
},
created () {
this.arr[0] = [{id: 5}]
this.arr2[0] = 5
console.log(this.arr[0]) // 5
console.log(this.arr2[0]) // 5
}
}
模版里面循环arr和arr2, 当我们按照上述代码在created改变数组的值,那么按照前面的分析,此处应该只会改变数据而不会更新视图,但是实际上恰恰相反,这个是因为什么呢?我感觉是因为在created截图只是初始化了数据,而还没有渲染dom,等渲染dom的时候会从data内取值,这个时候data内的数组现在是更改完以后的
{
...
mounted () {
this.arr[0] = [{id: 5}]
this.arr2[0] = 5
console.log(this.arr[0]) // 5
console.log(this.arr2[0]) // 5
}
}
那么我们把更新数据的代码放到mounted会出现什么情况呢?arr改完以后视图会更新,而arr2反而不会,那么这又是什么情况呢?
arr会更新视图,我个人感觉是因为数组内的数据类型是object,因为在initData阶段会循环arr数组,如果数组内的数据是object,同样会给当前值绑定上get和set方法,所以arr直接修修改值的时候会触发对象的set方法,然后调用该值对应的watcher内的express方法(this.update),驱动视图更新
总结
数组更新机制其实官网也有表述但是比较简单,只是说了Vue对数组的方法做了二次封装,但是具体怎么实现的没有写,希望此篇文章能让大家对Vue的数组有一个更深入的了解