Vue设计与实现读后感-响应式系统实现(三)-1

2022-03-22 14:37:39 浏览数 (1)

主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black, awesome-green, qklhk-chocolate

贡献主题:https://github.com/xitu/juejin-markdown-themes

theme: juejin highlight

这篇博客记录自己学习的感悟,也给一些学习vue3的同学一些面试技巧,实战经验。

github

  • goVue项目地址,所有的这次自己的vue3实现的代码都在这个仓库里面。

响应系统的作用与实现

如果要说Vue和其他框架的区别是什么,响应式我相信一定是排在前列的。vue2的Object.defineProperty或者是Vue3的Proxy都是深入人心的概念。实现的思路都是使用get,set,进行依赖收集和数据劫持。但两个api的使用方式方式和支持的能力上面有些区别。简单来说Proxy的功能更强大一下,所有vue3升级成这个api。

在defineProperty场景下面function f(){a.c = blur};f();console.log('hello word')

代码语言:javascript复制
// 数组特性 是不支持的
const data = [1, 2, 3];
try {
Object.defineProperty(data, "length", {
  get: () => {
    console.log("get 触发");
  },
  set: () => {
    console.log("set 触发");
  },
});
data.push(4);
} catch (error) {
console.warn(error);
}

对象属性是可以正常劫持数据,代码如下

代码语言:javascript复制
const data1: any = {};
Object.defineProperty(data1, "key", {
  get: () => {
    console.log("defineProperty get 触发");
  },
  set: () => {
    console.log("defineProperty set 触发");
  },
});
console.log(data1.key);
data1.key = 1;
  • 在Proxy场景下面 显然场景上面的支持更佳丰富,能力更强。
代码语言:javascript复制
const data2 = [1, 2, 3];
const dataProxy = new Proxy(data2, {
  get: (target: any, key: string, receiver: any) => {
    if (key == "length") {
      console.log("Proxy get 数组长度变化");
    }
    return Reflect.get(target, key, receiver);
  },
  set: <T>(target: any, key: string, value: T, receiver: any) => {
    if (key == "length") {
      console.log("Proxy set 数组长度变化");
    }
    Reflect.set(target, key, value, receiver);
    return true;
  },
});
dataProxy.push(4);

数组调用push,以及push之后数组的长度变化这两个都是可以监听到的。

  • 课外知识

Reflect 这个神奇的函数,说白了这个函数有啥用呢?其实没啥大用。大佬的博客 Reflect有什么用。

代码语言:javascript复制
const dataProxy = new Proxy(data2, {
  get: (target: any, key: string, receiver: any) => {
    if (key == "length") {
      console.log("Proxy get 数组长度变化");
    }
    return target[key]
  },
  set: <T>(target: any, key: string, value: T, receiver: any) => {
    if (key == "length") {
      console.log("Proxy set 数组长度变化");
    }
    target[key] = value;
    return true;
  },
});

这样写也没啥大的问题。有个js继承的场景可能要注意一下,下面是我的写的例子。

代码语言:javascript复制
const animal: any = {
  _name: "animal",
  get name() {
    return this._name;
  },
  get eat() {
    console.log(this._name   " eat something");
    return null;
  },
};
const animalProxy = new Proxy(animal, {
  get: (target: any, key: string) => {
    return target[key];
  },
});
const dog = {
  _name: "dog",
  get name() {
    return this._name;
  },
  __proto__: animalProxy,
} as Record<string, any>;
dog.eat;

//-----------------------我是分割线-----------------------
const animalProxy1 = new Proxy(animal, {
  get: (target: any, key: string, receiver) => {
    return Reflect.get(target, key, receiver);
  },
});
const dog1 = {
  _name: "dog",
  get name() {
    return this._name;
  },
  __proto__: animalProxy1,
} as Record<string, any>;

dog1.eat;

在没有使用Reflect.get(target, key, receiver)的时候,没有使用之前this._name指向的值是"animal",修改之后指向值改变为了"dog"。这也是我第一次学到,与君共勉。

