Bootstrap

【Vue2.x 源码学习】第十篇 - 数组数据变化的观测情况

一,前言

上篇,主要介绍了对象数据变化的观测情况,涉及以下几个点:

  • 实现了对象老属性值变更为对象、数组时的深层观测处理;

  • 结合实现原理,说明了对象新增属性不能被观测的原因,及如何实现数据观测;

本篇,数组数据变化的观测情况(数组中,新增对象、数组、普通值的情况)

二,数组中,新增对象、数组、普通值的观测问题

1,问题分析

向数组 arr 中新增对象、数组、普通值,会触发数据更新吗?

let vm = new Vue({
  el: '#app',
  data() {
    return { arr: [{ name: "Brave" }, 100] }
  }
});

vm.arr.push({a:100});
vm.arr[2].a = 200;

截止至当前版本,针对数组类型的处理:

  • 重写了数组链上的方法,能够对引起原数组变化的 7 个原型方法进行劫持;

  • 对数组中的每一项递归调用 observe 进行处理,使数组类型实现递归观测;

由于 observe 仅处理对象类型,所以数组中的普通值不会被观测;

虽然已经实现了数组的数据劫持,但尚未实现数据劫持后的具体逻辑:

// src/Observer/array.js

let oldArrayPrototype = Array.prototype;
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)
    // 劫持到数组变化后,尚未实现处理逻辑
  }
});

所以,向数组中添加内容,是能够触发数据劫持的,但还没有实现劫持后的具体逻辑

在 Vue2.x 中,向数组中新增对象,及修改新增对象的属性,都是可以触发更新的;

2,思路分析

重写 push 方法逻辑:

由于 7 个方法的入参数量不一致,例如 push 可以传入多个参数

3,代码实现

当 push 的参数为对象类型时,需要再次进行观测

// src/observe/array.js

methods.forEach(method => {
  // 当前的外部调用:arr.push
  arrayMethods[method] = function (...args) {
    console.log('数组的方法进行重写操作 method = ' + method)
    // AOP:before 原生方法扩展... 
    // 调用数组原生方法逻辑(绑定到当前调用上下文)
    oldArrayPrototype[method].call(this, ...args)
    // AOP::after 原生方法扩展...

    // 数组新增的属性如果是属性,要继续观测
    // 哪些方法有增加数组的功能: splice push unshift
    let inserted = [];
    switch (method) {
      // arr.splice(0,0,100) 如果splice方法用于增加,一定有第三个参数,从第三个开始都是添加的内容
      case 'splice':  // 修改 删除 添加
        inserted = args.slice(2); // splice方法从第三个参数起是新增数据
      case 'push':    // 向前增加
      case 'unshift': // 向后增加
        inserted = args // push、unshift的参数就是新增
        break;
    }
    // 遍历inserted数组,看一下它是否需要进行劫持
  }
});

当 push 的参数为对象类型时,需继续对其进行观测;

问题 1

数组深层劫持的 observeArray 方法,在 Observer 类中

由于没有导出,在 src/observe/array.js 的 methods.forEach 中是访问不到的

Observer 类中也拿不到 vm,

所以为当前 this 添加自定义属性进行关联:value.__ob__ = this;

value:为数组或对象添加自定义属性__ob__ = this,

this:为当前 Observer 类的实例,实例上就有 observeArray 方法;

如此,便可在src/observe/array.js 的 methods.forEach 中,调用到 observeArray 方法实现数组的深层劫持;

// src/observe/index.js
class Observer {
  
  constructor(value) {
    // value:为数组或对象添加自定义属性__ob__ = this,
    // this:为当前 Observer 类的实例,实例上就有 observeArray 方法;
    value.__ob__ = this;

    if (isArray(value)) {
      value.__proto__ = arrayMethods;
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }
}

添加了__ob__后的数组,调用了 push 方法,所以能够通过__ob__属性获取到 ob

// src/observe/array.js

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 有值就是数组
  }
});

所以,当向数组 push 对象或数组时,会继续走 observeArray 方法,使对象或数组成为响应式

问题 2

运行会导致死循环

// src/observe/index.js

class Observer {

  constructor(value) {
    value.__ob__ = this;

    if (isArray(value)) {
      value.__proto__ = arrayMethods;
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }

  walk(data) {
    Object.keys(data).forEach(key => {
      defineReactive(data, key, data[key]);
    });
  }
}

在 Observer 类中,由于 value.__ob__ = this; 这段代码

value 如果是对象,会走到 this.walk(value); 方法,继续循环对象的属性,

这时,属性__ob__会被循环出来,而__ob__又是一个对象,且在这个对象上还有__ob__

所以,在 walk 循环中对属性__ob__做 defineProperty 后,它的值还是一个对象,就无限递归造成了死循环

value 是对象就会进入 walk 方法,循环 value 对象中的所有属性,

其中__ob__属性将被循环出来,而 __ob__ 就是当前实例,实际也是一个对象,会被继续观测,造成死循环

所以,这段代码不能这么写,即__ob__不能被遍历,否则遍历出来后就会被defineProperty,造成死循环;

冻结:属性冻结后只是不能被修改了,但还是能被遍历出来的

需要使用 defineProperty 定义__ob__ 属性,并将 __ob__ 属性配置为不可被枚举

// src/observe/index.js
class Observer {

  constructor(value) {
    // value.__ob__ = this;	// 可被遍历枚举,会造成死循环
    // 定义__ob__ 属性为不可被枚举,防止对象在进入walk都继续defineProperty,造成死循环
    Object.defineProperty(value, '__ob__', {
      value:this,
      enumerable:false  // 不可被枚举
    });
    
    if (isArray(value)) {
      value.__proto__ = arrayMethods;
      this.observeArray(value);
    } else {
      this.walk(value); 
    }
  }
}

再执行,问题解决:

三,结尾

又成功水了一篇,还剩 11 篇

本篇,主要介绍了数组数据变化的观测情况:

  • 实现了数组数据变化被劫持后,已重写原型方法的具体逻辑;

  • 数组各种数据变化时的观测情况分析;

至此,数据劫持就全部完成了

下一篇,数据渲染的流程