vue源码之Array

目录

  • Vue中对Array和Object追踪方式的不同之处?
  • 拦截原型
    • 创建拦截器
    • 如何拦截
  • 响应式具体实现
    • 依赖收集到哪里?
    • 如何收集?
    • 通知依赖
  • 数组子集和新增元素的追踪
    • 数组子集转换成响应式
    • 新增元素转换成响应式
  • Array中的问题

Vue中对Array和Object追踪方式的不同之处?

Object通过setter改变属性的值,所以我们利用getter时发送依赖收集,在setter时触发依赖更新,而且Vue将数据转换成响应式数据是在数据初始化时,对Object中之后的属性新增和删除操作,无法做到自动更新,而是通过vm. s e t 和 v m . set和vm. setvm.delete手动转换成响应式,并立即发出更新通知。

但是,一般在对数组的操作中,可以改变数组自身内容的方法有push、pop、shift、unshift、splice、sort、reverse七个。当我们给一个数组类型的属性赋值时,属性的setter函数会触发,从而通知更新。但是在使用push等一系列操作方法时,由于ES6之前,JS没有元编程能力,没有提供可以拦截原型方法的能力,所以,我们思考,如果能在用户使用这些方法操作数组时得到通知,那就达到了追踪的目的。

拦截原型

如何做到在操作这些原型方法时能得到通知呢?

对!就是拦截原型。

基本原理:用一个拦截器覆盖Array.prototype, 每当使用原型上的方法操作数组时,实际上执行的都是拦截器提供的方法,在拦截器中除了调用原生的方法操作数组外,还可以干点别的事,比如:通知依赖更新!

创建拦截器

  1. 编写拦截器
  • 定义需要拦截的原型方法集合 methodsToPatch
  var arrayProto = Array.prototype;
  // 创建一个新的空对象arrayMethods,并将原型指向Array.prototype
  var arrayMethods = Object.create(arrayProto);

  var methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
  ];
  • 方法重写
methodsToPatch.forEach(function (method) {
    // 缓存原始方法
    var original = arrayProto[method];
    // 方法重写,屏蔽了Array.prototype上的方法,同时内部调用Array.prototype上的原始方法
    def(arrayMethods, method, function mutator (...args) {

      var result = original.apply(this, args);
      
      // 占位符D1 :这里可以做些事:比如通知依赖更新...
      
      return result
    });
  });

function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
      value: val,
      enumerable: !!enumerable,
      writable: true,
      configurable: true
    });
  }
  1. 拦截Array.prototype

拦截实质上运用了JS中 [[Prototype]]机制,在对象自身上查不到属性和方法引用时,引擎就会继续在[[Prototype]]关联的对象上进行查找,直到顶层Array.prototype

so! 我们可以在Array类型的数据上定义这些方法,从而拦截了使用Array.prototype上的原生方法。然而为每个需要追踪的数据都添加这七个方法,实在是繁琐。但是,我们有了原型链查找这种思想,可以轻松的实现委托。

即:可以通过将数据的原型(可以通过__proto__访问)直接关联到拦截器对象arrayMethods上,实现拦截。

现在,我们访问某个数组(如:list )的push方法时,它的查找顺序是:

list自身——>arrayMethods ——> Array.prototype

注意:
对于那些不支持__proto__的浏览器,我们只能手动的为每个数据添加方法了。

  • 我们仅对需要追踪的数据进行拦截,因此数据初始化时,在Observer中增加对数组的响应式转换。
 const hasProto = '__proto__' in {}
 const arrayKey = Object.getOwnPropertyNames(arrayMethods)
    
var Observer = function Observer (value) {
    this.value = value;
    this.dep = new Dep();
    def(value, '__ob__', this);
    
    // 集中看这一段
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      // 接下来就要讲这个
      this.observeArray(value);
      
    // 结束 
    } else {
      this.walk(value);
    }
  };
  function protoAugment (target, src) {
    target.__proto__ = src;
  }
  function copyAugment (target, src, keys) {
    for (var i = 0, l = keys.length; i < l; i++) {
      var key = keys[i];
      def(target, key, src[key]);
    }
  }

