【Vue源码】数据响应式原理 - 依赖收集 - defineReactive - Observer - Dep - Watcher

754 阅读10分钟

我们在使用Vue时,只需要修改数据,视图就会自动更新,这就是数据响应 今天来学习Vue实现数据响应式的原理~

源码链接 gitee.com/ykang2020/v…

前置知识 Object.defineProperty() 可以参考之前的博文 【JS】JavaScript对象属性-属性类型-数据属性-访问器属性-Object.defineProperty()方法-get方法-set方法

gettersetter方法可以对数据进行监听,访问和设置都会被监听捕获 读取数据的时候会触发getter,而修改数据的时候会触发setter

1. 定义 defineReactive 函数

1.1 Why (临时变量)

我们要进行数据劫持,先想到的就是Object.defineProperty()中给属性添加gettersetter方法,但是这么做有点问题~

defineProperty() 方法需要临时的全局变量周转gettersetter

我们来看下面这个例子

let obj = {};
let temp;

Object.defineProperty(obj, "a", {
  get() {
    console.log("getter试图访问obj的a属性");
    return temp;
  },
  set(newValue) {
    console.log("setter试图改变obj的a属性", newValue);
    temp = newValue;
  },
});

console.log(obj.a); 
obj.a = 5
console.log(obj.a);

在这里插入图片描述

1.2 How (闭包)

所以我们就自己定义一个函数,对defineProperty进行封装,来实现数据劫持

使用defineReactive 函数不需要设置临时变量了,而是用闭包

function defineReactive(data, key, value) {
  Object.defineProperty(data, key, {
    // 可枚举 可以for-in
    enumerable: true,
    // 可被配置,比如可以被delete
    configurable: true,
    // getter
    get() {
      console.log(`getter试图访问obj的${key}属性`);
      return value;
    },
    // setter
    set(newValue) {
      console.log(`setter试图改变obj的${key}属性`, newValue);
      if (value === newValue) return;
      value = newValue;
    },
  });
}

let obj = {};
// 初始化
defineReactive(obj, "a", 10);
console.log(obj.a);

obj.a = 5;
console.log(obj.a);

在这里插入图片描述

2. 对象的响应式处理——递归侦测对象全部属性 object

2.1 Why (嵌套)

上面定义的defineProperty()函数,不能监听到对象嵌套的形式

也就是对象嵌套对象

function defineReactive(data, key, value) {
  if (arguments.length === 2) {
    value = data[key];
  }
  Object.defineProperty(data, key, {
    // 可枚举 可以for-in
    enumerable: true,
    // 可被配置,比如可以被delete
    configurable: true,
    // getter
    get() {
      console.log(`getter试图访问obj的${key}属性`);
      return value;
    },
    // setter
    set(newValue) {
      console.log(`setter试图改变obj的${key}属性`, newValue);
      if (value === newValue) return;
      value = newValue;
    },
  });
}

let obj = {
  b: {
    c: {
      d: 4,
    },
  },
};
// 初始化
defineReactive(obj, "b");
console.log(obj.b.c.d);

这里显示没有监听到内部(obj.b.c.d) 在这里插入图片描述

2.2 How(递归)

所以我们要创建一个Observer类 ——> 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object

遍历对象

在这里插入图片描述

observe.js

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

import Observer from "./Observer";
/**
 * 监听 value
 * @param {*} value 
 * @returns 
 */
export default function observe(value) {
  // 如果value不是对象,就什么都不做
  if (typeof value != "object") return;
  let ob;
  if (typeof value.__ob__ !== "undefined") {
    ob = value.__ob__;
  } else {
    ob = new Observer(value);
  }
  return ob;
}

Observer.js

将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object

Observer 类会附加到每一个被侦测的object上 一旦被附加,Observer会将object所有属性转换成getter/setter的形式

__ob__的作用可以用来标记当前value是否已经被Observer转换成了响应式数据了;而且可以通过value.__ob__来访问Observer的实例

import def from "./def";
import defineReactive from "./defineReactive";
/**
 * 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
 */
export default class Observer {
  // 构造器
  constructor(value) {
    // 给实例添加__ob__属性,值是当前Observer的实例,不可枚举
    def(value, "__ob__", this, false);
    
    console.log("Observer构造器", value);
    
    // 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
    this.walk(value);
  }
  // 遍历value的每一个key
  walk(value) {
    for (let key in value) {
      defineReactive(value, key);
    }
  }
}

def.js