响应式的基本api已经介绍了,其实Porxy的api非常丰富有兴趣的小伙伴可以详细阅读。

你们以为我要进入正文了吗?还早呢,其实古人最喜欢做的事情就是借古变法,文艺复兴也是一样。我们喜欢借用古人的口吻和观点,说出我们自己想说的事情。这边这个读后感系列,不仅仅是我在阅读这本书,查看vue3源码,我也在一边表述着我个人的开发感受。

场景的技术实现是最重要的。 并不是我们了解这两个api就可以了,他可以使用在什么场景下面。

Ajax-hook 这个库就是巧用了Object.defineProperty这个方法劫持了XMLHttpRequest这个对象,实现在请求前后插入自己自定义的参数。面向切片在我这篇博客就有详细的说明。一个小小的api却解决了我在一个ifream到处内嵌的项目中统一加密解密的接口的问题。这是很炫酷的事情。当然自己也有异想天开的时候,想在媒体流上面动手脚,显然有些不合时宜。后来接触直播才知道,在媒体中打入实时数据需要自定义协议,非常的复杂。但不妨碍我灵机一动,使用Proxy作为队列监听机制的基础,在 异步队列,请求控制中使用这一api。感觉这是令人兴奋的一瞬间,真的是用来很巧的方案解决了一个很大的问题。

正文真的开始了

响应式数据与副作用函数

之前在摇树优化那边已经提到过副作用函数了,可以下面的代码示例。

代码语言:javascript复制
/**
 * @name: effect
 * @description: 副作用函数改变全局的dom
 */
function effect() {
  document.body.innerHTML = "hello world";
}

/**
 * @name: effect1
 * @description: 副作用函数修改了全局变量
 */
window.a = 1;
function effect1() {
  window.a = 2;
}

这就是典型的产生了副作用,这段代码函数式不能被优化删除的。响应式需要的是什么呢?

代码语言:javascript复制
let obj = {
  text: "hello world",
};
function effect2() {
  document.body.innerHTML = obj.text;
}
obj.text = "hello 吴文周 大帅哥";

响应式需要的就是在obj.text变为"hello 吴文周 大帅哥"时,dom中的文本内容也随之发生变化。这样的效果就达成了,前端开发只需要关注数据的变化,不需要执行命令式的方式将dom操作再执行一遍; 这里对初学者有个很好的经验总结,什么是vue?我只要改变js里面的数据,我就可以完成页面的变化了。数据即UI,UI即数据。

响应式数据的基本实现

代码语言:javascript复制
副作用函数effct定义 --> 收集依赖 --> 数据变化  --> 触发副作用函数

这个流程应该是清晰的,下面就是应该如何实现了。

代码语言:javascript复制
// 原始数据
const obj = {
  text: "hello world",
};
// 正在执行的副作用函数
let effectActiveFu: null | Function = null;
// 副作用函数的缓存池子,现在就定义了一个
let effectCacheFu: null | Function = null;

// 将原数据转换为代理数据使它具有响应式的特性
let objProxy = new Proxy(obj, {
  get(target: any, key: string, receiver: any) {
    // 将正在执行的副作用函数放入缓存
    effectCacheFu = effectActiveFu;
    return Reflect.get(target, key, receiver);
  },
  set(target: any, key: string, value: string, receiver: any) {
    // 先设值在执行回调
    Reflect.set(target, key, value, receiver);
    // 设值你的操作触发副作用函数
    if (effectCacheFu) {
      effectCacheFu();
    }
    return true;
  },
});
/**
 * @name: effectAction
 * @description: 副作用函数的行为
 * @param {type} {*}
 * @return {type} {*}
 */
function effectAction() {
  document.body.innerHTML = objProxy.text;
}
/**
 * @name: effect
 * @description: 副作用封装
 * @param {Function} fun
 */
