参考资料 ——《深入浅出 Vue.js》刘博文著

前四章,讲述变化侦测的笔记。

# 概述

Vue 的变化侦测属于 “推”,当状态变化时,Vue 可以知道是哪些状态变化了。当状态变化了,它会向所有依赖这个状态的视图(或者说组件 / DOM 节点)发出更新通知

在 Vue1.x 之中,Vue 对每个状态都进行依赖追踪,更新粒度相当的细,但是也带来了比较大的内存开销。

在 Vue2.x 之中,引入了虚拟 DOM 并且把更新粒度调整到了组件级别(比状态级别要粗),但是也大大降低了依赖数量及依赖追踪所消耗的内存。

尽管 Vue 调整到了组件级别,也仍然要比 React 更细:React 的做法是粗放的,它使用虚拟 DOM,若检查到该组件类型变化或数据变化,它会把以该组件为根的整个子树销毁重建或更新渲染。由于更新过程是同步的,于此同时也带来了性能问题,后来 React16 便引入了 React fiber 来解决这个问题。

至于为什么 Vue 不用,因为 Vue 更新 DOM 是走的异步更新队列(与 React 的 this.setState 异曲同工),暂时还没有出现严重的性能问题。

# 具体追踪

追踪对象变化的方式有二:

  • Object.defineProperty
  • Proxy(ES6)

在 Vue1.x 及 Vue2.x 之中,用的是前者。而在 Vue3.x 之中,用的是后者。

在 getter 中收集依赖,在 setter 中触发依赖。就是说,先收集依赖,知道某个属性都在哪些地方被用上,然后当属性发生变化时,通知这些地方进行更新。

# Dep—— 存储依赖的地方

既然说到依赖收集,那么我们就需要一个地方用来管理依赖。

Vue 之中封装了一个 Dep 类,专门用于管理依赖。这个类可以用来收集、删除依赖、或者向依赖发出通知。

# Watcher—— 依赖类型

依赖收集好了,当属性发生变化时,就要向对应的依赖发出通知。但是依赖有很多种,他们不是统一的,可能是开发者写的一个 watch,也可能是在模板里面用,为了统一这个依赖,我们抽象出一个处理这个问题的类,这个类就叫做 watcher

依赖收集阶段,我们只收集这个封装好的类的实例,通知也只通知这个实例,然后它再负责通知具体依赖。

export default class Watcher{
  constructor(vm,expOrFn,cb){
    this.vm = vm;// 对应的 vue 实例
    this.getter = parsePath(expOrFn);// 执行 this.getter (),可以读取 data.a.b.c 的内容
    this.cb = cb;// 更新时要调用的方法
    this.value = this.get();
  }
  get(){
    // 利用 window.target 作中转,触发对象属性的 getter,getter 中自然会将 window.target 收集到对应的 dep 实例
    window.target = this;
    let value = this.getter.call(this.vm,this.vm);
    window.target = undefined;
    return value;
  }
  update(){
    const oldValue = this.value
    this.value = this.get();
    this.cb.call(this.vm,this.value,oldValue);// 更新,传入新值和旧值
  }
}

当某个属性的 getter 被触发时,会执行 Watcher 实例的 get 方法进行依赖收集。

当某个属性改变(触发 setter 时),会将对应的 Dep 实例中收集到的依赖逐个通知,即通知 Watch 实例触发 update 方法。

# Observer—— 定义响应式对象

通过前面的 API,可以侦测到数据的变化,不过有时候会存在对象嵌套的现象,于是 Vue 封装一个 Observer 类。

这个类的作用是将一个对象内所有属性都转换成 getter/setter 的形式,然后追踪他们的变化

export class Observer{
  constructor(value){
    // 传入一个对象
    this.value = value;
    // 数组的监听解决方案下面会说
    if(!Array.isArray(value))this.walk(value);
  }
  /**
  * 当参数为 object 时被调用
  */
  walk(obj){
    const keys = Object.keys(obj);
    for(let i = 0;i<keys.length;i++){
      defineReactive(obj,keys[i],obj[keys[i]]);
    }
  }
}
function defineReactive(data,key,val){
  if(typeof val === 'object')new Observer(val);// 递归对象子属性
  let dep = new Dep();
  Object.defineProperty(data,key,{
    enumerable: true,
    configurable: true,
    get: function(){
      dep.depend();
      return val;
    },
    set:function(newVal){
      if(val === newVal)return;
      val = newVal;
      dep.notify();
    }
  })
}

当 data 中的属性发生变化时,这个属性对应的依赖就会收到通知。

而由于 Object.defineProperty 这个 API 本身的问题,无法追踪新增的属性和删除的属性,也无法追踪到数组的修改,而且它对于深层对象需要递归进行遍历,性能上不是那么的好。因此后续便使用的 ES6 的 Proxy 来进行替代。

# Vue2 中响应式的缺点及解决方案

# 缺点

Vue2 使用的是 Object.defineProperty 来进行数据劫持,也就是说,变化侦测的方式是通过 getter/setter 实现的

那么就会存在以下两个问题(刚刚也提到了):

  • 无法检测属性的添加与移除

    因为在初始化时 Vue 对每个属性进行 getter/setter 转化,初始化时不存在的话就无法转化,自然也不是响应式的。

  • 无法检测数组的变动,如使用 push/pop 方法改变,或者利用索引直接设置、修改数组长度之类的变动。

    因为这些方法并不会触发 setter,自然也就无从监听了。

# 解决方案

