这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战
完整代码 随文章更新....
前言
前段时间跟着教学视频,过了一遍vue2的底层实现。主要聚焦:1. 响应式数组 2. 组件渲染 3. diff算法这三个方面。理解这三块有助于自己写出更健壮的代码,若来年要出去面试的话这一块也必不可少。距离上次学习这部分知识已经过去了很久了,感觉还在,细节已经模糊。所以想重新出发,就当是的代码和源码一起去对这部分知识来一个系统的总结。
概览
个人理解数据观测就是重写data对象的属性的getter,setter。之所以需要重写,是因为我们需要在getter的时候搜集依赖,setter的时候通知依赖更新(以后的章节)。
这一章聚焦在以下问题:
- 对对象观测
- Observe类的实现
- 为什么不观测数组的索引
- 劫持数组部分原型方法
目标
有一个data数据结构如下:
data: {
name: 'xxx',
schoole: {
name: 'xxx',
age: 19
},
arr: [
{ name: 'xxx' }, 2, 3,
]
}
经过一个方法洗礼,完成蜕变
observe(data)
data.app.push({name: 1})
对对象进行观测
总结之后,思路如下:
- 创建observe函数,接受一个data。遍历对象的属性,设置递归结束条件
- 创建Observe类,其功能就是对对象进行观测
observe函数实现
function observe(data) {
// 递归结束条件
if( !isObject(data)) return
return new Observe(data)
}
Observe类
class Observe {
constructor (data) {
if(Array.isArray(data)) {
// 数组劫持策略
} else {
this.wark(data)
}
}
wark (data) {
// 遍历对象属性
Object.keys(data).map(key => {
let value = data[key]
observe(value)
Object.defineProperty(data, key, {
get() {
console.log(`getter: ${key}: ${value}`)
return value
},
set (newVal) {
// 如果设置的值是一个对象,对设置的值观测
if(isObject(newVal)) observe(newVal)
console.log(`setter: ${key}: ${JSON.stringify(newVal)}`)
value = newVal
}
})
})
}
}
- 使用Object.defineProperty重写对象属性的getter,setter。
- 如果setter的值也是一个对象,则对设置的值进行观测
数组观测
对于数组有不同的策略,一是不能对其索引进行观测,后续会说明原因。再则就是对一些能够增长数组的原型方法如:push。要对其参数进行观测。
数组为什么不能观测其索引
数组也是对象,数组的key就是其下标
如果对数组的key进行观测,就会下图的效果
取值arr(vm.arr),效果如下。可以看到首先会输出数组的每一项,然后再输出整个数组。你所认为,简简单单的一个取值操作,其实底层有这么多的性能开销。
所以在写代码的时候要反复用到对象的属性,首先应该把它取出来。避免不必要的开销
// 坏习惯
let obj = {name: 'pual'}
console.log(obj.name)
console.log(obj.name)
console.log(obj.name)
// 好习惯
let {name} = obj
console.log(name)
console.log(name)
console.log(name)
console.log(name)
数组观测策略
-
只对数组的项进行观测,不对索引观测
-
劫持数组部分原型方法
思路:数组实例(arr)<----自创原型(重写方法)<-----数组原型(Array.prototype)。在数组实例与数组原型一个自己的原型对象,在这个原型对象上重写方法,达到劫持效果。思路如下:
let arr = [1,2]
let arrProtoNext = Object.create(Array.prototype)
arrProtoNext.push = function push(item) {
// 这里可对item进行观测
const res = Array.prototype.push.call(this, item)
return res
}
Object.setPrototypeOf(arr, arrProtoNext)
总结
写到这里差不多完了,完整代码查看git仓库。写到这里突然发现了一个问题,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<span v-for="(item, i) in arr">{{item}},</span>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
const vm = new Vue({
data() {
return {
name: 'xxx',
schoole: {
name: 'xxx',
age: 19
},
arr: [
1, 2, 3,
],
}
},
el: '#app'
})
vm.arr[3] = 4
</script>
</body>
</html>
这里通过cdn引入Vue,然后vue里面有一个长度为3的数组,当我们通过索引,显示增加一行的时候。页面并没有更新。原因很简单:数组的索引没有做观测。我们只监听了数组变化的原型函数。所以操作数组的时候我们要通过原型上的方法操作数。
vue3使用了proxy来代替Object.defineProperty,出于什么原因了?是不是能解决这个问题?