function effect(fun: Function) {
  // 当前活跃的副作用函数就是此函数
  effectActiveFu = fun;
  fun();
}
// 触发副作用的收集依赖
effect(effectAction);
setTimeout(() => {
  // 改变数据
  objProxy.text = "hello 吴文周 大帅哥";
}, 500);

这个代码显然是不健壮的,有很多问题,例如我们同一个key的副作用不可能只有一个,例如同时两个dom元素的内容关联一个数据集,我们不同key的副作用肯定不能相互触发,例如在objProxy上面新增一个text1的key,不能再触发之前text这个key的依赖函数了,当然还有很多其他细节,具体实现如下。

代码语言:javascript复制
/*
 * @Description: 描述
 * @Author: 吴文周
 * @Github: https://github.com/fodelf
 * @Date: 2022-03-13 18:05:07
 * @LastEditors: 吴文周
 * @LastEditTime: 2022-03-13 22:12:46
 */
// 原始数据
const obj = {
  text: "hello world",
  text1: "hello world tex1",
};
// 正在执行的副作用函数
let effectActiveFu: null | Function = null;

// 所有key的缓存map
let effectCacheMap: Map<string, Set<Function>> = new Map();

// // 副作用函数的缓存池子,现在就定义了一个
// let effectCacheFu: Set<Function> = new Set();

// 将原数据转换为代理数据使它具有响应式的特性
let objProxy = new Proxy(obj, {
  get(target: any, key: string, receiver: any) {
    // 是否存在激活方法
    if (effectActiveFu) {
      let effectList: Set<Function> = new Set();
      // 判读是否存在相同key的缓存列表
      if (effectCacheMap.has(key)) {
        effectList = effectCacheMap.get(key) as Set<Function>;
      } else {
        effectCacheMap.set(key, effectList);
      }
      // 添加到依赖列表
      effectList.add(effectActiveFu);
    }
    return Reflect.get(target, key, receiver);
  },
  set(target: any, key: string, value: string, receiver: any) {
    // 先设值在执行回调
    Reflect.set(target, key, value, receiver);
    // 设值你的操作触发副作用函数
    // 如果缓存池中存在
    if (effectCacheMap.has(key)) {
      let effectList = effectCacheMap.get(key);
      effectList!.forEach((item) => {
        item();
      });
    }
    return true;
  },
});
/**
 * @name: effectAction
 * @description: 副作用函数的行为改变dom
 * @param {type} {*}
 * @return {type} {*}
 */
function effectAction() {
  document.body.innerHTML = objProxy.text;
}
/**
 * @name: effectAction
 * @description: 副作用函数的行为弹出弹窗
 * @param {type} {*}
 * @return {type} {*}
 */
function effectAction1() {
  console.log(objProxy.text);
}
/**
 * @name: effectAction
 * @description: 副作用函数的行为弹出弹窗
 * @param {type} {*}
 * @return {type} {*}
 */
function effectAction2() {
  console.log(objProxy.text1);
}
/**
 * @name: effect
 * @description: 副作用封装
 * @param {Function} fun
 */
export function effect(fun: Function) {
  // 当前活跃的副作用函数就是此函数
  effectActiveFu = fun;
  // 触发收集
  fun();
  // 清空活跃函数
  effectActiveFu = null;
}
// 触发text两个场景副作用的收集依赖
effect(effectAction);
effect(effectAction1);
// 触发text1的收集
effect(effectAction2);
setTimeout(() => {
  // 改变数据text
  objProxy.text = "hello 吴文周 大帅哥";
}, 500);
setTimeout(() => {
  // 改变数据text1
  objProxy.text1 = "hello text1 吴文周 大帅哥";
}, 600);

上面代码基本完成了我们功能述求,就是还有一个场景没有覆盖,例如多个对象的多个key的数据劫持,当前我的缓存池显然是不够用了,第二就是方法的抽象与封装。从设计的原则来说,单一职责的角度,显然我们在新建proxy对象的时候的get,set都已经有些臃肿了,我们需要对它进行抽离,而且未以后更多的场景提供空间,不可所有的vue3代码都写到那个set和get函数里面吧。