工具方法 定义一个对象属性

/**
 * 定义一个对象属性
 * @param {*} obj 
 * @param {*} key 
 * @param {*} value 
 * @param {*} enumerable 
 */
export default function def(obj, key, value, enumerable) {
  Object.defineProperty(obj, key, {
    value,
    enumerable,
    writable: true,
    configurable: true,
  });
}

defineReactive.js

给对象data的属性key定义监听

import observe from "./observe";

/**
 * 给对象data的属性key定义监听
 * @param {*} data 传入的数据
 * @param {*} key 监听的属性
 * @param {*} value 闭包环境提供的周转变量
 */
export default function defineReactive(data, key, value) {
  console.log('defineReactive()', data,key,value)
  if (arguments.length === 2) {
    value = data[key];
  }
  
  // 子元素要进行observe,形成递归
  let childOb = observe(value)
  
  Object.defineProperty(data, key, {
    // 可枚举 可以for-in
    enumerable: true,
    // 可被配置,比如可以被delete
    configurable: true,
    // getter
    get() {
      console.log(`getter试图访问${key}属性`);
      return value;
    },
    // setter
    set(newValue) {
      console.log(`setter试图改变${key}属性`, newValue);
      if (value === newValue) return;
      value = newValue;
      
      // 当设置了新值,新值也要被observe
      childOb = observe(newValue)
    },
  });
}

文件之间的依赖结构 在这里插入图片描述 三个函数互相调用形成了递归 在这里插入图片描述

处理完对象,如果嵌套的是数组怎么办,下面我们来看看数组是怎么处理的

2.3 测试

index.js

import observe from "./observe";

let obj = {
  a: 1,
  b: {
    c: {
      d: 4,
    },
  },
};

observe(obj);
obj.a = 5;
obj.b.c.d = 10;

在这里插入图片描述

3. 数组的响应式处理

正因为我们可以通过Array原型上的方法来改变数组的内容,所以ojbect那种通过getter/setter的实现方式就行不通了。

ES6之前没有提供可以拦截原型方法的能力,我们可以用自定义的方法去覆盖原生的原型方法。

Vue是通过改写数组的七个方法(可以改变数组自身内容的方法)来实现对数组的响应式处理

这些方法分别是:pushpopshiftunshiftsplicesortreverse 在这里插入图片描述

这七个方法都是定义在Array.prototype上,要保留方法的功能,同时增加数据劫持的代码

思路就是 以Array.prototype为原型,创建一个新对象arrayMthods 然后在新对象arrayMthods上定义(改写)这些方法 定义 数组 的原型指向 arrayMthods

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

3.1 前置知识

Object.setPrototypeOf() 修改对象原型

Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或null

Object.setPrototypeOf(obj, prototype)

obj 要设置其原型的对象 prototype 该对象的新原型(一个对象 或 null)

Object.create() 创建一个新对象

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

Object.create(proto,[propertiesObject])

proto 新创建对象的原型对象。 propertiesObject 可选。需要传入一个对象,该对象的属性类型参照Object.defineProperties()的第二个参数。如果该参数被指定且不为 undefined,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符。

返回 一个新对象,带着指定的原型对象和属性。

3.2 实现

array.js

import def from "./def";

const arrayPrototype = Array.prototype;

// 以Array.prototype为原型创建arrayMethod
export const arrayMethods = Object.create(arrayPrototype);

// 要被改写的7个数组方法
const methodsNeedChange = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

// 批量操作这些方法
methodsNeedChange.forEach((methodName) => {
  // 备份原来的方法
  const original = arrayPrototype[methodName];

  // 定义新的方法
  def(
    arrayMethods,
    methodName,
    function () {
      console.log("array数据已经被劫持");

      // 恢复原来的功能(数组方法)
      const result = original.apply(this, arguments);
      // 把类数组对象变成数组
      const args = [...arguments];

      // 把这个数组身上的__ob__取出来
      // 在拦截器中获取Observer的实例
      const ob = this.__ob__;

      // 有三种方法 push、unshift、splice能插入新项,要劫持(侦测)这些数据(插入新项)
      let inserted = [];
      switch (methodName) {
        case "push":
        case "unshift":
          inserted = args;
          break;
        case "splice":
          inserted = args.slice(2);
          break;
      }

      // 查看有没有新插入的项inserted,有的话就劫持
      if (inserted) {
        ob.observeArray(inserted);
      }

      return result;
    },
    false
  );
});

Observer.js

