架构的思考(4)
准备
目标的实现里涉及的两个问题:
- 监听数据的读和写。
- 关联函数和数据
前面部分已经实现了监听数据的读和写
,涉及了依赖收集和派发更新
,每次读写都会触发这两个,之前是仅仅通过打印来看效果,现在就要真正地去实现了。
如何把这两个函数搞定呢?肯定要建立一个对应关系,数据和函数的对应关系。这个对应关系呢肯定要有一个数据结构。那怎么样的数据结构比较合适呢?
这个数据结构与
vue
有点不同,比vue
复杂一点点。
-
targetMap
map结构,键值对,键就是我们的对象,每个对象的属性又对应一个map。
-
propMap
这个是属性的map,属性的映射关系,每个属性就是他的键,属性值又对应一个map。为啥会有个问号?那就是当进行迭代,操作类型是
INTERATE
的时候,压根就没有传属性。 -
typeMap
这里的键呢就是操作行为,每个操作里边是一个集合,这里的集合称之为 dep,表示依赖。
也就是说,哪个函数依赖哪个对象的哪个属性的读取行为,那dep是一个集合,就会保留很多个函数。
接下来就是在effect.js
里建立数据结构。
//effect.js
const targetMap = new WeakMap();
const INTERATE_KEY = Symbol('iterate') //迭代时的属性
好了,明确一下目标,依赖收集就是按照这些结构去建立这些对应关系。派发更新就是要找到对应关系里的函数重新运行一遍。
回到我们的effect.js
,我们的参数,有对象,属性,操作类型,但缺少了函数,怎么搞,怎么找到哪个使用了属性的函数?
function fn() {
state.a;
}
fn();
这个很明确,就是fn
在使用。那如果是这样呢?
function fn() {
function fn1() {
state.a;
}
fn1();
}
fn();
这种情况到底是哪个将哪个函数存入集合?如果再有多层嵌套呢?那就说不清了到底是哪个函数了?说不清
这三个字熟悉吗?之前在讲函数与数据
的时候,也是说不清是哪个数据。所以,把决定权交给用户,给需要进行依赖收集的函数打上个标记。比如有这么个函数effect
,这个函数帮你运行函数。
function fn1() {
state.a;
}
fn1();
}
effecty(fn) //运行函数
在运行函数的期间,用到的所有响应式数据,比如state.a
,不管他在哪个地方用的,只要是运行fn
的期间,用到了某个响应式数据,那这个响应式数据要关联的函数就是fn
。
/**
* @description: 副作用函数。运行fn函数期间,将用到的所有响应式数据与fn进行关联
* @param {* function} fn 要执行的函数
* @return {*}
*/
export function effect(fn) {}
接下来就是要在track
依赖收集里使用这个fn
函数。
export function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}
刚开始把fn
赋值给activeEffect
,然后执行fn()
,这就能保证在函数运行期间,这个activeEffect
是有值的,函数运行结束之后,设置为null
。而fn
运行期间有可能用到了响应式数据,用到了那就会触发track
函数,也就是说在track
运行期间,activeEffect
是有值的。
export function track(target, type, key) {
//不应该进行依赖收集 或 缺少函数 则不进行依赖收集
if (!shouldTrack || !activeEffect) {
return;
}
...
}
现在是信息齐全了,但我们想一个问题,依赖收集的目的是什么?是派发更新对吧,将来我数据变动的时候能通过这个对应关系,找到对应函数让它重新运行。
那将来重新运行这个函数的时候,还能够建立这个对应关系吗?我们的对应关系是每一次运行函数都要重新建立的。为啥要重新建立而不是一开始就遍历呢?来说个比较抽象的逻辑。
function fn() {
if (state.a === 1) {
state.b;
} else {
state.c;
}
}
第一次我运行这个函数的时候,依赖关系是state.a
和state.b
的读和fn
关联起来了。有一天进行派发更新,state.a=2
,这个函数是不是得重新运行,依赖关系是不是得变成 state.a
和state.c
的读和fn
啊。目前我们的做法是收集了具体的函数fn
,那将来重新运行的是这个:
if (state.a === 1) {
state.b;
} else {
state.c;
}
那直接重新运行这个函数,不通过effect
来运行,fn
就没有啦,那 activeEffect = fn;
那就没了,有了针对activeEffect
的两个操作,才能保证在执行fn
期间,能拿到fn
,才能进入track
进而收集依赖。有点抽象啊,头疼😫😫😫
因此收集依赖的时候,应该把那个执行环境,那3行代码都收集进来。这样子,将来在派发更新的时候,会把这个3行代码重新运行一遍,这样子才能达到重新收集依赖的目的。
export function effect(fn) {
const effectFn = () => {
try {
activeEffect = fn;
return fn();
} finally {
activeEffect = null;
}
};
effectFn();
}
依赖收集,建立对应关系
都处理差不多了,下面处理tarck
函数了。
- 拿
targetMap
里的对象
let propMap = targetMap.get(target);
if (!propMap) {
propMap = new Map();
targetMap.set(target, propMap);
}
这下propMap
已经是有东西了,现在要把propMap
里的属性拿出来。
- 那
propMap
里的属性
if (type === TrackOpTypes.INTERATE) {
key = INTERATE_KEY;
}
let typeMap = propMap.get(key);
if (!typeMap) {
typeMap = new Map();
propMap.set(key, typeMap);
}
- 拿
typeMap
里的操作类型
let depsMap = typeMap.get(type);
if (!depsMap) {
depsMap = new Map();
typeMap.set(type, depsMap);
}
//将函数加入 deps
if (!depsMap.has(activeEffect)) {
depsMap.add(activeEffect);
}
看一下这个结构,一个对象对应一个map,一个属性对应一个map,一个操作类型对应一个set,set里面存了那个函数。
派发更新,找到对应函数一次运行
你改的是哪个对象的哪个属性,改的是哪个行为,然后找到那个函数,将那个函数重新运行。
/**
* @description: 处理函数,找到对应的函数
* @param {* object} target 源对象
* @param {* stirng} type 操作类型
* @param {* stirng} key 属性
* @return {*}
*/
function getEffectFns(target, type, key) {
const propMap = targetMap.get(target);
if (!propMap) {
return;
}
}
如果有迭代和修改属性一起,那可能有多个属性要拿,所以在这个typeMap
里,得处理。
const keys = [key];
if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
keys.push(INTERATE_KEY);
}
const effectFn = new Set();
for (const key of keys) {
const typeMap = propMap.get(key);
if (!typeMap) {
continue;
}
console.log('typeMap',typeMap);
}
return effectFn;
如果操作类型是ADD
或DELECTE
时,还得把这个迭代属性
加上,因为这个收集和派发的属性是对应的,等会儿说明。 所以得有个数组装这些属性,然后遍历这个属性,把这个属性从propMap
里拿出来。
那现在直接从typeMap
里把对应的动作
的函数集合拿出来不就行了?不一样,比如之前是get
动作,存了一些函数。那现在是add
动作,要到哪里去拿这个集合?所以说,派发动作和收集动作有一个关联关系。
export const TrackOpTypes = {
GET: "get", //读取属性值
HAS: "has", //判断属性是否存在
INTERATE: "interate", //迭代对象
};
export const TriggerOpTypes = {
SET: "set", //设置属性
ADD: "add", //添加属性
DELETE: "delete", //删除属性
};
这就是他们的影响关系,所以我们得建立一个关系。
const triggerTypeMap = {
[TriggerOpTypes.SET]: [TrackOpTypes.GET],
[TriggerOpTypes.ADD]: [
TrackOpTypes.GET,
TrackOpTypes.HAS,
TrackOpTypes.ITERATE,
],
[TriggerOpTypes.DELETE]: [
TrackOpTypes.GET,
TrackOpTypes.HAS,
TrackOpTypes.ITERATE,
],
};
下面就是处理了,把对应的函数拿出来。
function getEffectFns(target, type, key) {
const propMap = targetMap.get(target);
if (!propMap) {
return;
}
const keys = [key];
if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
keys.push(ITERATE_KEY);
}
const effectFns = new Set();//用来存储函数的集合
const triggerTypeMap = {
[TriggerOpTypes.SET]: [TrackOpTypes.GET],
[TriggerOpTypes.ADD]: [
TrackOpTypes.GET,
TrackOpTypes.HAS,
TrackOpTypes.ITERATE,
],
[TriggerOpTypes.DELETE]: [
TrackOpTypes.GET,
TrackOpTypes.HAS,
TrackOpTypes.ITERATE,
],
};
//循环所有属性
for (const key of keys) {
const typeMap = propMap.get(key);
if (!typeMap) {
continue; //拿不到这个 操作 就继续
}
const trackTypes = triggerTypeMap[type]; //派发操作对应的依赖操作集合
//对操作集合 比如 get has iterate 进行循环
for (const trckType of trackTypes) {
const dep = typeMap.get(trckType); //拿出这个操作类型的函数集合
if (!dep) {
continue;
}
for (const effectFn of dep) {
effectFns.add(effectFn); //将函数集合存起来
}
}
}
return effectFns;
}
/**
* @description: 派发更新
* @param {* object} target 代理的源对象
* @param {* stirng} key 属性
* @param {* stirng} type 写的操作类型
* @return {*}
*/
export function trigger(target, type, key) {
const effectFns = getEffectFns(target, type, key);
for (const effectFn of effectFns) {
effectFn(); //依次执行函数
}
}
现在effect
的核心模块的核心已经结束了。
麻了,感觉是就是一直在打补丁,现在发现一个问题。
function fn() {
console.log("fn");
if (state.a == 1) {
state.b;
} else {
state.c;
}
}
effect(fn); //运行函数
state.a=2;
state.b=4
- 第一次,条件成立,会运行,打印
fn
。没问题。 - 第二次,改变了a的值,重新收集依赖,重新运行。没问题。
- 第三个,改变这个b的值,因为涉及到了,也会重新运行函数。
但仔细想想,修改了a之后,那收集的依赖就是a和c,和b就没有关系了,这时候再去改变b,是不是就不应该重新运行函数? 那我们说的是运行fn
的时候,是重新收集依赖,也就是抛弃之前的不要了,但目前好像还保留之前的。 那就是说在运行fn
之前,要把它从集合里删掉,重新运行fn
,重新进行依赖收集。
那问题就来了,运行之前查找所有表,把fn
从所有表里删除,这是非常浪费效率的。所以可以有这么个方案,我去记录一下这个函数在那个集合里面。
开搞,给这个fn
添加个属性,用来记录所在的集合。
export function effect(fn) {
const effectFn = () => {
try {
activeEffect = effectFn;
clearFn(effectFn); //清除函数所在集合
return fn();
} finally {
activeEffect = null;
}
};
effectFn.deps = [];// 存放函数集合
effectFn();
}
// effect.js
// tract()
if (!depsSet.has(activeEffect)) {
depsSet.add(activeEffect);
activeEffect.deps.push(depsSet);//往属性里添加 函数集合
}
/**
* @description: 辅助函数,用来清除 fn 所在集合
* @param {* function} effectFn fn
* @return {*}
*/
export function clearFn(effectFn) {
const { deps } = effectFn;
if (!deps.length) {
return;
}
for (const dep of deps) {
dep.delete(effectFn);//把fn 从dep函数集合里删掉
}
deps.length = 0;
}
那这个重新收集依赖的需求就搞定了。还有一个问题,函数嵌套。
function fn() {
console.log("fn");
effect(() => {
console.log("inner");
state.a;
});
state.b;
}
effect(fn);
state.b = 4;
第一次执行,打印fn
,打印inner
,没毛病。改变之后,应该是会执行fn
的,可是并没有打印fn
。来分析下逻辑。
activeEffect
刚开始是没有值的,运行fn
的时候,它被赋值为fn
所在的环境,在这个fn
运行的期间呢,又运行了inner
,然后又把inner
所在的环境赋值给activeEffect
,然后inner
运行结束,activeEffect
变为null
,这时fn
还没运行完,回去运行fn
,这时state.b
,就收集不到依赖了。
这是一个执行栈的问题。第一个函数调用第二个函数,第二个运行第三个函数。第三个运行结束出栈,到第二个函数出栈,最后第一个函数出栈。
所以上面是的执行逻辑和栈的逻辑是不一样的,那我们就准备一个执行栈。
//effect.js
...
const effectStack = [];
...
export function effect(fn) {
const effectFn = () => {
try {
activeEffect = effectFn;
effectStack.push(effectFn); //把函数加入栈
clearFn(effectFn); //清除函数所在集合
return fn();
} finally {
effectStack.pop();//把函数推出
activeEffect = effectStack[effectStack.length - 1];//取栈顶
}
};
effectFn.deps = []; // 存放函数集合
effectFn();
}
好了还有个问题,无限递归造成栈溢出。只需要判断一下触发的函数是不是当前函数。
export function trigger(target, type, key) {
const effectFns = getEffectFns(target, type, key);
for (const effectFn of effectFns) {
if (effectFn === activeEffect) {
continue;
}
effectFn();
}
}
ok没问题了,优化一下,现在函数是立即执行,那优化下把交给用户选择。
//index.js
const effectFn = effect(fn, {
lazy: true,
});
//effect.js
export function effect(fn, optitons) {
const { lazy = false } = optitons; //默认立即执行
const effectFn = () => {
try {
activeEffect = effectFn;
effectStack.push(effectFn);
clearFn(effectFn); //清除函数所在集合
return fn();
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
};
effectFn.deps = []; // 存放函数集合
if (!lazy) {
effectFn();
}
return effectFn; //返回
}
还有个问题,
function fn() {
console.log("fn");
state.a = state.a + 1;
}
const effectFn = effect(fn, {
lazy: true,
});
effectFn()
state.a++
state.a++
state.a++
state.a++
state.a++
这个执行了很多次,嗯怎么说呢,vue
不是每次都更新,是会等所有数据都变动之后,再统一更新,这个样可以避免无用更新哈。那我们也可以把这个执行权交给用户。
现在默认是把函数拿出来重新执行的,那我们可以配置一个调度器,把要执行的函数交给调度器,然后由调度器决定要不要执行。
export function effect(fn, optitons) {
const { lazy = false } = optitons;
const effectFn = () => {
try {
activeEffect = effectFn;
effectStack.push(effectFn);
clearFn(effectFn);
return fn();
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
};
effectFn.deps = [];
effectFn.optitons = optitons; //保存配置项
if (!lazy) {
effectFn();
}
return effectFn;
}
export function trigger(target, type, key) {
const effectFns = getEffectFns(target, type, key);
for (const effectFn of effectFns) {
if (effectFn === activeEffect) {
continue;
}
//如果有配置 让用户自行决定
if (effectFn.optitons.scheduler) {
effectFn.optitons.scheduler(effectFn);
} else {
effectFn();
}
}
}
转载自:https://juejin.cn/post/7347962680604475446