之前我们缓存一个对象的key是使用map作为我们缓存,如果是多个对象的话是否还是合适的?在vue3的分享里面尤雨溪着重提出了这个点。使用weakmap作为数据缓存,他讲到内存释放的时候我感觉就是眼神是冒光的。这就是我所说的有成就感的代码,开发的路是很枯燥的,真的找个那个很炫酷的方式,解决很复杂的问题,真的是很有成就感。我之前博客我自己能感受到到那一点,在工作中的收获和满足,解决多ifream之间通用模态框通信机制,解决特定接口的加密解密。每每至此,情不自禁。

扯远了,回到weakmap的场景,作者之所以用weakmap还是在于weakmap是弱引用,在key被删除后,浏览器自带的垃圾回收机制就会将对象回收,而map不会销毁会一直停留在内存当中。有的人讲到这就结束了,我不是,我们打开浏览器的控制台,根据我的截图你们就可以按照相同的步骤查看浏览器的,js对象内存使用情况了。

  • 步骤一

  • 步骤二

这样就真正去关注一下开发的js对象使用的细节了,如果是做低代码那种复杂系统的一定关注内存问题,这是我个人经验,如果处理不好,组件挂载和卸载,页面的内存使用肯定是不健康的。当然如果是基于vue或者react这些框架做的话,问题小一点点,但是也是要关注的。

回归到我们的weakmap缓存场景,我们可以用一个树形结构表述我们当前整个响应式的体系。

依据树形结构以及代码结构,继续优化代码,其他还有一些在ts里面有unknow和any的区别,这篇文章大家可以阅读一下理解TypeScript 中 any 和 unknown。有时候自己图方便写any,这就是大家经常说的ts变成了anyts了,如果any写了之后其实很不安全的,基本失去了ts编译提示的意义了,很容易出现写错了也不知道的情况,真的是any除外。

响应式核心代码如下:

代码语言:javascript复制
export interface Target {
  [string: string]: any;
}
// 正在执行的副作用函数
let effectActiveFu: null | Function = null;
// 所有响应式对象缓存
let targetsMap: WeakMap<object, Map<unknown, Set<Function>>> = new WeakMap();
/**
 * @name: track
 * @description: 收集依赖函数
 * @param {type} {*}
 * @return {type} {*}
 * @param {any} target 响应式的原始数据
 * @param {unknown} key 触发读取的key
 */
function track(target: object, key: unknown) {
  // 是否存在激活方法
  if (effectActiveFu) {
    // 当前对象的缓存map
    let effectCacheMap: Map<unknown, Set<Function>> = new Map();
    // 当前对象当前key的依赖列表
    let deps: Set<Function> = new Set();
    // 判断当前对象是否存在缓存
    if (targetsMap.has(target)) {
      effectCacheMap = targetsMap.get(target);
    } else {
      targetsMap.set(target, effectCacheMap);
    }
    // 判读是否存在相同key的缓存列表
    if (effectCacheMap.has(key)) {
      deps = effectCacheMap.get(key);
    } else {
      effectCacheMap.set(key, deps);
    }
    // 添加到依赖列表
    deps.add(effectActiveFu);
  }
}
/**
 * @name: trigger
 * @description: 触发函数
 * @param {type} {*}
 * @return {type} {*}
 * @param {object} target
 * @param {unknown} key
 */
function trigger(target: Target, key: unknown) {
  // 如果对象是否在对象WeakMap的缓存池中存在
  if (targetsMap.has(target)) {
    const effectCacheMap = targetsMap.get(target);
    //判断当前key是否存在依赖列表
    if (effectCacheMap.has(key)) {
      let deps = effectCacheMap.get(key);
      deps.forEach((item) => {
        item();
      });
    }
  }
}
/**
 * @name: createReactiveObject
 * @description: 创建响应式对象
 * @param {type} {*}
 * @return {type} {*}
 * @param {Target} target
 */
