【vue2原理】-第三篇,数组的劫持

63 阅读4分钟

数组的单层劫持

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

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

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

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

这时,为了实现数组索引劫持,需要对数组中每一项进行处理,实现了对数组的深层观测;

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

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

2,数组的劫持思路

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

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

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

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

  1. 数据观测observe 方法,

  2. 如果数据为对象类型就 new Observer

  3. Observer 初始化时,会遍历对象属性,逐一递归 Object.defineProperty

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

// 判断是否是数组
export function isArray(val) {  
    return Array.isArray(val)
}

import { arrayMethods } from "./array";

class Observer {  
    constructor(value) {    
        if(isArray(value)){      
            // 对数组类型进行单独处理:重写 7 个变异方法    
        } else {      
            this.walk(value);    
        }  
     }
 }

3,数组方法的拦截思路

  • 重写方法需要在原生方法基础上,实现对数据变化的劫持操作

  • 仅对响应式数据中的数组进行方法重写,不能影响非响应式数组

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

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

4,数组方法重写的实现

// 拿到数组的原型方法let 
oldArrayPrototype = Array.prototype;// 原型继承,将原型链向后移动 
arrayMethods.__proto__ == oldArrayPrototypeexport 
let arrayMethods = Object.create(oldArrayPrototype);

// 重写能够导致原数组变化的七个方法let 
methods = [  'push',  'pop',  'shift',  'unshift',  'reverse',  'sort',  'splice']
// 在数组自身上进行方法重写,对链上的同名方法进行拦截
methods.forEach(method => {
  arrayMethods[method] = function (...args) {
    oldArrayPrototype[method].call(this, ...args)
    let inserted = null;
    let ob = this.__ob__;	// 通过 __ob__ 属性获取到 ob
    switch (method) {
      case 'splice': 
        inserted = args.slice(2);
      case 'push':  
      case 'unshift':
        inserted = args 
        break;
    }
    
    // observeArray:内部遍历inserted数组,调用observe方法,是对象就new Observer,继续深层观测
    if(inserted)ob.observeArray(inserted);// inserted 有值就是数组
  }
});

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

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:原始方法

5,数组方法拦截的实现

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 方法

数组的深层劫持

1,代码实现

通过以上分析,实现数组的深层劫持,需要处理两种情况:

  1. 数组中嵌套数组
  2. 数组中嵌套对象
class Observer {
  constructor(value) {    
      if (isArray(value)) {     
          value.__proto__ = arrayMethods;      
          this.observeArray(value);	// 对数组数据类型进行深层观测    
      } else {      
          this.walk(value);    
      }  
  }
  
  /**   
  * 遍历数组,对数组中的对象进行递归观测  
  * 1)[[]] 数组套数组  
  * 2)[{}] 数组套对象   
  */  
  observeArray(data) {    
      // observe方法内,如果是对象类型,继续 new Observer 进行递归处理    
      data.forEach(item => observe(item))    
  }
}

observeArray 方法:为数组中的每一项,调用 observe 进行深层观测处理

这样,数组中的数组 [ [] ]、数组中的对象 [ {} ] ,两种情况都实现了数据的深层观测

2,问题分析

由于仅重写了数组的部分原型方法,未对每一项进行数据劫持

所以,数组中的普通值,不能被观测;(仅重写数组方法,递归中对值类型不再处理)

数组中的引用类型,能够被观测;(observe 实现对象类型深层观测)

举例分析:

  1. 若 vm.arr[0] 为普通值:(仅重写数组方法,递归中对值类型不再处理)

vm.arr[0] = 100 操作数组索引,不会触发视图更新(没对数组索引观测)

  1. 若 vm.arr[0] 为对象:(observe 实现对象类型深层观测)

vm.arr[0].a = 1 修改对象属性,会触发视图更新