【Vue】源码—数据响应式原理

240 阅读12分钟

源码链接:Vue2.x数据响应式原理

1.数据响应式原理

1.1什么是数据响应式?

【Vue—非侵入式】

当数据改变后,Vue会通知到使用该数据的代码。数据响应式原理的核心是采用了数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,通过Object.defineProperty()对数据进行拦截,把这些属性全部转换为getter/setter,get()方法可以读取数据、收集数据,set()方法可以改写数据,在数据变动时会对数据进行比较,如果数据发生了变化,会发布信息通知订阅者,更新视图。

1.2侦测数据变化的方法—Object.defineProperty()

该方法会直接在一个对象上定义一个新属性,或者修改一个对象现有属性,并返回对象。

功能:将属性转换为getter/setter的形式来追踪变化。

getter:读取数据会触发;收集观察者

setter:修改数据会触发,去通知getter中的依赖数据发生变化;通知观察者进行更新视图或处理事务。

Vue2中利用闭包将Object.defineProperty封装到defineReactive方法中,临时变量也存放在此方法中。

1.3实现defineReactive函数

实现数据劫持, 来实现监听数据的改变和读取(属性的getter和setter)

Object.defineProperty把数据转为getter和setter,并为每个数据添加一个订阅列表的过程。

此函数还需要用到observe函数以及Dep函数

observe:

【用于监听对象嵌套对象】

Dep:

【用于收集依赖】

defineProperty函数执行过程中新建了一个Dep,不需要设置临时变量,而是闭包在了属性的getter和setter中,因此每个属性都有一个唯一的Dep与其对应,当读取响应式对象的某个属性时,它会进行收集依赖。当改变某个属性时,他会派发更新

get: 属性的getter函数。当访问该属性时,会调用此函数。执行时不闯入任何参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值

set: 属性的setter函数。当属性值被改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值的this对象,默认为undefined

import observe from "./observe";
import Dep from "./Dep";
// 给对象obj定义一个响应式的属性
// obj: 传入的数据,key:监听的属性,value:闭包环境提供的周转变量
export default function defineReactive (obj, key, val) {
  // 每个数据都要维护一个属于自己的数组,用来存放依赖自己的watcher
  // 创建信息中心
  const dep = new Dep();
  // 当前传入参数的个数
  if(arguments.length == 2) {
    val = obj[key];
  }
  // 子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数,类循环调用
  let childOb = observe(val);
  // 可以使用Object.defineProperty去定义一些隐藏的属性
  // 对obj的key进行属性拦截
  // val的值相当于get和set这两个函数闭包中的环境
  // 闭包是一定要有内外两层函数嵌套,get、set是内层,defineReactive是外层
  Object.defineProperty(obj, key, {
    // 可枚举
    enumerable: true,
    // 可以被配置,比如可以被delete
    configurable: true,
    // 是否可写
    // writable: true
    // get和value不能同时使用
    // getter/setter 需要变量周转才能工作
    // 临时变量不是特别美观,可以封装到一个函数中,利用函数的闭包特性
    // 闭包就是函数外部的作用域
    // getter 获取数据
    get() {
      console.log('访问'+ key + '属性');
      // 如果现在处于收集依赖阶段
      // Dep.target就是Watcher实例
      // 数据变化时,通知添加订阅者
      if(Dep.target) {
        dep.depend();
        // 数组情况,判断有没有子元素
        if(childOb) {
          // 数组收集依赖
          childOb.dep.depend();
        }
      }
      return val;
    },
    // setter对变量的赋值
    set(newValue) {
      // 负责劫持
      console.log('修改' + key + '属性', newValue);
      // 如果传入的值相等就不用修改
      if(val == newValue) {
        return;
      }
      // 修改数据
      val = newValue;
      // 当设置了新值,这个新值也要被observe
      // 新值如果是对象,仍然需要递归遍历处理
      childOb = observe(newValue);
      // 触发依赖
      // 发布订阅模式,通知dep执行更新方法
      dep.notify();
    }
  });
}

上述代码用于实现数据劫持,

对象的响应式处理 ↓