function createReactiveObject(target: Target) {
  return new Proxy(target, {
    get(target: Target, key: string, receiver: unknown) {
      // 创建收集
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target: Target, key: string, value: string, receiver: unknown) {
      // 先设值在执行回调
      Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return true;
    },
  });
}

业务验证代码如下:

代码语言:javascript复制
// 原始数据obj
const obj = {
  text: "hello world",
  text1: "hello world tex1",
};
// 原始数据obj1
const obj1 = {
  text: "obj1 hello world",
  text1: "obj1 hello world tex1",
};
// 将原数据转换为代理数据使它具有响应式的特性
let objProxy = createReactiveObject(obj);
let objProxy1 = createReactiveObject(obj1);
/**
 * @name: effectAction
 * @description: 副作用函数的行为改变dom
 * @param {type} {*}
 * @return {type} {*}
 */
function effectAction() {
  document.body.innerHTML = objProxy.text;
}
/**
 * @name: effectAction1
 * @description: 修改响应对象objProxy的text的key
 * @param {type} {*}
 * @return {type} {*}
 */
function effectAction1() {
  console.log(objProxy.text);
}
/**
 * @name: effectAction2
 * @description: 修改响应对象objProxy的text1的key
 * @param {type} {*}
 * @return {type} {*}
 */
function effectAction2() {
  console.log(objProxy.text1);
}
/**
 * @name: effect
 * @description: 副作用封装
 * @param {Function} fun
 */
export function effect(fun: Function) {
  // 当前活跃的副作用函数就是此函数
  effectActiveFu = fun;
  // 触发收集
  fun();
  effectActiveFu = null;
}
// obj 触发text 两个场景副作用的收集依赖
effect(effectAction);
effect(effectAction1);
// obj 触发text2的收集
effect(effectAction2);
/**
 * @name: otherEffectAction
 * @description: 修改响应对象objProxy1的text的key
 * @param {type} {*}
 * @return {type} {*}
 */
function otherEffectAction() {
  console.log(objProxy1.text);
}
// obj1 对象的收集
effect(otherEffectAction);
setTimeout(() => {
  // 改变数据 objProxy text
  objProxy.text = "hello 吴文周 大帅哥";
  // 改变数据 objProxy1 text
  objProxy1.text = "hello objProxy1 吴文周 大帅哥";
}, 500);
setTimeout(() => {
  // 改变数据 objProxy text
  objProxy.text1 = "hello text1 吴文周 大帅哥";
}, 600);

分支切换与cleanup

没get到点,我的实现跟这个逻辑好像没什么关系。主要是自己没看懂。

嵌套的effect与effect栈

其实这就是一个嵌套的场景,了解过编译原理的这本书,大家都知道一个符号表的概念。说白了组件是嵌套的,使得我们这个依赖收集处理时也要能够支持这个场景。 当只有一个effectActiveFu 激活的副作用函数的时候,嵌套的场景就不行了。

代码语言:javascript复制
export function effect(fun: Function) {
  // 当前活跃的副作用函数就是此函数
  effectActiveFu = fun;
  // 触发收集
  fun();
  effectActiveFu = null;
}
function track(target: object, key: unknown) {
  // 是否存在激活方法
  if (effectActiveFu) {
  }
}

这样的代码在下面场景中就完了

代码语言:javascript复制
// obj 触发text 副作用的收集依赖
effect(() => {
  console.log("f1 执行了");
  effect(() => {
    console.log("f2 执行了");
    document.body.innerHTML = objProxy.text;
  });
  document.body.innerHTML = objProxy.name;
});

嵌套在里面的effect 占用全局那个effectActiveFu,外面的effect修改objProxy.name时就不能在我现在的代码中正常收集了,因为track函数中effectActiveFu判断为null。

