这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战
数据劫持是Vue
的一个特点,由于Vue
对数据进行劫持,当数据改变之后可以及时看到页面相应,这也是MVVM模式的核心所在,通过前文已经大致知道数据初始化的一个过程,接下来便是对于对象的数据劫持处理,由于Vue
中对于数组和对象的处理是不一致的,本篇先从简单的对象入手
数据劫持
响应式的核心便是通过 Objet.defineProperty
为属性增加get和set方法,由于数据劫持属于核心流程,将其单独抽离一个模块进行处理
创建src/observe/index.js
export function observe(data) {
}
此时state.js
中将其引入,在initData
函数中使用
import { observe } from "./observe/index";
function initData(vm) {
// 删除无关代码
observe(data);
}
由于Objet.defineProperty
存在一个问题:只能劫持一层数据,嵌套的数据格式需要递归劫持,需要将核心逻辑拆分,避免一个一个函数做了太多的事情,将其封装到类Observer
中
observe
只负责提供一个入口和容错判断
export function observe(data) {
/**
* 判断是否为对象, 不是对象便跳出递归
*/
if (!isObject(data)) {
return;
}
return new Observer(data);
}
在Observer
中主要做了一件事情:将数据进行遍历且数据劫持
class Observer {
constructor(data) {
this.walk(data);
}
walk(data) {
const keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
const key = keys[i]; // 获取到对象的键
const val = data[key]; // 获取对应键的值
defineReactive(data, key, val); // 数据劫持
}
}
}
数据劫持的逻辑提取到defineReactive
方法中
function defineReactive(data, key, val) {
// 是否可以配置
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
observe(val); // 递归遍历接触
Object.defineProperty(data, key, {
get() {
return val;
},
set(newVal) {
if (val === newVal) { // 如果新设置的值和旧的值一致,不处理
return;
}
val = newVal; // 新值覆盖旧值
observe(newVal); // 递归遍历接触
},
});
}
需要注意defineReactive
函数中observe
调用了两次,两次调用函数的目标是一致的,但是触发的逻辑是不同的
当初始化传递的数据是具有嵌套层级时
const vm = new Vue({
data() {
return {
person: {
name: 'nordon',
info: {
foo: 'bar'
}
}
}
}
})
这个时候触发的是第一个observe
当改变数据触发set
时
vm._data.person.info = {
msg: 'msg'
}
触发的是第二个observe
两个observe
的调用确保数据无论是初始化还是设置时都能继续劫持数据
通过上述代码可以发现,当开发者定义了具有深层嵌套的数据结构时,由于会递归劫持,会导致性能受到影响,因此建议在定义数据结构时保持数据层级的扁平化一些。若是真的需要深层嵌套数据格式时,可以使用Object.freeze
对数据进行处理