Vue2数据响应式原理深度解析( 三 )

1,002 阅读8分钟

Vue2数据响应式原理逻辑深度解析(三)

前言

大家好,欢迎来到今天的学习,在上一篇文章当中我们实现了关于对象绑定响应式的操作,今天我们来实现关于如何对数组内部的元素及数组内部新添加的元素进行响应式绑定。

目标

利用模块式编程,通过多个文件之间相互调用,文件内部的函数等方法嵌套循环使用来实现数组相关的响应式绑定

在这个系列中,有很多地方需要创建方法和文件,初学者可能一开始不知为什么这样做,等慢慢深入了解就能够把逻辑衔接起来了

在写逻辑代码实现功能之前,我们要知道我们现在具体要实现什么。第一,当访问数组时,会触发相应的get函数,这在上一篇已实现; 第二,我们操作数组的元素时,要对七个方法进行改写,例如当我们往原数组里追加数组时,那这个子数组也应该设置为响应时;第三,当我们把数组的其中一个元素设置为一个新数组时,这个新数组也应该设置为响应式

实现步骤

首先,我们在对象obj里设置一个数组g

let obj = {
  g: [22, 33, 44, 55]
}

接着我们要对obj及其内部的对象g来添加响应式,我们可以先把具体的流程走一走

梳理执行顺序

首先让这个obj进行响应式绑定

image.png

然后这个obj会经过observe.js --> Observer.js --> utils.js -->defineReactive.js , 此时在defineReactive.js文件中,传递进去的data就是obj本身,key是属性g,因此这时候的val就是一个数组,接着这个数组会被observe一下,然后再次进入到Observer.js文件 , 此时我们必须要对传进来的value进行判断,如果为数组,我们要将这个数组的原型指向一个arrayMethods里的所有属性

注意此时obj对象和内部的数组g身上都追加了一个 __ob__的属性

因此在Observer.js对传值的判断需要重写一下,如图

image.png

从图中可以看到,我们走第二轮时,因为此时value是一个数组,于是执行 if 判断语句内部的 Object . setPrototypeOf (... , ...) 的arrayMethods方法,这个方法会将数组的原型指向这个arrayMethods。

创建array.js文件

我们现在还没有 arrayMethods这个对象,于是我们需要创建一个array.js的文件,并向外暴露arrayMethods对象

image.png

向外暴露arrayMethods对象

image.png

接着,那这个arrayMethods到底是什么呢?其实它是一个原型为数组原型的一个对象,此时我们需要在上面声明一个新变量arrayPrototype来存放数组的原型。arrayPrototype,顾名思义,数组的原型于是,在这个对外暴露的arrayMethods上面,我们写上

image.png

代码

// 得到Array.prototype
const arrayPrototype = Array.prototype;

这时候我们就把数组Array的原型整体赋值给了变量arrayPrototype,于是接下来我们就可以利用Object.create(...)的方法,将这个arrayMethods的原型指向这个arrayPrototype

代码

// 这里是把arrayMethods这个变量的原型替换成Array.prototype 并暴露
// 注意这里在导出arrayMethods之前,会将这里该执行的代码执行完再做暴露
export const arrayMethods = Object.create(arrayPrototype)

稍后我们将会对这个arrayMethods进行打印,来查看其原型是否指向数组原型。

开头我们说到要对数组的七个方法进行改写,我们先声明这个需要被改写的七个数组的方法

// 需要被改写的七个数组方法
const methodsNeedChange = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

接着往下我们要做一个稍显复杂的forEach遍历,遍历的数组就是上面的methodsNeedChange,在这个forEach遍历里面,我们首先要完成下面几件事

  • 依次备份原来数组的方法
  • 为 arrayMethods对象内部追加属性,属性就为methodsNeedChange中的元素

先把这个大的循环遍历的框架写出来

methodsNeedChange.forEach(methodName => {

    // 备份原来的方法,因为push、pop等7个函数的功能不能被剥夺
    const original = arrayPrototype[methodName];
    
});

此时每经过一轮遍历,这个original就保存了一份真实有效的方法,当然这些方法的属性名是methodsNeedChange数组内的。接着我们要调用一下Dep.js内的方法,来为arrayMethods属性内部添加属性和方法

因此我们在头部引入def

image.png

接着在这个foreEach内部调用dep