其实这个跟less 嵌套理论上是一致的,每一层都有对应自己的选择器,每一个嵌套函数都有属于自己的作用域,还是有点抽象。

代码语言:javascript复制
#container {
  span {
    font-size: 24px;
  }
  #parent {
    span {
      font-size: 12px;
    }
    #child {
      span {
        font-size: 6px;
      }
    }
  }
}

其实编译处理出来的结果是这样的

代码语言:javascript复制
#container span {
  font-size: 24px;
}
#container #parent span {
  font-size: 12px;
}
#container #parent #child span {
  font-size: 6px;
}

其实作者使用栈这样的数据结构表述也没啥问题更准确一定,我只是联想一下跟符号表这样的场景比较明显。大家遇到嵌套的场景都可以使用栈数据结构加递归的方式能够解决通用的一些问题。

| 栈 |

|:----|

| effect3 |

| effect2 |

| effect1 |

| 栈-先进后出 |

|:----|

| effect2 |

| effect1 |

| 栈-先进后出 |

|:----|

| effect1 |

| 栈-先进后出 |

|:----|

具体代码实现我们使用effectActiveFuList 作为我们的函数列表缓存,具体实现如下

代码语言:javascript复制
/**
 * @name: effect
 * @description: 副作用封装
 * @param {Function} fun
 */
export function effect(fun: Function) {
  effectActiveFuList.push(fun);
  fun();
}
/**
 * @name: track
 * @description: 收集依赖函数
 * @param {any} target 响应式的原始数据
 * @param {unknown} key 触发读取的key
 */
function track(target: object, key: unknown) {
  // 从数组中去除最后面那一个
  const effectActiveFu = effectActiveFuList.pop();
  // 是否存在激活方法
  if (effectActiveFu) {
    // 当前对象当前key的依赖列表
    let deps: Set<Function> = new Set();
    // 当前对象的缓存map
    let effectCacheMap: Map<unknown, Set<Function>> = new Map();
    // 判断当前对象是否存在缓存
    if (targetsMap.has(target)) {
      effectCacheMap = targetsMap.get(target);
    } else {
      targetsMap.set(target, effectCacheMap);
    }
    // 判读是否存在相同key的缓存列表
    if (effectCacheMap.has(key)) {
      deps = effectCacheMap.get(key);
    } else {
      effectCacheMap.set(key, deps);
    }
    // 添加到依赖列表
    deps.add(effectActiveFu);
  }
}

避免无线递归循环

业务场景在依赖收集函数中存在count 的这样场景,简化来看就是代码如下。

代码语言:javascript复制
// 业务验证 index.ts
import { createReactiveObject } from "./reactive";
import { effect } from "./effect";
const obj = {
  count: 0,
  isShow: true,
  text: "hello world",
  name: "吴文周",
};
// 将原数据转换为代理数据使它具有响应式的特性
let objProxy = createReactiveObject(obj);
// obj 触发count 副作用的收集依赖
effect(() => {
  objProxy.count  ;
  console.log(objProxy.count);
});
setTimeout(() => {
  objProxy.count = 20;
}, 700);

// 响应对象创建 reactive.ts
import { Target, track, trigger } from "./effect";
/**
 * @name: createReactiveObject
 * @description: 创建响应式对象
 * @param {type} {*}
 * @return {type} {*}
 * @param {Target} target
 */
export function createReactiveObject(target: Target) {
  return new Proxy(target, {
    get(target: Target, key: string, receiver: unknown) {
      // 创建收集
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target: Target, key: string, value: string, receiver: unknown) {
      // 先设值在执行回调
      Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return true;
    },
  });
}

// 依赖收集 effect.ts
// 所有响应式对象缓存
export let targetsMap: WeakMap<
  object,
  Map<unknown, Set<Function>>