前者的解决方案较简单:Vue 提供了一个 Vue.set (object,propertyName,value) 方法用于向嵌套对象添加响应式属性

后者的解决方案就稍微复杂一点。Vue2 的解决办法是:将常用的数组方法进行重写,从而覆盖原生的数组方法 (push,pop,shift,unshift,splice,sort,reverse)

# 侦测数组的解决方案细节

像上面所说的一样,首先新建一个以 Array.prototype 为原型的对象(伪数组原型,下面我们称之为拦截器),然后将其覆盖到要定义为响应式数组的原型上

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
['push','pop','shift','unshift','splice','sort','reverse']
.forEach(function(method){
  // 缓存原始方法
	const original = arrayProto[method]
  Object.defineProperty(arrayMethods, method ,{
    value:function mutator(...args){
      return original.apply(this,args);
    }
    enumerable:false,
    writable:true,
    configurable:true
  });
})
import {arrayMethods} from './array';
const hasProto = '__proto__' in {};
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
export class Observer{
  constructor(value){
    this.value = value;
    
    if(Array.isArray(value)){
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
    }else{
      this.walk(value);
    }
  }
}
function protoAugment(target, src, keys){
	target.__proto__ = src;
}
function copyAugment(target, src, keys){
  for(let i = 0 ,l = keys.length;i<l;i++){
		const key = keys[i];
    def(target, key , src[key]);
  }
}

这里为什么不直接覆盖 Array.prototype 呢?因为我们不希望直接覆盖全局,尽可能的不向外污染内部的设计,所以使用覆盖响应式对象的原型来代替。

顺带一提,如果浏览器不支持_ _proto__来访问对象的原型,就直接将这些重写方法设置到被侦测的数组上(因为原型屏蔽的原因,只有对象自身不存在该方法才按照原型链向上查找)。

然后,还需要进行依赖收集。Array 在 getter 中收集依赖,在拦截器中触发依赖(当你调用方法修改数组时,实际上是调用拦截器的方法)。

在 Vue.js 中,Array 的依赖存放在 Observer 中(对象是存在定义响应式对象的那个函数中,即 defineReactive),然后将 Observer 实例挂载到对应数组实例上,拦截器就能够访问 Observer 实例中的 dep 了。

之所以存在那里,是为了让 getter 和拦截器都可以访问到依赖。

通过覆盖,我们就也能知道数组自身的变化了,不过光是这样还不够,对于数组之中的元素我们也需要进行侦测。于是 Vue 中还会进行一次循环,将数组的每个元素通过 Observer 进行转化。同时对新增的元素也进行转化。

# Vue3 中的更改

后来 Vue3 使用了 ES6 的 Proxy 进行重写响应式系统,不仅能够监听到对象属性的增加与删除,同时也能够监听数组的变化。

# 总结

总的来说,分为几个关键词:数据劫持(Vue3 是数据代理)、依赖收集、发布 / 订阅

  • 数据劫持。Vue 之中使用 Observer 类,把一个对象的所有属性转换成 getter/setter。

  • 依赖收集。

    简单来说,对象和数组都是在 getter 中进行依赖收集,但是对象在 setter 中触发依赖,而数组在拦截器中触发依赖。因此,对象将依赖保存在 defineReactive 中,而数组将其保存在 Observer 实例中,并且将该 Observer 实例存放到数组上。

    依赖收集在 Dep 之中,依赖的类型则是 Watcher 实例。当外界某个地方依赖某个数据,通过 Watcher 读取数据时。新建的时候触发对应数据的 getter,通过 window.target 作为中转,在触发数据 getter 时收集到 Dep 实例中。

  • 发布 / 订阅模式。依赖收集的时候建立数据 - Watcher 的对应关系其实就是订阅,当数据发生变化(数据的 setter 方法被调用),则会通过 dep.notify 通知对应的所有依赖(对应的所有 Watcher 实例),然后 Watcher 实例再通过自己的 update 方法通知外界进行更新。

前面说过,在 Vue1.x 之中,更新粒度是属性级别的,即一个属性对应一个 Watcher 实例。

但在 Vue2.x 之中,更新粒度是组件级别的,即一个组件实例对应一个组件级的 Watcher 实例 (实际上在组件内部用户可以通过 $watch 方法创建 watcher)。一个组件可能依赖很多数据,这些数据对应的 Dep 都收集了这个 Watcher 实例,在数据变化的过程中,只要一个数据变化,都会引起这个组件的重新渲染。

不同数据的 Dep 可能收录同一个 Watcher 实例,数据的 Dep 可收录多个 Watcher 实例。(多对多)

Vue变化侦测关系图

# Q&A

# 简述 Vue 的响应式原理?

主要是三个关键词,数据劫持、依赖收集、发布订阅模式。Vue 通过 Observer 类将对象转化成 getter/setter,将数组的原型用拦截器覆盖,都在 getter 中收集依赖存放到 Dep 实例中。对象在 setter 中通知依赖更新,而数组在拦截器中通知依赖。依赖就是 Watcher 实例,外界通过依赖读取数据,当数据发生变化时通知依赖,依赖再通知外界进行更新。

# Vue2 响应式原理的缺点?

Vue2 中使用的是 Object.defineProperty 进行数据劫持,这个 API 对对象的每个属性进行遍历转化,因此 Vue2 无法监听新增 / 删除的属性。同时该响应式基于数据改变触发 setter 的基础,而数组的修改方法不触发 setter,需要进行重写覆盖。

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

orange 微信支付

微信支付

orange 支付宝

支付宝

orange 贝宝

贝宝