对象是在getter中收集依赖,在setter中触发依赖

1.4 递归侦测对象全部属性object

1.4.1 实现observe函数:对象版

用于创建Observer实例

监听value尝试创建Observer实例,如果value已经是响应式数据,就不需要再创建Observer实例,直接返回已经创建的Observer实例即可,避免重复侦测value变化的问题

由于defineProperty函数不能监听到对象嵌套对象,所以我们要创建一个Observer类 —>将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object。

此函数还需要Observer函数

Observer:

【把一个object中的所有数据(包括子数据)都转成响应式,它会侦测object中所有的数据的变化】

import Observer from "./Observer.js";
0
export default function observe(value) {
  // 如果value不是对象,什么都不做(表示该递归到的是基本类型,其变化可被侦听的)
  if(typeof value != 'object')
  {
    return;
  }
  // Observer实例
  // 定义ob
  let ob;
  // 第一步是调observe(obj)来触发全部东西
  // 第二步是看obj身上有没有__ob__
  // 如果没有就会new Observer()
    /*
      将产生的实例,添加到__ob__上
    */
  // 遍历下一层属性,逐个defineReactive
    /*
      当设置某个属性值的时候,会触发set,里面有newValue,
      这个newValue也得被observe()一下
    */
  // __ob__是value上的属性,其值是对应的Observer实例(表示其已经是可侦听的状态)
  if(typeof value.__ob__ !== 'undefined') {
    // 已经侦听状态
    ob = value.__ob__;
  } else {
    // 是对象且该上属性还是未能够侦听状态
    // 此时构造器就会被执行
    ob = new Observer(value);
  }
  return ob;
}

1.4.2 实现Observer类

把一个object中的所有数据(包括子数据)都转成响应式,它会侦测object中所有的数据的变化。

对象的侦测属性还需要Dep函数、def函数、defineReactive函数的辅助【其余的函数需求是用于数组的侦测】

Dep:

【收集依赖】

def:

【给实例添加__ob__属性,值是Observer的实例,且不可枚举】

defineReactive:

【将object的所有数据转成响应式】

  1. __ob__的作用可以用来标记当前value是否已经被Observer转换为响应式数据了; 而且可以通过value.__ob__来访问Observer的实例
  2. walk:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object【通过循环遍历属性,使用defineReactive方法将属性变为响应式】
import { def } from './utils.js'
import defineReactive from './defineReactive.js';
import { arrayMethods } from './array.js';
import observe from './observe.js';
import Dep from './Dep.js';
// Observer 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
export default class Observer {
  // 构造器
  constructor(value) {
    // 每个Observer的实例,成员中都有一个Dep的实例
    // 将__ob__绑在dep上
    // 当数据被修改时,就会触发dep
    this.dep = new Dep();

    // 添加__ob__属性,实际上是不可枚举属性
    // 给实例this,一定要注意,构造函数中的this不是表示类本身,而是表示实例),给实例添加了__ob__属性,值是这次new的实例
    // 定义一个对象属性
    def(value, '__ob__', this, false);
    console.log('构造器', value);
    // 检查是数组还是对象
    if(Array.isArray(value)) {
      Object.setPrototypeOf(value, arrayMethods);
      this.observeArray(value);
    } else {
      // 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
      this.walk(value);
    }
  };
  // 遍历,遍历value里面的每一个key,让每一个key设置为defineReactive
  // 对于对象上的属性进行遍历,将其变为响应式
  walk(value) {
    // defineReactive被Observer的walk方法调用
    for(let k in value) {
      // 把value的k属性变成reactive
      // 通过这步操作,外层变成响应式
      defineReactive(value, k);
    }
  }
  // 数组的特殊遍历
  observeArray(arr) {
    for(let i = 0; i < arr.length; i++) {
      observe(arr[i]);     
    }
  }
}

1.4.3 实现def函数

用于定义一个对象属性

export const def = function (obj, key, value, enumerable) {
  Object.defineProperty(obj, key, {
    value,
    enumerable,
    writable: true,
    configurable: true
  });
};