> = new WeakMap();
// 收集函数数据栈
export let effectActiveFuList: Array<Function> = [];
// 当前激活的函数
export let effectActiveFu: null | Function = null;
// 当前是否处理依赖收集阶段
export let isTrackActive = false;
// 劫持对象类型
export interface Target {
  [string: string]: any;
}
/**
 * @name: effect
 * @description: 副作用封装
 * @param {Function} fun
 */
export function effect(fun: Function) {
  // 将当前收集函数放入栈中
  effectActiveFuList.push(fun);
  // 设置激活收集状态 新增改动
  isTrackActive = true;
  function effectFu(fun: Function) {
    effectActiveFu = fun;
    fun();
    // 嵌套收集的情况下数据是否清空 新增改动
    if (effectActiveFuList.length == 0) {
      isTrackActive = false;
      effectActiveFu = null;
    }
  }
  effectFu(fun);
}
/**
 * @name: track
 * @description: 收集依赖函数
 * @param {any} target 响应式的原始数据
 * @param {unknown} key 触发读取的key
 */
export function track(target: object, key: unknown) {
  // 从数组中去除最后面那一个 或者 是否是依赖收集函数触发的 新增改动
  effectActiveFu = effectActiveFuList.pop() || effectActiveFu;
  // 是否存在激活方法 新增改动
  if (effectActiveFu && isTrackActive) {
    // 当前对象当前key的依赖列表
    let deps: Set<Function> = new Set();
    // 当前对象的缓存map
    let effectCacheMap: Map<unknown, Set<Function>> = new Map();
    // 判断当前对象是否存在缓存
    if (targetsMap.has(target)) {
      effectCacheMap = targetsMap.get(target);
    } else {
      targetsMap.set(target, effectCacheMap);
    }
    // 判读是否存在相同key的缓存列表
    if (effectCacheMap.has(key)) {
      deps = effectCacheMap.get(key);
    } else {
      effectCacheMap.set(key, deps);
    }
    // 添加到依赖列表
    deps.add(effectActiveFu);
  }
}
/**
 * @name: trigger
 * @description: 触发函数
 * @param {type} {*}
 * @return {type} {*}
 * @param {object} target
 * @param {unknown} key
 */
export function trigger(target: Target, key: unknown) {
  // 如果对象是否在对象WeakMap的缓存池中存在
  if (targetsMap.has(target)) {
    const effectCacheMap = targetsMap.get(target);
    //判断当前key是否存在依赖列表
    if (effectCacheMap.has(key)) {
      let deps = effectCacheMap.get(key);
      // const effects = new Set(deps);
      deps.forEach((item) => {
        // 当前get和set 触发同一个收集函数不执行解决oject.count   的这样的问题   新增改动
        if (effectActiveFu != item) {
          effectActiveFu = item;
          item();
          effectActiveFu = null;
        }
      });
    }
  }
}

总结

  1. 这个重写最大的感受是单元测试的重要性,每个场景的扩展,加入新的代码能不能保证以前的逻辑是否还是正常的。在技术选型时还是有些浅薄,本来就想用golang去做扩展生态链,属性golang,但是在单元测试的场景还是只能隐忍ts的单元测试,node.js还得回来做单元测试。

2.本书阅读,自己的理解,源码,这三个流程的结合,本来初衷是全心全意的做一个vue3从零到有的过程,兼顾各方,效率和理解上面不高。如果是只看源码,就把源码的无效代码删除再跑基础demo就行了。如果把顺序换成源码,自己的思考,再到本书可能会好一点。

3.遗留问题,构建单元测试,构建源码的最小demo。

4.至此响应式的核心应该是阅读完成了,利用了更强大的Proxy的劫持,数组这样的数据类型上面也有很好的支持,其核心思想还是在get的时候将函数缓存,在数据变化触发set的时候重新执行缓存的函数。执行过程中很多细节注意例如函数嵌套的场景,依赖收集时触发赋值的场景。

5.充分利用了weakmap弱引用和set数据去重的特性,在嵌套时候使用栈这样树结构实现了符号表的操作。

0 人点赞