methodsNeedChange.forEach(methodName => {
  // 备份原来的方法
  const original = arrayPrototype[methodName]

  // 定义新的方法
  // 注意此时并没用执行下面的function 只是给arrayMethods这个空对象添加方法而已
  def(arrayMethods, methodName, function () {
    // 恢复原来的功能
    const result = original.apply(this, arguments)
    // console.log(arguments)

    // ob.dep.notify()
    return result
    // console.log(arguments)
  }, false)

如此一来,我们就在这个arrayMethods身上添加了这七个方法了

注意此时我们给这七个属性添加的是方法,而后期一旦执行这七个方法中的一个,就会去执行original.apply(...)

接着我们打印一下这个obj.g这个数组

image.png

打印结果

image.png

此时就能看到这个数组的原型已经被覆盖,因为上面提到过Object . setPrototypeOf (value , arrayMethods),这个方法用来改变对象的原型,value就是传进来的数组obj,指向引进来的arrayMethods对象

这时候再来打印array.js中的arrayMethods

image.png

打印结果

image.png

可以看到它的属性和原型都已被添加

我们再来看一下操作原数组是否能够生效,回到index.js

image.png

打印结果

image.png

结果显示正常,数组可以正常使用它原型上的方法

实现数组内新添加元素的响应式

我们现在要解决一个问题,就是当往这个数组里添加数组或者对象的时候,怎么样能保证这个新内容也是响应式的呢?很显然我们现在的逻辑代码做不到

我们来思考一下怎么样实现这个功能,首先我们要在这个foreEach中的dep里获得当前数组的__ob__这个属性,接着要声明放一个数组inserted,这个inserted就是来存放我们往目标数组内添加的元素,接着我们要判断当前数组到底用的是什么方法,再来执行内部,于是methodsNeedChange内部的完整代码就呼之欲出了

methodsNeedChange.forEach(methodName => {
    // 备份原来的方法,因为push、pop等7个函数的功能不能被剥夺
    const original = arrayPrototype[methodName];
    // 定义新的方法
    def(arrayMethods, methodName, function () {
        // 恢复原来的功能
        const result = original.apply(this, arguments);
        // 把类数组对象变为数组
        const args = [...arguments];
        // 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?因为数组肯定不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层的时候,已经给g属性(就是这个数组)添加了__ob__属性。
        const ob = this.__ob__;

        // 有三种方法push\unshift\splice能够插入新项,现在要把插入的新项也要变为observe的
        let inserted = [];

        switch (methodName) {
            case 'push':
            case 'unshift':
                inserted = args;
                break;
            case 'splice':
                // splice格式是splice(下标, 数量, 插入的新项)
                inserted = args.slice(2);
                break;
        }

        // 判断有没有要插入的新项,让新项也变为响应的
        if (inserted) {
            ob.observeArray(inserted);
        }

        return result;
        
    }, false);
});

这里需要注意一点,argument是伪数组,我们不能使用正常数组的方法来操作它,再一个就是splice也可以为数组内部追加元素,因此这里的slice方法可以锁定slice方法内部的第三个元素,也就是追加的新项,最终这个新项也给响应式一下

切记,当传进来的是简单数据类型的时候,argument是[ ... ], 如果是复杂数据类型,则argument是[ [...] ]或[ { ... } ] 等,然后给inserted,再调用ob身上的observeArray的方法,这个方法是干嘛的呢?

完善Observer.js文件

这个方法就是用来给数组内部追加的新元素添加响应式的,我们要把这个数组定义在Observer的实例身上,也就是__ob__ , 所以我们回到Observer.js的walk方法的下面,循环遍历这个特殊的数组

    // 遍历
    walk(value) {
        for (let k in value) {
            defineReactive(value, k);
        }
    }
    // 数组的特殊遍历
    observeArray(arr) {
        for (let i = 0, l = arr.length; i < l; i++) {
            // 逐项进行observe
            observe(arr[i]);
        }
    }

大家可以在这里把流程稍微走一下,如果在是新元素内部添加的是数组,则到这里就会给这个新元素追加响应式 ; 如果添加的是简单数据类型,则走完这里在observe.js文件中直接会被return 出去,因为简单数据类型本身不需要被响应式

至此array.js文件内部已经相对完善

总结

关于数组内追加新元素运用数组方法并使之变为响应式的过程当中,要理清以下几点

  1. Object.setPrototypeOf(..., ...)方法的运用
  2. Object.create(...)方法的运用
  3. slice(...)方法在其中的作用
  4. forEach大循环内部的代码逻辑

今天的内容先到这里,在后面文章当中我们将讲解收集依赖和Watcher和Dep类的运用

本节代码

由于文件数量及大小限制,暂无法存入网盘,需要的评论区滴滴