> 文章列表 > 【Vue】Vue2-Vue3响应式原理

【Vue】Vue2-Vue3响应式原理

【Vue】Vue2-Vue3响应式原理

什么是响应式?

我们先来看一下响应式意味着什么?
💁‍♀️ 我们来看一段代码:⬇️

  • m有一个初始化的值,有一段代码使用了这个值;
  • 那么在m有一个新的值时,这段代码可以自动重新执行;
let m = 20;
console.log(m);
console.log(m*2);m=40;

⬆️ 上面的这样一种可以自动响应数据变量的代码机制,我们称之为是响应式的。
👉 对象的响应式:⬇️

响应式函数设计

☝️ 首先,执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中:

  • 那么我们的问题就变成了,当数据发生变化时,自动去执行某一个函数;


🥺 但是,有一个问题:
❓❓在开发中我们是有很多的函数的,我们如何区分一个函数需要响应式,还是不需要响应式呢?

  • 很明显,下面的函数中 foo 需要在obj的name发生变化时,重新执行,做出相应;
  • bar函数是一个完全独立于obj的函数,它不需要执行任何响应式的操作;
function foo() {//依赖于obj.namelet newName = obj.name;console.log(obj.name);
}
function bar() {const result = 20 + 20;console.log(result);console.log('hello world');
}

响应式函数的实现watchFn

将依赖于另一个数据的代码片段,放进一个函数中,
当数据发生变化时,依赖于该数据的函数自动执行。
🥺 但是,我们怎么区分呢?

  • 这个时候我们封装一个新的函数watchFn;
  • 凡是传入到watchFn的函数,就是需要响应式的;
  • 其他默认定义的函数都是不需要响应式的;
let obj = {name: '张三',age: 32,
};const reactiveFns = [];function watchFn(fn) {reactiveFns.push(fn);fn();
}reactiveFns.forEach((fn) => {fn();
});console.log('---name发现变化----');
obj.name = '李四';
watchFn(function () {let newName = obj.name;console.log('foo:', obj.name);console.log('bar:', obj.age);
});
watchFn(function () {console.log('bar:', obj.age);console.log('bar:', obj.name + '!!');
});

响应式依赖的收集

  • 目前我们收集的依赖是放到一个数组中来保存的,
  • ❓ 但是这里会存在数据管理的问题:
    • 我们在实际开发中需要监听很多对象的响应式;
    • 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;
    • 我们不可能在全局维护一大堆的数组来保存这些响应函数;
  • 所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数: 
    • 相当于替代了原来的简单 reactiveFns 的数组;
class Depend {constructor() {this.reactiveFns = [];}addDepend(fn) {if (fn) {this.reactiveFns.push(fn);}}notify() { //通知函数重新执行this.reactiveFns.forEach((fn) => {fn();});}
}
let obj = {name: '张三',age: 32,
};
// 设置一个专门执行响应式函数的一个函数
const dep = new Depend();
function watchFn(fn) {dep.addDepend(fn);fn();
}// console.log('---name发现变化----');watchFn(function foo() {let newName = obj.name;console.log('foo:', obj.name);console.log('foo:', obj.age);console.log('foo:function');
});
watchFn(function bar() {console.log('bar:', obj.age + 20);console.log('bar:', obj.name + '!!');console.log('bar:function');
});console.log('---修改obj的属性值---');
console.log('-----name发生改变-----');obj.name = '李四';
dep.notify(); //每次都是手动触发 修改通知console.log('-----age发生改变-----');
obj.age = 42;
dep.notify();console.log('-----name发生改变-----');obj.name = '王五';
// 不通知dep.notify(); 就不会执行 

监听对象的变化

那么我们接下来就可以通过之前学习的方式来监听对象的变量: 
方式一:通过 Object.defineProperty 的方式(vue2采用的方式); 
方式二:通过new Proxy的方式(vue3采用的方式);

