开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情
对象响应式原理(一)
本篇主要针对Vue中对对象、数组的数据劫持的处理过程
数据劫持defineProperty
首先我们要明白为什么要使用defineProperty?
defineProperty(属性所在的对象,属性的名字,数据属性【用来描述这个数据的】)
数据属性
- configurable:给不给重写
- enumerable: 能不能被for in访问,默认false
- writable : 能不能修改
- value:值,默认undefined
根据MDN对defineProperty的解释为,在对象上定义一个新属性,或者修改现有属性并返回该对象。因此,我们就可以根据这个特性实现双向数据绑定,实现视图随着数据改变。
而defineProperty主要依赖于内部的getter和setter函数对数据进行修改,而这个使用getter和setter来重写本身行为的操作也被称作数据劫持,而这就不得不依赖于一个全局变量(一个临时变量)来接受改变之后的value值。由此,我们也就引出了defineReactive函数,专门用于解决这个临时的变量。
defineReactive
针对上述需要使用全局的临时变量保存更新之后的value方式,vue底层通过更优雅的方式处理了这个问题——使用闭包。
这里给忘记什么是闭包的兄弟说明:闭包可以理解为在函数外部拿到局部的值,即能够读取其他函数内部变量的函数
function defineReactive(data, key, value = data[key]) {
Object.defineProperty(data, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
}
})
}
defineReactive(obj, a, 1)
多重嵌套的对象的响应式实现
利用递归,逐层为其原型上加入可被观测属性。
因此,我们的架构即可以定义为:
- 入口函数:接受参数并判断是什么数据类型,调用数据劫持函数
- 数据劫持:首先要递归地对传入对象进行层层解析,递归调用判断参数类型的函数,如果不是对象类型即返回一个对象类型的参数。
以下为具体实现代码:
// 入口函数
function observe(data) {
if (typeof data !== 'object') return
// 调用Observer
new Observer(data)//使每一个数据都变成可侦测到的
}
class Observer {
constructor(value) {
this.value = value
this.walk()
}
walk() {
// 遍历该对象,并进行数据劫持
Object.keys(this.value).forEach((key) => defineReactive(this.value, key))
}
}
function defineReactive(data, key, value = data[key]) {
// 如果value是对象,递归调用observe来监测该对象
// 如果value不是对象,observe函数会直接返回
observe(value)
Object.defineProperty(data, key, {
get: function reactiveGetter() {
return value
},
set: function reactiveSetter(newValue) {
if (newValue === value) return
value = newValue
observe(newValue) // 设置的新值也要被监听
}
})
}
const obj = {
a: 1,
b: {
c: 2
}
}
observe(obj)
以上总结为流程图的话,即:
graph TD
observe --> newObserver遍历obj.b的属性
newObserver遍历obj.b的属性 --> defineReactive
defineReactive --> observe
数组响应式原理
下文主要针对Vue对数组内方法重写以及相对注意点进行描述。
如何创建一个数组原型到vue实例身上
使用prototype将数组创建的方法放到Object原型身上
const arrayMethods = Object.create(Array.protyotype) //以Array.prototype为原型创建arrayMethods对象
接下来,vue底层需要实现响应式,其实是对数组原本方法的一个重写(七个方法)
//先备份一下原本的七个数组内的方法,然后我们就可以对他进行重写然后绑定到他的原型上
const methodsNeedChange=[//先定义要重写的七个方法
pop;
push;
shift;
unshift;
reverse;
sort;
splice;
]
methodsNeedChange.forEach(methodName=>{
const original = arrayPrototype[methodName];//拿到原型上他原本的方法
//定义新的方法
def(arrayMethods,methodName,function(){//def为我们定义的一个是否允许原型上被重写绑定的函数;
//这里值得注意的是,我传入的重定义的参数是arrayMethods,也就是说我后续需要暴露重定向的也是它
console.log('我已经绑定了新的方法!')
},false)
})
通过以上写法,我们即做了一件事情,即将重写后的数组方法绑定到Object原型身上。那么你可能想问了,我们通过什么方式将他覆盖原本就拥有他的方法呢?答案很简单,将数组的方法类强行指向当前的这个我们自己新重写的arrayMethods上
//先判断他是一个对象还是数组
if(是一个数组){
Object.setPrototypeof(value,arrayMethods)
}else{
是一个对象
}
而值得注意的是,如果我们调用的是七大方法里的shift、splice、push方法时,我们插入或者删除的元素也必须是observe的,而当我们判断这个参数是什么类型的时候实际上也在第一层遍历的时候往数组身上添加了__ob__属性为什么?——数组也相当于对象呀,我们在创建实例的时候实际上也就相当于往他原型上添加了__ob__属性;每个被双向绑定的对象元素(数组也是对象)都会有一个__ob__ ,而且是单例的