首先理解响应式逻辑
- 知道收集视图依赖了哪些数据
- 感知被依赖数据的变化
- 数据变化时,自动“通知”需要更新的视图部分,并进行更新
01.简单编写响应式手动收集依赖
const obj = {
name: "james",
age: 38,
};
//定义一个函数,专门执行响应式函数
const reactiveFns = [];
function watchFn(fn) {
reactiveFns.push(fn);
fn();
//函数保存起来,默认执行一次
//类似于wachEeffct(),自动收集依赖,立即执行执行一次
}
//凡是传进watchFn函数里面就是响应式的
watchFn(function foo() {
console.log("foo name is", obj.name);
console.log("foo age is ", obj.age);
});
watchFn(function bar() {
console.log("bar name is", obj.name + "kobe");
console.log("bar age is ", obj.age + 10);
});
console.log("监听age发生变化~~~~~~~~");
obj.age += 100;
//把之前依赖的函数遍历出来执行
reactiveFns.forEach((fn) => {
fn();
});
//目前存在缺点,
// + 在实际开发中,响应式对象肯定不止一个
// + 目前还不能自动收集依赖
02.对之前的版本改进,创建一个类Depend,用类去管理所有收集的函数
class Depend {
constructor() {
this.reactiveFns = [];
}
//添加对应的依赖
addDepend(fn) {
if (fn) {
this.reactiveFns.push(fn);
}
}
//通知数据发生变化,收集依赖
notify() {
this.reactiveFns.forEach((fn) => {
fn();
});
}
}
这样在使用的时候只需要new Depend()调用即可
const dep = new Depend();
function watchFn(fn) {
dep.addDepend(fn);
fn();
//函数保存起来,默认执行一次
//类似于wachEeffct(),自动收集依赖,立即执行执行一次
}
//当obj的值发生改变时-----
//手动更新通知
dep.notify();
// 目前存在的缺点:
// + obj值发生变化,还是需要手动去调用notify收集依赖
03.监听属性变量,自动去收集依赖更新
-
监听属性变量
到这一步时候,可以用vue2的obj.definproperty()数据劫持,或者vue3de new Proxy()去监听对象属性
//方案一。Object.defineProperty() -> Vue2
//对obj里面的数据进行遍历
Object.keys(obj).forEach((key) => {
let value = obj[key];
//对每个key和set操作进行对应劫持
Object.defineProperty(obj, key, {
set: function (newValue) {
//保存value值
value = newValue;
//去通知更新
dep.notify();
},
get: function () {
return value;
},
});
});
目前是对象属性发生改变,不用再去手动调用notify()更新
但是目前实现到这里,出现一个错误情况,那就是收集依赖既收集name的,又同时收集age的。如果只改变name的值,age的依赖也同时更新发生变化了,这种做法是错误的。
想要实现的,是在当前属性发生变化的时候,收集依赖只能是当前属性的依赖,通知更新也应当前属性的更新,所以这里需要解决这个问题,应该是一一对应的关系,自动去收集依赖
-
- dep对象数据结构的管理
- 每一个对象的每一个属性都会对应一个dep对象
- 同一个对象的多个属性的dep对象是存放一个map对象中
- 多个对象的map对象, 会被存放到一个objMap的对象中
- 依赖收集: 当执行get函数, 自动的添加fn函数
所以这种做法是不要的。
-
重新封装执行自动收集依赖
//定义一个函数,专门执行响应式函数
//形成闭包
let reactiveFn = null;
function watchFn(fn) {
//一旦执行函数,将值赋值给reactiveFn
reactiveFn = fn;
fn();
//函数保存起来,默认执行一次
//类似于wachEeffct(),自动收集依赖,立即执行执行一次
reactiveFn = null;
//执行完操作之后,赋值为null,有可能对后续操作造成影响
}
//封装一个函数,来获取对应的depend对象
//利用weakMap,弱引用,即使obj为null,也不能对后续造成影响,
const objMap = new WeakMap();
function getDepend(obj, key) {
//根据对象obj,找到对应的map对象
let map = objMap.get(obj);
if (!map) {
//weakMap的key必须要是对象,所以这里用Map()
//如果拿不到,我们就new一个
map = new Map();
objMap.set(obj, map);
}
//根据key,找到对应的depend对象
let dep = map.get(key);
if (!dep) {
dep = new Depend();
map.set(key, dep);
}
//所以一定可以返回拿到一个dep对象
return dep;
}
//方案一。Object.defineProperty() -> Vue2
//对obj里面的数据进行遍历
Object.keys(obj).forEach((key) => {
let value = obj[key];
//对每个key和set操作进行对应劫持
Object.defineProperty(obj, key, {
set: function (newValue) {
//保存value值
value = newValue;
const dep = getDepend(obj, key);
//去通知更新
dep.notify();
},
get: function () {
// 拿到obj -> key
// 找到对应的obj对象的key对应的dep对象
const dep = getDepend(obj, key);
//间接的把函数添加进去
dep.addDepend(reactiveFn);
return value;
},
});
});
此时已经完成可以自动收集依赖更新,但是还存在俩个问题:
- 如果监听重复obj.age的代码,它会重复执行一遍
watchFn(function() {
console.log(obj.name)
console.log(obj.age)
console.log(obj.age)
})
- 只是只是针对单个对象,还不能针对多对象
04.最后完善优化,封装函数针对多对象调用
- 针对于重复执行问题,可以在类Depend里面进行if判断,或者直接使用Set()
constructor() {
//set不允许重复
this.reactiveFns = new Set();
}
-
针对多对象使用问题,
-
封装成一个reactive函数返回出去,让外面的对象在使用的时包裹一层reactive()
function reactive(obj) { Object.keys(obj).forEach((key) => { let value = obj[key]; //对每个key和set操作进行对应劫持 Object.defineProperty(obj, key, { set: function (newValue) { //保存value值 value = newValue; const dep = getDepend(obj, key); //去通知更新 dep.notify(); }, get: function () { // 拿到obj -> key // 找到对应的obj对象的key对应的dep对象 const dep = getDepend(obj, key); //间接的把函数添加进去 dep.depend(); return value; }, }); }); return obj; } //在使用时 const obj = reactive({ name: "james", age: 38, });
-
到这一步已实现vue2的响应式原理,实现vue3的响应式原理那就太简单,直接把Object.defineProperty换成Proxy去实现就行了
- vue2与vue3响应式原理的区别:
- Vue2采⽤数据劫持并结合了发布者-订阅者模式,通过 Object.defineProperty()来劫持各个属性setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调
- Vue3放弃了Object.defineProperty API,⽽使⽤了更快的Proxy API。Proxy 是在 ES6 中引⼊,它允许你拦截对该对象的任何交互,也可以避免 Vue 早期版本中存在的⼀些响应性问题。