// 方式一:Object.defineProperty
Object.keys(obj).forEach((key) => {let value = obj[key];Object.defineProperty(obj, key, {set: function (newValue) {value = newValue;dep.notify();},get: function () {return value;},});
});
  const objProxy = new Proxy(obj, {set: function (target, key, newValue, receiver) {Reflect.set(target, key, newValue, receiver);dep.notify();},get: function (target, key, receiver) {return Reflect.get(target, key, receiver);},});

❓❓此时的问题:

改变一个对象的属性值,整个函数都会重新执行,实际上我们需要执行的只是依赖于发生改变的属性的函数。
比如:改变obj的name属性,只需要通知只是依赖于name的函数重新执行
但是目前的问题:
对obj来说,对应的是同一个dep对象;
只有一个dep对象,对于name属性依赖的函数和age属性依赖的函数都在同一个dep对象的属性reactiveFns中;
属性的依赖都在reactiveFns数组中,无法区分。

解决方式:⬇️ 通过 map

【Vue】Vue2-Vue3响应式原理

对象的依赖管理

我们目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数: 
但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;
我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
在前面我们刚刚学习过WeakMap,并且在学习WeakMap的时候我讲到了后面通过WeakMap如何管理这种响应式的数据依赖:

对象依赖管理的实现

分析:比如:obj.name 使用就放进 obj的map中的name对应的dep中

  1. dep对象数据结构的管理(最难理解)
    1. 每一个对象的每一个属性都会对应一个dep对象
    2. 每一个对象的多个属性的dep对象是存放在一个map对象中
    3. 多个对象的map对象,会被存放到一个objMap的对象中
  2. 依赖收集:当执行get函数,自动添加fn函数

写一个getDepends()专门管理

// 自动通过obj的key自动收集对象的Depend对象
const objMap = new WeakMap();
function getDepends(obj, key) {//1.根据对象的obj,找到对应的Depend对象let map = objMap.get(obj);if (!map) {map = new Map();objMap.set(obj, map);}// 2.根据不同的key,找到对应的depend对象let dep = map.get(key);if (!dep) {dep = new Depend();map.set(key, dep);}return dep;
}

正确的依赖收集

我们之前收集依赖的地方是在 watchFn 中:
但是这种收集依赖的方式我们根本不知道是哪一个key的哪一个depend需要收集依赖;
你只能针对一个单独的depend对象来添加你的依赖对象;
那么正确的应该是在哪里收集呢?
应该在我们调用了Proxy的get捕获器时
因为如果一个函数中使用了某个对象的key,那么它应该被收集依赖;