1.5侦测对象全部属性-文件之间的依赖结构

数组的响应式处理 ↓

数组是在getter中收集依赖,在拦截器中触发依赖

1.6 递归侦测数组全部属性

Array追踪变化的方式和Object不一样。因为它是通过方法来改变内容。所以需通过拦截器去覆盖数组原型的方法来追踪变化。由于使用依赖位置不同,数组要在拦截器中向依赖发信息,所以依赖不能像Object那样保存在defineReactive中,而是把依赖保存在Observer实例中。

1.6.1 实现Observer类:数组版

  1. 在Observer中,我们对每个侦测变化的数据都标记__ob__,并把this(Observer实例)保存在__ob__上
  • 作用: 一方面是为了标记数据是否被侦测变化,另一方面方便通过数据取到__ob__,从而拿到Observer实例上保存依赖。当拦截到数组发生变化时,向依赖发送通知。
  1. 除了侦测数组自身的变化外,数组中的元素发生的变化也要侦测。我们在Observer中判断如果当前被侦测的数据是数组,则调用observeArray方法将数组中的每一个元素都转换为响应式的并侦测变化。
  2. Object.setPrototypeOf(obj, prototype):修改对象原型
  • obj要设置其原型的对象prototype该对象放入新的原型(一个对象或null)
  1. Object.create(): 创建一个新对象
  • 该方法创建一个新的对象,使用现有的对象来提供新创建的对象的__proto__

数组的侦测属性还需要observe函数以及arrayMethods函数的辅助

observe

【用于监听对象嵌套对象】

arrayMethods

自定义方法去覆盖原生的原型方法】

import { def } from './utils.js'
import defineReactive from './defineReactive.js';
import { arrayMethods } from './array.js';
import observe from './observe.js';
import Dep from './Dep.js';
// Observer 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
export default class Observer {
  // 构造器
  // 类需要思考如何被实例化
  constructor(value) {
    // 每个Observer的实例,成员中都有一个Dep的实例
    // 将__ob__绑在dep上
    // 当数据被修改时,就会触发dep
    this.dep = new Dep();

    // 添加__ob__属性,实际上是不可枚举属性
    // 给实例this,一定要注意,构造函数中的this不是表示类本身,而是表示实例),给实例添加了__ob__属性,值是这次new的实例
    // 定义一个对象属性
    def(value, '__ob__', this, false);
    console.log('构造器', value);
    // 检查是数组还是对象
    if(Array.isArray(value)) {
      // 如果是数组,要强行将这个数组的原型指向arrayMethods
      Object.setPrototypeOf(value, arrayMethods);
      // 让数组变得observe
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  };
  walk(value) {
      defineReactive(value, k);
    }
  }
  // 数组的特殊遍历
  observeArray(arr) {
    for(let i = 0; i < arr.length; i++) {
      /*
      * Observer类会附加到每一个被侦测的object上,一旦被附加,Observer会将object所有属性转换为getter/setter的形式
      */
      // 逐项进行observe
      observe(arr[i]);     
    }
  }
}

1.6.2 实现ArrayMethods函数

改写数组的七个方法(可以改变数组自身内容的方法),在保留原有功能的前提下,将其添加的项变为响应式,来实现对数组的响应式处理。

当用户使用push、unshift、splice方法向树祖中新增数据时,新增数据也要进行变化侦测。我们使用当前操作数组的方法来进行判断,如果是push、unshift、splice,则从参数中将新增数据提取出来,然后使用observeArray来新增数据进行变化侦测。

1.以arrayPrototype为原型(proto)创建一个arrayMethods对象,并将其暴露;

Object.create():创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

2.用ES6强制定义数组的原型指向arrayMethods:Object.setPrototypeOf(o, arrayMethods) / Object.create() / o.__proto = arrayMethods要被改写的7个数组方法,Vue通过改写数组的七个方法(可以改变数组自身内容的方法)来实现对数组的响应式处理

Object.defineProperty不能直接监听数组内部的变化,那么数组内容变化应该怎么操作?

