【Vue2.x 源码学习】第四篇 - 对象的深层劫持

678 阅读4分钟

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

一,前言

上篇,主要介绍了在 Vue 的数据初始化流程中,对象属性单层劫持的实现;

回顾一下,主要涉及以下几个核心点:

  • data 为函数和对象的处理,及当 data 为函数时的 this 指向问题
  • Observer 类:对数据进行观测
  • walk 方法:遍历 data 属性(对象属性遍历)
  • defineReactive 方法:利用 Object.defineProperty 实现数据劫持(单层劫持)

本篇,继续对 data 数据进行初始化操作,实现对象属性的深层劫持;


二,对象属性的深层观测问题

1,抛出问题

当前版本存在的问题:

data 对象中,如果存在属性值依然为对象类型的情况
这个对象是不会被观测到的(目前所说的观测,还仅仅停留在数据劫持的阶段)
当前仅实现了对 data 对象中的属性进行单层劫持,当出现嵌套对象是,则不会被深层劫持;

2,测试问题

<script>
  let vm = new Vue({
    el: '#app',
    data() {
      // 在data函数返回的对象中,存在obj属性的值仍为对象类型,此时深层的对象不会被观测
      return { message: 'Hello Vue', obj: { key: "val" } }
    }
  });
</script>

3,验证结果

image.png

用户执行new Vue传入的 options 选项中,

在 data 函数中,存在属性 obj 的值依然是一个对象 { key: "val" } ,

控制台输出的对象 { key: "val" } 中,key 并没有被添加 get、set 方法,说明此时并没有实现数据的深层劫持;


三,对象属性深层观测的实现

0,回顾:对象属性单层观测

  • new Vue 时传入options,此处options.data可能是对象也可能是函数;
  • Vue 初始化阶段,执行原型方法 _init:
    • 通过vm.$options暴露options
    • 调用initState方法完成状态的初始化;
    • 如果存在options.el,需要将 data 数据渲染到视图上;
  • 状态初始化阶段,执行 initState 方法:
    • 在 initState 方法中,状态的来源有多种:data、props、watch、computed,目前仅处理 data;
    • 通过vm.$options.data获取到 data,如果 data 为函数,执行函数设置上下文为 vm 实例,获取内部返回的对象;通过这异步处理之后,data 必为函数;
    • 通过 observe 方法,实现数据观测;
  • 数据响应式阶段,执行 observe 方法:
    • 创建Observer实例,遍历 data 对象中的属性,使用Object.defineProperty重写对象中的所有属性,实现数据劫持(单层劫持);

1,实现思路:对象属性深层观测

有了之前对象属性单层观测的基础,对象属性的深层观测就简单了:

{ message: 'Hello Vue', obj: { key: "val" } }为例:

当对象中的属性 obj 即将通过 Object.defineProperty 被劫持之前,先执行一次“对象属性的单层劫持”;

2,代码实现

// src/obseve/index.js
function defineReactive(obj, key, value) {
  
  // 对 key 进行观测前,调用 observe方法,如果属性值为对象则会继续向下找,实现深层递归观测
  observe(value);
  
  Object.defineProperty(obj, key, {
    get() {
      return value;
    },
    set(newValue) {
      if (newValue === value) return
      value = newValue;
    }
  })
}

data.obj 属性的值 { key: "val" }对象中,如果依然存在属性的值为对象或更深层的对象嵌套时(当属性值为对象:Object类型且非null),则继续执行 observe 进行观测,将会形成 observe “对象的单层劫持”的递归,从而实现对象属性的深层劫持;

3,验证结果

<script>
  let vm = new Vue({
    el: '#app',
    data() {
      return { message: 'Hello Vue', obj: { key: "val" } }
    }
  });
</script>

image.png

data 对象中属性 obj 值为对象类型,实现了深层观测,如果存在更深层次的对象嵌套,也会递归实现数据观测;

<script>
  let vm = new Vue({
    el: '#app',
    data() {
      return { message: 'Hello Vue', obj_lv1: { obj_lv2: { obj_lv3: "val" } } }
    }
  });
</script>

image.png

四,data 性能与优化

由于Object.defineProperty的性能比较低,这也成为了 vue2 的性能瓶颈;

Vue 加载时需要对数据进行递归观测,数据越多、层级越深,性能开销就会越大;

优化注意:

  • 1,非响应式数据尽量不要放在 data 中,以减少Object.defineProperty性能开销;
  • 2,数据层次尽量扁平化,不要嵌套过深,深层递归将导致性能降低;

五,结尾

本篇主要实现了 Vue 数据初始化流程中,对象属性的深层劫持,核心思路就是递归;

  1. 通过data = isFunction(data) ? data.call(vm) : data;处理后的 data 一定是对象类型;
  2. 通过data = observe(data)处理后的 data 就实现了数据的响应式(目前只有劫持)
  3. observe 方法最终会返回一个 Observer 类
  4. Observer 类初始化时,通过 walk 遍历属性
  5. 对每一个属性进行 defineReactive(Object.defineProperty)实现对象属性的单层数据劫持
  6. 在 defineReactive 中调用 observe,如果当前属性的值为对象类型,继续对当前对象属性进行观测(即递归执行步骤 3~5),实现对象属性的深层数据劫持

下一篇,数组的劫持


维护日志

  • 20230108:调整内容排版,优化部分文字描述和代码注释,添加性能问题说明与性能优化,使内容更加清晰易懂;
  • 20230109:添加 data 对象深层观测的验证,补充描述和图片;