Vue数据代理的实现流程逐步讲解

2023-01-07,,

通过一个对象代理对另一个对象中的属性的操作(读/写),就是数据代理。要搞懂Vue数据代理这个概念,那我们就要从Object.defineProperty()入手,Object.defineProperty()是Vue中比较底层的一个方法,在数据劫持,数据代理以及计算属性等地方都或多或少的用到了本函数

目录
  • 一,前言
  • 二,对象劫持回顾
    • 1,Demo
  • 三,数组类型的处理
    • 1,当前逻辑分析
    • 2,Vue 对性能的权衡
    • 3,数组的劫持思路
    • 4,数组方法的拦截思路
    • 5,数组方法重写的实现
    • 6,数组方法拦截的实现
  • 四,结尾

    一,前言

    上篇,主要介绍了 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),这样就实现了对象属性的深层数据劫持

    本篇,继续介绍 Vue 数据初始化流程中,对于数组类型的劫持

    二,对象劫持回顾

    1,Demo

    data 数据中对象属性的深层观测,即对象属性为对象(包含多层)的情况

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

    当 data 中的属性为数组时,Vue 是如何进行处理的

    三,数组类型的处理

    1,当前逻辑分析

    按照当前版本的处理逻辑,所有对象类型会对被进行深层观测,数组也不例外

    let vm = new Vue({
      el: '#app',
      data() {
        return { message: 'Hello Vue', obj: { key: "val" }, arr:[1,2,3]}
      }
    });
    

    可以看到,数组中的每一项,都被添加了 get、set 方法,也就相当于实现了对数组的深层观测

    备注:Object.defineProperty支持数组数据类型的劫持

    2,Vue 对性能的权衡

    在 Vue2.x 中,不支持通过修改数组索引和长度的数据劫持;

    那么,为什么原本可以实现对数组索引的观测,Vue 却选择了不支持呢?

    主要是考虑了性能问题,比如,数组中的数据量非常大时:

    let vm = new Vue({
      el: '#app',
      data() {
        return { arr:new Array(9999) }
      }
    });
    

    这时,数组中 9999 条数据,将全部被添加 get、set 方法

    而这一套操作就比较费劲了:为了实现数组索引劫持,需要对数组中每一项进行处理

    还有就是,虽然数组能够通过 defineProperty 实现对索引更新劫持

    但在实际开发场景真的需要吗?似乎很少会使用 arr[888] = x 这种操作

    所以,权衡性能和需求,Vue 源码中没有采用 defineProperty 对数组进行处理

    当然,这也就导致了在 Vue 中无法通过直接修改索引、length 触发视图的更新

    3,数组的劫持思路

    核心目标是要实现数组的响应式:

    Vue 认为这 7 个方法能够改变原数组:push、pop、splice、shift、unshift、reverse、sort

    所以,只要对这 7 个方法进行处理,就能劫持到数组的数据变化,实现数组数据的响应式

    备注:这种实现思路,也直接导致了 vue2 修改数组的索引和长度不能触发视图更新

    梳理对象属性深层劫持的实现:

    • 数据观测入口:src/observe/index.js#observe方法
    • 如果数据为对象类型就 new Observer
    • Observer 初始化时,会遍历对象属性,逐一递归 Object.defineProperty

    数组也是对象,所以,要把数组的处理逻辑单独拆出来。即对 7 个变异方法进行重写

    // src/utils
    /**
     * 判断是否是数组
     * @param {*} val 
     * @returns 
     */
    export function isArray(val) {
      return Array.isArray(val)
    }
    // src/observe/index.js
    import { arrayMethods } from "./array";
    class Observer {
      constructor(value) {
        if(isArray(value)){
          // 对数组类型进行单独处理:重写 7 个变异方法
        }else{
          this.walk(value);
        }
      }
    }
    

    4,数组方法的拦截思路

    • 重写方法需要在原生方法基础上,实现对数据变化的劫持操作
    • 仅对响应式数据中的数组进行方法重写,不能影响非响应式数组

    所以,对响应式数据中数组这 7 个方法进行拦截,即优先使用重写方法,其他方法还走原生逻辑

    数组方法的查找,先查找自己身上的方法(即重写方法),找不到再去链上查(原生方法)

    5,数组方法重写的实现

    // src/Observer/array.js
    // 拿到数组的原型方法
    let oldArrayPrototype = Array.prototype;
    // 原型继承,将原型链向后移动 arrayMethods.__proto__ == oldArrayPrototype
    export let arrayMethods = Object.create(oldArrayPrototype);
    // 重写能够导致原数组变化的七个方法
    let methods = [
      'push',
      'pop',
      'shift',
      'unshift',
      'reverse',
      'sort',
      'splice'
    ]
    // 在数组自身上进行方法重写,对链上的同名方法进行拦截
    methods.forEach(method => {
      arrayMethods[method] = function () {
        console.log('数组的方法进行重写操作 method = ' + method)
      }
    });

    添加 new Observer 时,对数组方法重写的逻辑:

    // src/observe/index.js
    import { arrayMethods } from "./array";
    class Observer {
      constructor(value) {
        // 分别处理 value 为数组和对象两种情况
        if(isArray(value)){
          value.__proto__ = arrayMethods; // 更改数组的原型方法
        }else{
          this.walk(value);
        }
      }
    }
    

    测试数组方法的重写:

    数组的链:

    • array.proto:包含 7 个重写方法
    • array.proto.proto:原始方法

    6,数组方法拦截的实现

    // src/state.js#initData
    function initData(vm) {
        let data = vm.$options.data;
        data = isFunction(data) ? data.call(vm) : data;
        observe(data);	// 在observe方法中new Observer执行后,数组的原型方法已完成重写
        // 测试数组方法的拦截效果
        data.arr.push(666); 
        data.arr.pop()
    }
    

    • arrayMethods.push:会在数组自身找到重写的push方法,不会继续到链上查找,实现拦截
    • arrayMethods.pop:数组自身没找到重写方法,继续到链上找到原生pop方法

    四,结尾

    本篇主要介绍了 Vue 数据初始化流程中,数组类型的数据劫持,核心有以下几点:

    出于对性能的考虑,Vue 没有对数组类型的数据使用 Object.defineProperty 进行递归劫持,而是通过对能够导致原数组变化的 7 个方法进行拦截和重写实现了数据劫持

    下一篇,数据代理的实现

    到此这篇关于Vue数据代理的实现流程逐步讲解的文章就介绍到这了,更多相关Vue数据代理内容请搜索北冥有鱼以前的文章或继续浏览下面的相关文章希望大家以后多多支持北冥有鱼!

    《Vue数据代理的实现流程逐步讲解.doc》

    下载本文的Word格式文档,以方便收藏与打印。