Vue主要采用的是改装数组方法的方式(push,pop,shift,unshift,splice,sort,reverse), 在保留其原有功能的前提下,将其添加的项变为响应式

3.相当于一个拦截器覆盖Array.prototype,每当使用Array原型上的方法操作数组时,其实执行的是拦截器中提供的方法。

此函数还需要用到def函数

def

【用于定义新的方法】

import { def } from './utils.js';
/* 正因为可以通过Array原型上的方法来改变数组的内容,所以像对象那种通过getter/setter的实现方式就行不通
*  ES6之前没有提供可以拦截原型方法的能力,我们可以用自定义的方法去覆盖原生的原型方法
*/
// 得到Array的原型
const arrayPrototype = Array.prototype;
// 这七个方法定义在Array.prototype上,要保留方法的功能,同时增加数据劫持
console.log(arrayPrototype);
export const arrayMethods = Object.create(arrayPrototype);

const methodsNeedChange = [
//   // Vue底层改写这7个方法
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];
methodsNeedChange.forEach(methodName => {
  // 备份原来的方法
  const original = arrayPrototype[methodName];
  // 定义新的方法
  // 对象,名字,定义什么值
  def(arrayMethods, methodName, function() {
    // 恢复原来的功能
    const result = original.apply(this, arguments);
    // 把类数组对象变为数组
    const args = [...arguments];
    // 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?
    /*
    * 因为数组不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层
      的时候,已经给g属性(就是这个数组)添加了__ob__属性
    */
   // 数组不会是最外层,所以其上已经添加了Observer实例
   // 把这个数组身上的__ob__取出来
   // 在拦截器中获取Observer的实例
    const ob = this.__ob__;
    // 有三种方法push/unshift/splice能够插入新项,需把插入的新项也要变为observe的
    let inserted = [];
    switch(methodName) {
      case 'push':
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
        // splice格式是splice(下标, 数量, 插入的新项)
        inserted = args.slice(2);
    }
    // 查看有没有新插入的项inserted,有的话就劫持
    if(inserted) {
      ob.observeArray(inserted);
    }
    console.log('被修改啦');
    // 发布订阅模式,通知dep
    // 数据发生了变化,因此需向依赖发送信息
    ob.dep.notify();
    return result;
  }, false);
});

1.7 侦测数组全部属性-文件之间的依赖结构

2.发布订阅模式

发布-订阅是对象的一种一对多的依赖关系,当一个对象触发一个事件的时候,所有订阅该事件的对象将得到通知。在Vue2.x中数据响应式中,也是一个发布订阅模式

发布者:通过实践中心派发事件—observe

  • 执行了发布者的权利,增加了订阅者,并且改变时通知订阅者。

事件中心:负责存放事件和订阅者的关系—Dep

  • 管理真实订阅者Wacther

订阅者:通过事件中心进行事件的订阅—Watcher

  • 通过触发get,将watcher添加到key对应的Dep中

2.1收集依赖-Dep,依赖-Watcher

2.1.1为什么要收集依赖?

之所以要劫持数据,目的是当数据的属性发生变化时,可以通过哪些曾经用到的该数据的地方。

先收集依赖,把用到的数据的地方收集起来,等属性改变,在之前收集好的依赖中循环触发一遍。

总结:对象是在getter中收集依赖,在setter中触发依赖;而数组在getter中收集依赖,在拦截器中触发依赖

2.1.2如何收集依赖?

收集依赖到哪里? Dep

我们要在getter中收集依赖,将依赖收集封装成一个类Dep,这个类帮我们管理依赖可以进行收集依赖、删除依赖、向依赖发送通知。只有Watcher触发的getter才会进行收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中

Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有Watcher都通知一遍。

