简易 Vue3 响应式原理
本文最后更新于:2022年4月22日 上午
手写简易 Vue3 响应式原理
我们知道vue 3
的响应式撅弃了 Object.definePropertie()
采用了 es6
新增的 proxy。
当我们知道上述前提后,想象一下,当我们修改一个对象中的属性时,要如何做到,可以通知到使用方这个属性已经发生变更了呢?
因为我们一个应用程序可能会有非常多个对象,每个对象都有自己的属性,当属性发生变化的时候,需要通知所有用到该属性的地方,那么这个数据结构该怎么设计呢?
先决条件
我们可以如下设计
const targetMap = new WeapMap();
const obj = { name: 'abc', age: 20 }
const depMap = new Map();
depMap.set('name',[fn1, fn2]); // 所有依赖 name 的
targetMap.set(obj, depMap);
解释一下上述的操作是什么意思
创建一个 WeapMap
对象 targetMap
,用来存储所有被监听的对象
key
就是 被监听的对象,value
则是 一个 map
,这个map
是 obj
的所有依赖
    这个 map
的key
就是被监听的对象的 key
    value
就是一个个依赖了 obj.name
的方法,如果 name
发生改变后就调用里面的方法以通知更新
    
那当我们需要通知的时候如下操作就可以非常方便的通知了
当
obj.name
发生改变的时候,通过obj
取到obj
所有的depend
再从
obj
的depend
中取到name
的depend
遍历所有依赖了
name
的function
,并执行
obj.name = 'js';
const depMap = targetMap(obj);
const dep = depMap('name');
dep.forEach(fn => fn());
通过以上的设计,就可以配合 proxy
设计出一个简单的响应式系统了
实现
reactive
function reactive(obj) {
return new Proxy(obj, {
get() {
// 第一次执行时先获取一下依赖并执行
const depend = getDepend(...arguments);
depend.depend();
return Reflect.get(...arguments);
},
set() {
// 当数据发生改变时拿到 这个 depend 调用 notice 来进行通知
const depend = getDepend(...arguments);
depend.notice();
Reflect.set(...arguments);
},
});
}
Depend
Depend
是具体某一个属性的依赖对象,里面主要有以下三个属性
depends:保存当前所有属性的
set
,类似数组结构,使用set
是为了去重depend():添加依赖
notice():通知/广播
class Depend {
constructor() {
this.depends = new Set();
}
// 通知
notice() {
this.depends.forEach((cb) => cb());
}
// 收集依赖
depend() {
// 此处需要做非空检验,否则,当 notice 通知的时候会再次收集依赖的时候会 拿到 null,且陷入死循环
currentFunc && this.depends.add(currentFunc);
}
}
watch
该函数接收一个函数参数,在这里函数里面使用了响应式变量的话,会被自动收集依赖
当watch
接收到 func
的时候,会将它先暂存在currentFunc
,然后执行 这个函数,此时就可以触发 proxy
中的 get
,在 get
中我们就可以收集依赖了
let currentFunc = null;
function watch(func) {
currentFunc = func;
currentFunc();
// 调用完成置空
currentFunc = null;
}
reactive
reactive
会将一个普通对象变成一个响应式对象,如 const obj = reacive({ name: 'abc', age: 18 })
,那么这个 obj
就变成了一个响应式对象
reactive
会通过 Proxy
返回一个响应式对象
get:会拿到当前属性的
depend
,然后收集依赖set:会拿到当前属性的
depend
,然后发出通知
function reactive(obj) {
return new Proxy(obj, {
get() {
// 第一次执行时先获取一下依赖并执行
const depend = getDepend(...arguments);
depend.depend();
return Reflect.get(...arguments);
},
set() {
// 当数据发生改变时拿到 这个 depend 调用 notice 来进行通知
const depend = getDepend(...arguments);
depend.notice();
Reflect.set(...arguments);
},
});
}
完整示例
let currentFunc = null; // 当前监听的函数
class Depend {
constructor() {
this.depends = new Set();
}
// 通知
notice() {
this.depends.forEach((cb) => cb());
}
// 收集依赖
depend() {
// 此处需要做非空检验,否则,当 notice 通知的时候会再次收集依赖的时候会 拿到 null,且陷入死循环
currentFunc && this.depends.add(currentFunc);
}
}
const targetMap = new WeakMap();
/**
* 获取 dep
* @param {*} target
* @param {*} key
* @returns
*/
function getDepend(target, key) {
// 1. 从全局中获取到当前对象的 map,如果没有则设置一个空的 map
let depMap = targetMap.get(target);
if (!depMap) {
depMap = new Map();
targetMap.set(target, depMap);
}
// 2. 从当前的 map 中取到当前 key 的 depend,如果没有的话则在 map 中添加一个新的空 map
let dep = depMap.get(key);
if (!dep) {
dep = new Depend();
depMap.set(key, dep);
}
return dep;
}
function reactive(obj) {
return new Proxy(obj, {
get() {
// 第一次执行时先获取一下依赖并执行
const depend = getDepend(...arguments);
depend.depend();
return Reflect.get(...arguments);
},
set() {
// 当数据发生改变时拿到 这个 depend 调用 notice 来进行通知
const depend = getDepend(...arguments);
depend.notice();
Reflect.set(...arguments);
},
});
}
// 将当前需要监听的函数暂存并且首次先执行一次,以添加监听
function watch(func) {
currentFunc = func;
currentFunc();
// 调用完成置空
currentFunc = null;
}
const obj = {
name: 'lfm',
age: 24,
};
const objProxy = reactive(obj);
watch(function () {
console.log('我监听了name', objProxy.name);
console.log('我还监听了name', objProxy.age);
});
console.log('^^^^^^^^^^^^^^^^^ 分割线 ^^^^^^^^^^^^^^^^^^^');
objProxy.name = 'aaa';
objProxy.age = 123;
输出
可以看到第一次我们会先执行一次
然后我们修改了 name
的时候触发了一次
修改了 age
的时候也触发了一次
$ node index.js
我监听了name lfm
我还监听了name 24
^^^^^^^^^^^^^^^^^ 分割线 ^^^^^^^^^^^^^^^^^^^
我监听了name lfm
我还监听了name 24
我监听了name aaa
我还监听了name 24
总结
基于以上,我们实现了一个简易的 响应式系统,可以在数据发生改变的时候可以给各个使用到了该属性的地方发出通知
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处。