__ob__的作用可以用来标记当前value是否已经被Observer转换成了响应式数据了 而且可以通过value.__ob__来访问Observer的实例

import def from "./def";
import defineReactive from "./defineReactive";
import observe from "./observe";
import {arrayMethods} from './array'
/**
 * 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
 * Observer 类会附加到每一个被侦测的object上
 * 一旦被附加,Observer会将object所有属性转换成getter/setter的形式
 * 来收集属性的依赖,并且当属性发生变化时会通知这些依赖
 */
export default class Observer {
  // 构造器
  constructor(value) {
  
    // 给实例添加__ob__属性,值是当前Observer的实例,不可枚举
    def(value, "__ob__", this, false);
    // __ob__的作用可以用来标记当前value是否已经被Observer转换成了响应式数据了;而且可以通过value.__ob__来访问Observer的实例
    
    // console.log("Observer构造器", value);
    // 判断是数组还是对象
    if (Array.isArray(value)) {
      // 是数组,就将这个数组的原型指向arrayMethods
      Object.setPrototypeOf(value, arrayMethods);
      // 早期实现是这样
      // value.__proto__ = arrayMethods;
      
      // observe数组
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }
  // 对象的遍历方式 遍历value的每一个key
  walk(value) {
    for (let key in value) {
      defineReactive(value, key);
    }
  }
  // 数组的遍历方式
  observeArray(arr) {
    for (let i = 0, l = arr.length; i < l; i++) {
      // 逐项进行observe
      observe(arr[i]);
    }
  }
}

4. 收集依赖

在这里插入图片描述

4.1 Why 为什么要收集依赖

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

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

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

4.2 How 怎么收集依赖

依赖收集到哪里? Dep

目标明确,我们要在 getter 中收集依赖,那 依赖收集到哪里?

将依赖收集封装成一个类 Dep 这个类帮我们管理依赖 可以收集依赖、删除依赖、向依赖发送通知

依赖是什么?Watcher

需要用到数据的地方,成为依赖,就是Watcher

只有Watcher 触发的 getter 才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher 收集到Dep

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

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

在这里插入图片描述

在这里插入图片描述

4.3 实现

代码实现的巧妙之处:Watcher 把自己设置到全局的一个指定位置,然后读取数据。 因为读取了数据,所以会触发这个数据的getter 。在getter 中就能得到当前正在读取数据的Watcher,并把这个Watcher收集到Dep 中。

需要用一个数组来存watcher watcher实例需要订阅 依赖(数据),也就是获取依赖或者收集依赖 watcher的依赖发生时,触发watcher的回调函数,也就是派发更新

Dep.js

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

Dep类专门帮助我们管理依赖,可以收集依赖,删除依赖,向依赖发送通知等

let uid = 0;
/**
 * Dep类专门帮助我们管理依赖,可以收集依赖,删除依赖,向依赖发送通知等
 */
export default class Dep {
  constructor() {
    console.log("Dep");
    this.id = uid++;
    // 用数组存储自己的订阅者,放的是Watcher的实例
    this.subs = [];
  }

  // 添加订阅
  addSub(sub) {
    this.subs.push(sub);
  }

  // 删除订阅
  removeSub(sub) {
    remove(this.subs, sub);
  }

  // 添加依赖
  depend() {
    // Dep.target 是一个我们指定的全局的位置,用window.target也行,只要是全局唯一,没有歧义就行
    if (Dep.target) {
      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();
    }
  }
}

/**
 * 从arr数组中删除元素item
 * @param {*} arr 
 * @param {*} item
 * @returns 
 */
function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1);
    }
  }
}

Watcher.js

import Dep from "./Dep";

let uid = 0;
/**
 * Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方
 */
export default class Watcher {
  constructor(target, expression, callback) {
    console.log("Watcher");
    this.id = uid++;
    this.target = target;
    // 按点拆分  执行this.getter()就可以读取data.a.b.c的内容
    this.getter = parsePath(expression);
    this.callback = callback;
    this.value = this.get();
  }

  get() {
    // 进入依赖收集阶段。
    // 让全局的Dep.target设置为Watcher本身
    Dep.target = this;
    const obj = this.target;
    var value;
    // 只要能找就一直找
    try {
      value = this.getter(obj);
    } finally {
      Dep.target = null;
    }
    return value;
  }

  update() {
    this.run();
  } 