let uid = 0;
// 定义依赖关系,收集依赖的核心
export default class Dep {
  constructor() {
    console.log('我是Dep的构造器');
    this.id = uid++;
    // 用数组存储自己的订阅者,放的是Watcher的实例
    this.subs = [];
  }
  // 添加订阅: 该方法将订阅者添加到subs中对应的数组中
  addSub(sub) {
    // sub:订阅者,当信息发生时被通知的对象

    this.subs.push(sub);
  }
  // 添加依赖
  // 将dep实例添加到当前的订阅者中(这个过程中也会将当前的订阅者添加到dep的订阅者列表中)
  depend() {
    // target是一个全局静态属性,可以理解为当前的目标Watcher,也就是当前的订阅者
    // Dep.target=>类的属性,并不是Dep实例的属性,就是一个我们自己指定的全局的位置,用window.target也行,只要是全局唯一,没有歧义就行
    if(Dep.target) {
      // 在dep的订阅者数组中存放了Dep.target,让Dep.target订阅dep
      // Dep.target就是Watcher实例
      // Why 为什么要存放在Dep.target?
      /* 举例: {{a+b}}
      * 因为getter函数并不能传参,dep可以通过闭包的形式放进去,watcher不行,watcher内部存放了a+b这个表达式,
      * 也是由watcher计算a+b的值,在计算前他会把自己放在一个公开的地方(Dep.target),然后计算a+b,从而触发
      * 表达式中所有遇到的依赖getter,这些getter执行过程中会把Dep.target加到自己的订阅列表中。等整个表达式
      * 计算成功,Dep.target又恢复null。这样就成功的让watcher分发到了对应依赖的订阅者列表中,订阅到自己所有依赖。
      */
      this.addSub(Dep.target);
    }
  }
  // 通知所有的订阅者进行更新
  notify() {
    console.log('Notify');
    // 浅克隆一份
    const subs = this.subs.slice();
    // 遍历
    for(let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
  // 从数组中删除元素item
  remove(arr, item) {
    if(arr.length) {
      const index = arr.indexOf(item);
      if(index > -1) {
        return arr.splice(index, 1);
      }
    }
  }
}
依赖是什么? Watcher

Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方

import Dep from "./Dep";

/*
*功能:
   实例化Watch时,往dep中添加自己
   当数据变化触发dep,dep通知所有对应的Watch实例更新视图
*/
let uid = 0;
// Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方
export default class Watcher {
  constructor(target, expression, callback) {
    // target 监听哪个对象实例,expression哪个对象怎样的表达式【订阅的属性名】,callback数据变化后要执行的回调
    console.log("我是watcher类的构造器");
    this.id = uid++;
    // 触发getter前,将当前订阅者实例村春给Dep类
    this.target = target;
    // 把表达式按点来拆分,执行this.getter()就可以读取data.a.b.c的内容
    this.getter = parsePath(expression);
    this.callback = callback;
    // 记录属性更改之前的值,用于进行更新状态检测(导致属性的getter的触发)
    this.value = this.get();
  }
  update() {
    this.run();
  }
  get() {
    // 进入依赖收集阶段,让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
    Dep.target = this;
    const obj = this.target;
    var value;
    // 只要能找到,就一直找
    try {
      value = this.getter(obj);
    } finally {
      // 操作完毕后清除target,用于存储下一个Watch实例
      Dep.target = null;
    }
    return value;
  }
  run() {
    this.getAndInvoke(this.callback);
  }
  getAndInvoke(cb) {
    const value = this.get();
    if(value !== this.value || typeof value == 'object') {
      const oldValue = this.value;
      this.value = value;
      // 调用Watcher实例的时候传递过来的回调函数,并且确定它的this指向this.target
      cb.call(this.target, value, oldValue);
    }
  }
};
// 将str用.分割成数组segments,然后循环数组,一层一层去读取数据,最后拿到的obj就是str中想要读的数据
function parsePath(str) {
  var segments = str.split('.');
  return(obj) => {
    for(var i = 0; i < segments.length; i++) {
      if(!obj) return;
      obj = obj[segments[i]];
    }
    return obj;
  }
}

2.2应用场景

  • DOM操作中的addEventListener
  • Vue中的事件总线的概念
  • Node.js中的EventEmitter以及内置库

2.3优点

  • 模块间进行耦合,不强关联与特定的其他模块,只需订阅相关事件即可
  • 异步编程中,代码可以松耦合。

2.4缺点

  • 松耦合弱化对象间的关系,debug时程序可能难以追踪