手写 Vue2 数组方法响应式,深度手写刨析 vue2 底层原理第二天

91 阅读4分钟

第一天我们手写了 vue 里面 data 里面的属性进行响应式,但是我们并没有对 data 里面的数组属性做响应式,接下来我们就开始做数组的响应式

首先我们要知道 vue2 数组响应式只是用数组的一些原生方法才能实现响应式,而不能通过索引来实现响应式,因为考虑到用户很少通过数组索引操作数组,而且要循环数组观测数据很消耗性能,所以 vue2 只对数组常用的方法进行拦截观测,那么 vue2 是如果在原生数组方法上进行拦截呢?

第一步:先判断属性是不是数组,打开 observer/index.js

    class Objserver {
        constructor(data) {
            // 判断
            if(Array.isArray(data)) {
                // 让数组的原型链改成我们重新方法过后的原型链
                data.__proto__ = arrayMethods
            }else {
                this.walk(data)
            }
         }
    }

第二步: 新建 observer/array.js 文件,我们在里面重写数组原生的方法,本质上不是重写,而是用户调用数组方法的时候,我们拦截做了一些操作之后在调用原生的数组方法

    let oldArrayPrototype = Array.prototype
    // 先复制数组的原型链
    export let arrayMethods = Object.create(oldArrayPrototype)
    
    let methods = ['push', 'shift', 'unshift', 'pop', 'reverse', 'sort', 'splice']
    
    old.forEach((methods) => {
        // 用户调用的如果是以上七个方法,会用我自己重写的方法,否则用原来数组的方法(复制了原型链数组的其它方法正常使用,只是对上面 7 个进行了拦截改写)
        arrayMethods[methods] = function(...args) {
            
            // .......这里进行改写
            
            // 最后调用原生的数组方法
            oldArrayPrototype.call(this,...args)
        }
    })

做到这里,我们想想好像还漏了什么,那就是对数组里面的对象进行观测,虽然我们不对数组里面的内容响应式,但是我们总得要让数组里面的对象实现响应式吧

第三步:对数组里面的对象进行响应式

     class Objserver {
        constructor(data) {
            // 判断
            if(Array.isArray(data)) {
                // 让数组的原型链改成我们重新方法过后的原型链
                data.__proto__ = arrayMethods
                // 对数组里面的数据是对象类型,需要监控对象的变化
                this.observeArray(data)
            }else {
                this.walk(data)
            }
         }
         observeArray(data) {
             // item 是数组的每一项,我们只需要拦截数组里面每一项的对象进行拦截观测,意思就是要把数组里面的对象变成响应式的,而不需要对数组进行拦截观测
             data.forEach((item) => {
                 // 如果是对象我们就进行拦截观测,数组的话也一样要递归
                 observe(item)
             })
         }
    }

接下来我们该做什么呢?虽然我们观测拦截了数组对象里面的数据,但是还没有对方法新增的数据做拦截观测,比如我 push 了一个对象,那么这个对象也要拦截观测变成响应式

第四步:对数组新增的数据对象做拦截观测

        // 打开 observer/array.js 文件
        methods.forEach((method) => {
          // 用户调用的如果是以上七个方法,会用我自己重写的方法,否则用原来数组的方法
          arrayMethods[method] = function (...args) {
              // 调用原来数组的原生方法
              oldArrayPrototype[method].call(this, ...args)
              // 用于保存新增的数据
              let inserted;
              // 从数据里面拿到 Observer 实例 
              let ob = this.__ob__
              switch(method) {
                  case 'push':
                    inserted = args
                    brack;
                  case 'unshift': 
                    inserted = args
                    brack;
                  case 'splice':
                    // 因为 splice 可以新增或替换数据,我们切割拿到第二个以后的参数就是新增或替换的数据
                    args.slice(2)
                    inserted = args
                    brack;
                  default:
                    brack;
              }
          }
          // 如果有新增或替换的数据,调用 Observer 实例的 observeArray 方法对新增或替换的对象变成响应式
          // 还是那句话,数组里面每一项的值为对象才进行观测,数组本身不需要观测,因为本来数组就不需要响应式
        if(inserted) ob.observeArray(inserted)
       })
       
       // 打开 observer/index.js 文件
       class Observer {
          // 对 data 中的所有属性进行数据劫持
          constructor(data) {
          
            为了放在栈溢出,我们把这个属性设为不可枚举的,然后有人读取这个属性的时候,我们就返回实例
            Object.defineProperty(data,'__ob__',{
                value: this,
                // 不可枚举
                enumerable: false
            })
          
            // 把实例保存到 data 上,构造函数的 this 都指向实例
            // 但是会出现死循环,因为是对象的话会一直递归,所以这种方法不可行
            // data.__ob__ = this
            if (Array.isArray(data)) {
            ......
            }
            ......
          }
          ......
       }
       

总结一下:

  1. 如果数据是对象,会将对象不停的递归,进行劫持
  2. 如果是数组,会劫持数组的方法,并对数组中不是基本数据类型的进行检测