// 设置一个专门执行响应式函数的一个函数
let reactiveFn = null;
function watchFn(fn) {reactiveFn = fn;fn();reactiveFn = null;
}
// 方式一:Object.defineProperty
Object.keys(obj).forEach((key) => {let value = obj[key];Object.defineProperty(obj, key, {set: function (newValue) {value = newValue;const dep = getDepends(obj, key);dep.notify();},get: function () {// console.log(obj, key);// 精准的找到obj key的dep对象const dep = getDepends(obj, key);dep.addDepend(reactiveFn);return value;},});
});
//方式二:通过new Proxy的方式(vue3采用的方式); 
const objProxy = new Proxy(obj, {set: function (target, key, newValue, receiver) {Reflect.set(target, key, newValue, receiver);const dep = getDepends(target, key);dep.notify();},get: function (target, key, receiver) {const dep = getDepends(target, key);dep.addDepend(reactiveFn);return Reflect.get(target, key, receiver);},
});

对Depend重构

⚠️ 但是这里有两个问题:
❓❓问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次;
❓❓问题二:我们并不希望将添加reactiveFn放到get中,以为它是属于Dep的行为;
所以我们需要对Depend类进行重构:
 ✅ ✅ 解决问题一的方法:不使用数组,而是使用Set;
 ✅ ✅ 解决问题二的方法:添加一个新的方法,用于收集依赖;

let obj = {name: '张三',age: 32,address: '北京',
};
Object.keys(obj).forEach((key) ...
......watchFn(function () {console.log('----1----');console.log(obj.name);console.log(obj.age);
});watchFn(function () {console.log('----2----');console.log(obj.age); console.log(obj.age);
});watchFn(function () {console.log('----3----');console.log(obj.name);console.log(obj.address);
});// 改变age
obj.age = 26; 

【Vue】Vue2-Vue3响应式原理

问题: console.log(obj.age);执行了两次
解决:⬇️

class Depend {constructor() {this.reactiveFns = new Set(); //Set 中的元素只会出现一次}addDepend(fn) {if (fn) {// this.reactiveFns.push(fn);this.reactiveFns.add(fn);}}depend() {if (reactiveFn) {this.reactiveFns.add(reactiveFn);}}notify() {this.reactiveFns.forEach((fn) => {fn();});}
}

多个对象响应式

封装Object

function reactive(obj) {//  方式一:Object.definePropertyObject.keys(obj).forEach((key) => {let value = obj[key];Object.defineProperty(obj, key, {set: function (newValue) {value = newValue;const dep = getDepends(obj, key);dep.notify();},get: function () {// console.log(obj, key);// 精准的找到obj key的dep对象const dep = getDepends(obj, key);dep.addDepend(reactiveFn);return value;},});});return obj;
}
// 方式二: new proxy()
function reactive(obj) {const objProxy = new Proxy(obj, {set: function (target, key, newValue, receiver) {Reflect.set(target, key, newValue, receiver);const dep = getDepends(target, key);dep.notify();},get: function (target, key, receiver) {const dep = getDepends(target, key);dep.addDepend(reactiveFn);return Reflect.get(target, key, receiver);},});return objProxy;
}
// ----业务代码----
let obj1 = reactive({name: '张三',age: 32,address: '北京',
});watchFn(function () {console.log('----obj1----');console.log(obj1.name);console.log(obj1.age);console.log(obj1.address);
});const user = reactive({nikiname: 'park',level: 11,
});watchFn(function () {console.log('----user----');console.log(user.nikiname);console.log(user.level);
});

完整代码:

class Depend {constructor() {this.reactiveFns = new Set();}addDepend(fn) {if (fn) {// this.reactiveFns.push(fn);this.reactiveFns.add(fn);}}depend() {if (reactiveFn) {this.reactiveFns.add(reactiveFn);}}notify() {this.reactiveFns.forEach((fn) => {fn();});}
}// 设置一个专门执行响应式函数的一个函数
let reactiveFn = null;
function watchFn(fn) {reactiveFn = fn;fn();reactiveFn = null;
}
// 自动通过obj的key自动收集对象的Depend对象
const objMap = new WeakMap();
function getDepends(obj, key) {//1.根据对象的obj,找到对应的Depend对象let map = objMap.get(obj);if (!map) {map = new Map();objMap.set(obj, map);}// 2.根据不同的key,找到对应的depend对象let dep = map.get(key);if (!dep) {dep = new Depend();map.set(key, dep);}return dep;
}// 方式一:Object.defineProperty
function reactive(obj) {Object.keys(obj).forEach((key) => {let value = obj[key];Object.defineProperty(obj, key, {set: function (newValue) {value = newValue;const dep = getDepends(obj, key);dep.notify();},get: function () {// console.log(obj, key);// 精准的找到obj key的dep对象const dep = getDepends(obj, key);//dep.addDepend(reactiveFn);dep.depend();return value;},});});return obj;
}
// // 方式二: new proxy()
// function reactive(obj) {
//   const objProxy = new Proxy(obj, {
//     set: function (target, key, newValue, receiver) {
//       Reflect.set(target, key, newValue, receiver);
//       const dep = getDepends(target, key);
//       dep.notify();
//     },
//     get: function (target, key, receiver) {
//       const dep = getDepends(target, key);
//       dep.depend();
//       return Reflect.get(target, key, receiver);
//     },
//   });
//
//   return objProxy;
// }
// ----业务代码----
let obj1 = reactive({name: '张三',age: 32,address: '北京',
});watchFn(function () {console.log('----obj1----');console.log(obj1.name);console.log(obj1.age);console.log(obj1.address);
});const user = reactive({nikiname: 'park',level: 11,
});watchFn(function () {console.log('----user----');console.log(user.nikiname);console.log(user.level);
});// 改变user.level = 12;