Vue原理:对象数据劫持

1,452 阅读2分钟

这是我参与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对数据进行处理