在前面的两篇文章中,我学习并实现了vue的响应式更新和依赖收集,并且了解了nextTick的原理。
现在的问题是:数组的变化驱动页面渲染还没有实现。
今天要学习的主要内容就这这一部分啦。😁
知识点回顾,究其原因
let methods = [
'shift',
'unshift',
'pop',
'push',
'splice',
'reverse',
'sort'
]
// 改写的用自己的,没有改写的用原来的。
methods.forEach(method => {
arrayMethods[method] = function (...args) {
// 这里的args是传入的参数列表
// 比如arr.push(1,2,3),则 ...args = [1, 2, 3]
oldArrayPrototype[method].call(this, ...args)
let inserted;
let ob = this.__ob__;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
default:
break;
}
if (inserted) {
// 如果有新增的值,就继续劫持,这里要观测的是数组的每一项
ob.observeArray(inserted)
}
}
})
在之前的学习中,了解了vue中的数据劫持有下面几个特性
- 在Vue中,对象通过defineProprety实现响应式。
- 针对数组,是对几个变异方法进行了重写。(通过上面的一段代码可以看出,我在之前的学习文章# vue的数组为啥只能用变异方法?index和length得罪了谁?一文中,也做了详细的阐述)
- 对象新增属性的时候,也不会被监听,因为新增属性的时候,不会走get和set,没有进行依赖收集
根据上面几个特性,在vue中使用对象和数组有两点需要注意:
1. 对于数组的操作,只能使用变异方法才可以驱动视图的变化
2. 对于对象的操作,如果是新增的话,只能使用$set处理,才可以驱动视图的变化
正文:数组的处理
对于数组,因为无法象对象那样在get的时候对依赖进行收集,从而也无法在set中去notify视图变化。
所以需要在对数组的key进行observer的时候,在这个属性本身挂载一个dep,以便于后期在操作数组的时候,调用dep的notify更新视图。
举个例子:
<div id="app">
{{arr}}
</div>
let vm = new Vue({
// 按找个套路,Vue就是一个类
el: '#app',
data() {
return { arr: [[1, 2]] }
}
});
给对象或者属性挂载一个dep
在这个例子中,会对arr这个变量进行劫持,劫持的时候,给这个变量挂载一个dep
class Observer {
constructor(data) {
this.dep = new Dep()
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false // 不可枚举
})
// ...
}
observeArray(data) {
// 省略对数组的劫持代码
}
walk(data) {
// 省略定义对象响应式的代码
}
}
让watcher记录dep
当vue对每一个属性使用defineProperty进行重写的时候,让这个对象或者是数组也记录当前的这个watcher
function defineReactive(data, key, value) {
let childOb = observe(value)
let dep = new Dep()
Object.defineProperty(data, key, {
get() {
console.log('key', key)
if (Dep.target) {
dep.depend()
// childOb可能是数组,也可能是对象,对象也要收集,后续写$set的时候需要用到它自己的更新操作
if (childOb) {
childOb.dep.depend() // 让数组和对象也记录watcher
}
}
return value
},
set(newV) {
// 省略代码...
}
})
}
export function observe(data) {
// 省略代码...
if (data.__ob__) {
// 返回这个已经代理过的数据
return data.__ob__
}
// 返回代理的数据
return new Observer(data)
}
用编译方法调用
当用户调用push、pop等变异方法的时候,会进入重写方法
这时候说明用户操作数据,需要调用ob.dep.notity(),重新渲染页面
// 改写的用自己的,没有改写的用原来的。
methods.forEach(method => {
arrayMethods[method] = function (...args) {
// 省略一系列重写方法...
let ob = this.__ob__;
ob.dep.notify()
}
})
「Tip:回顾一下notify做了什么」
notify就是让这个dep里面的subs记录的watcher全部重新更新,也就是依赖了这个变量的视图全部更新。
class Dep {
// ...省略代码
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
数组套数组 [[[]]] 的形式更新
在上面的代码中,我实现了单维数组的视图响应式更新,但是多维数组响应式更新还没有实现。
思路:
当发现Observer的数据是一个数组的时候,对数组进行处理,如果是多维数组,就对其进行递归处理,逐一执行depend方法。
function defineReactive(data, key, value) {
...
Object.defineProperty(data, key, {
get() {
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
// 新增下面的代码,对数组进行处理
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
}
...
})
}
function dependArray(value) {
for (let i = 0; i < value.length; i++) {
let current = value[i] // current 是数组里面的数组
console.log('current', current)
current.__ob__ && current.__ob__.dep.depend()
if (Array.isArray(current)) {
dependArray(current)
}
}
}
截止到这里,就完成了数组的视图更新了。
后记
通过今天的学习,我更加深刻的理解了Vue数据驱动视图更新的原理,也知道了Vue中多维数组是通过递归进行对数据进行监听的。
在实践中需要注意以下几点:
- Vue中,如果给对象不存在的属性赋值,不能使用
this.a.b = 1,而应该通过this.$set(a, b, 1) - Vue中,对于数组的操作需要使用7中变异方法,而不应该使用
index下标和长度处理 - Vue中,数组的项如果是对象,直接修改其对象,也可以更新视图
- Vue中,尽量使用扁平化数据,如果数组或者对象的嵌套太深,会导致大量的递归,性能下降