再说明this.observeArray(value);做了啥之前,我们先搞清楚如何收集跟数据相关的依赖(也就是视图中对应的坑{{}})以及操作数据时是如何通知依赖的呢?

响应式具体实现

如何收集依赖?

先看下我们访问一个数组的形式,如:
this.list
这一定会触发listgetter属性
所以我们依然在getter中收集数组依赖,在拦截器中触发依赖

依赖收集列表Dep

Object侦测中,依赖的收集、存放、通知都集中在defineReactive函数中。然而,数组通知依赖必须在拦截器中实现,所以,我们将收集的依赖集合放在Observer中,并将每一个数据实例化的Observer挂载到它的__ob__属性上。

在拦截器中,通过this.__ob__.dep就可以访问所有的依赖。

  • Observer 函数中
// this 指向Observer实例对象
this.dep = new Dep();
def(value, '__ob__', this);

__bo__的作用:

  1. 可以在拦截器中访问Observer实例
  2. 标记当前value是否被Observer转换成响应式数据
收集依赖

同理,我们通过拿到数据的Observer实例,进而拿到dep列表,进行依赖存储。

在getter中收集依赖
便于理解,我们举个例子:

{
    name: '',
    obj: {
        list: [...]
    }
}
  • defineReactive 函数
// key: list  val: [...]
function defineReactive (obj, key, val) {
    var dep = new Dep(); // 用于Object类型的数据
    
    // 1. 返回val的Observer实例,可以拿到dep列表
    var childOb = observe(val);
    
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function () {
        if (Dep.target) {
          // Object的依赖收集
          dep.depend(); 
          //2. Array的依赖收集
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function (newVal) {
        if(val === newVal) return
        val = newVal;
        observe(newVal);
        dep.notify();
      }
    });
  }
  
 function dependArray (value) {
    for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
      e = value[i];
      e && e.__ob__ && e.__ob__.dep.depend();
      if (Array.isArray(e)) {
        dependArray(e);
      }
    }
  }
  • observe函数, 将数据Observer实例化(变成响应式的数据),并返回
 function observe (value) {
    if (!isObject(value)) {
      return
    }
    var ob;
    // 判重 如果value具有'__ob__'属性,说明已经是响应式数据
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
    } else if (
      (Array.isArray(value) || isPlainObject(value)) &&
      Object.isExtensible(value) &&
      !value._isVue
    ) {
      ob = new Observer(value);
    }
    return ob
}

通知依赖

通过前面的赘述,在拦截器中我们可拿到数据的this.__ob__.dep

在拦截器方法重写代码中的占位符D1处添加依赖通知:

 var ob = this.__ob__;
 ob.dep.notify() //向依赖发送消息

数组子集和新增元素的追踪

除了对数组本身的操作进行追踪外,还要对数组的子集(如 ["a", {name: 'xxx'}, 3])以及通过push、unshift、splice操作新增的数组元素进行响应式绑定。

侦测数组中元素的变化

还记得之前那个this.observeArray(value)吗?对,它的作用就是循环数组中的每一项,执行 observer函数,来侦测变化。

 Observer.prototype.observeArray = function observeArray (items) {
    for (var i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
}

observe 会将数据转换成响应式的。其实是递归调用了new Observer()

侦测新增元素的变化

思路:拿到新增的元素,并使用Observer侦测它

在方法调用函数中来拿到新增元素,还记得拦截器方法重写代码中的占位符D1吗?在这里添加:

    var ob = this.__ob__;
      var inserted;
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args;
          break
        case 'splice':
          inserted = args.slice(2);
          break
      }
      if (inserted) {
        ob.observeArray(inserted);
      }

同理,拿到新增元素后,调用observeArray方法进行响应式绑定。

Array中的问题

正式由于Array的响应式是通过拦截原型方式实现的,所以对于数组的某些操作,Vue是拦截不到的。

例如:

  1. this.list[0] = 2
  2. this.list.length = 0

在未来的Vue中,可能的解决方案是Proxy。

你可能感兴趣的:(Vue)