  run() {
    this.getAndInvoke(this.callback);
  }
  getAndInvoke(callback) {
    const value = this.get();
    if (value !== this.value || typeof value === "object") {
      const oldValue = this.value;
      this.value = value;
      callback.call(this.target, value, oldValue);
    }
  }
}

/**
 * 将str用.分割成数组segments,然后循环数组,一层一层去读取数据,最后拿到的obj就是str中想要读的数据
 * @param {*} str
 * @returns
 */
function parsePath(str) {
  let segments = str.split(".");
  return function (obj) {
    for (let key of segments) {
      if (!obj) return;
      obj = obj[key];
    }
    return obj;
  };
}

defineReactive.js

对象在getter中收集依赖,在setter中触发依赖 数组在getter中收集依赖,在拦截器中触发依赖

import Dep from "./Dep";
import observe from "./observe";

/**
 * 给对象data的属性key定义监听
 * @param {*} data 传入的数据
 * @param {*} key 监听的属性
 * @param {*} value 闭包环境提供的周转变量
 */
export default function defineReactive(data, key, value) {
  
  // 每个数据都要维护一个属于自己的数组,用来存放依赖自己的watcher
  const dep = new Dep();

  // console.log('defineReactive()', data,key,value)
  if (arguments.length === 2) {
    value = data[key];
  }

  // 子元素要进行observe,形成递归
  let childOb = observe(value);

  Object.defineProperty(data, key, {
    // 可枚举 可以for-in
    enumerable: true,
    // 可被配置,比如可以被delete
    configurable: true,
    // getter  收集依赖
    get() {
      console.log(`getter试图访问${key}属性`);

      // 收集依赖
      if (Dep.target) {
        dep.depend();
        // 判断有没有子元素
        if (childOb) {
          // 数组收集依赖
          childOb.dep.depend();
        }
      }

      return value;
    },
    // setter 触发依赖
    set(newValue) {
      console.log(`setter试图改变${key}属性`, newValue);

      if (value === newValue) return;
      value = newValue;

      // 当设置了新值,新值也要被observe
      childOb = observe(newValue);

      // 触发依赖
      // 发布订阅模式,通知dep
      dep.notify();
    },
  });
}

array.js

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

import def from "./def";

const arrayPrototype = Array.prototype;

// 以Array.prototype为原型创建arrayMethod
export const arrayMethods = Object.create(arrayPrototype);

// 要被改写的7个数组方法
const methodsNeedChange = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

// 批量操作这些方法
methodsNeedChange.forEach((methodName) => {
  // 备份原来的方法
  const original = arrayPrototype[methodName];

  // 定义新的方法
  def(
    arrayMethods,
    methodName,
    function () {
      console.log("array数据已经被劫持");

      // 恢复原来的功能(数组方法)
      const result = original.apply(this, arguments);
      // 把类数组对象变成数组
      const args = [...arguments];

      // 把这个数组身上的__ob__取出来
      // 在拦截器中获取Observer的实例
      const ob = this.__ob__;

      // 有三种方法 push、unshift、splice能插入新项,要劫持(侦测)这些数据(插入新项)
      let inserted = [];
      switch (methodName) {
        case "push":
        case "unshift":
          inserted = args;
          break;
        case "splice":
          inserted = args.slice(2);
          break;
      }

      // 查看有没有新插入的项inserted,有的话就劫持
      if (inserted) {
        ob.observeArray(inserted);
      }

      // 发布订阅模式,通知dep
      // 向依赖发送消息
      ob.dep.notify();

      return result;
    },
    false
  );
});

5. 测试侦测情况

let obj = {
  a: 1,
  b: {
    c: {
      d: 4,
    },
  },
  e: [22, 33, 44, 55],
};
console.log(obj);

在这里插入图片描述

observe(obj);

在这里插入图片描述

observe(obj);
console.log(obj);

在这里插入图片描述

observe(obj);
obj.a = 5;

在这里插入图片描述

observe(obj);
obj.a++

在这里插入图片描述

observe(obj);
console.log(obj.b.c)

在这里插入图片描述

observe(obj);
obj.b.c.d = 10;

在这里插入图片描述

observe(obj);
obj.e.push(66, 77, 88);
console.log(obj.e);

在这里插入图片描述

observe(obj);
obj.e.splice(2, 1, [13, 14]);
console.log(obj.e);

在这里插入图片描述

observe(obj);
new Watcher(obj, "b.c.d", (val) => {
  console.log("watcher监听", val);
});

在这里插入图片描述 源码链接 gitee.com/ykang2020/v…