目录

  1. 背景
  2. 我的心路历程
  3. 实现
  4. 嵌入现有项目试验一下
  5. 存在风险
  6. 结束语

1. 背景

Vue.js 3.0 ,整个源码是通过 monorepo 的方式维护的,根据功能将不同的模块拆分到 packages 目录下面不同的子目录中:
image.png

可以看出相对于 Vue.js 2.x 的源码组织方式,monorepo 把这些模块拆分到不同的 package 中,每个 package 有各自的 API、类型定义和测试。这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。

另外一些 package(比如 reactivity 响应式库)是可以独立于 Vue.js 使用的,这样用户如果只想使用 Vue.js 3.0 的响应式能力,可以单独依赖这个响应式库而不用去依赖整个 Vue.js,减小了引用包的体积大小,而 Vue.js 2 .x 是做不到这一点的。

2. 我的心路历程

这个其实说起来有点搞笑,当我有这个想法并实现出这个东西的时候,并且我还在我们组吹嘘了一波,我是这样。
image.png
但是,我在知乎、掘金上发现居然有人已经玩过了。本以为发现了新大陆,然而居然是玩剩下的,只有我还在鼓里。然后我是这样的。
image.png

3. 实现

  1. import { effect, reactive } from '@vue/reactivity';
  2. import { useRef, useEffect, useMemo, useState } from 'react';
  3. // 防止初始化的时候进行
  4. let initUpdate = false;
  5. const useReactive = (initState: any) => {
  6. // 使用一个状态,这个状态本身是没有用的,只是为了做强制更新
  7. const [, forceUpdate] = useState(0);
  8. // 保存响应式对象(useRef一直保存的的原始对象,防止响应式对象丢失)
  9. const reactiveState = useRef(initState);
  10. // 对响应式对象缓存
  11. const state = useMemo(() => reactive(reactiveState.current), [
  12. reactiveState.current
  13. ]);
  14. // 监听响应式对象的改变
  15. useEffect(() => {
  16. effect(() => {
  17. for (const i in state) {
  18. // 访问触发依赖收集
  19. state[i];
  20. }
  21. // 强制更新,利用前面无用的状态
  22. if (initUpdate) {
  23. forceUpdate((num) => num + 1);
  24. }
  25. if (!initUpdate) initUpdate = true;
  26. });
  27. }, [state]);
  28. return state;
  29. };
  30. export default useReactive;

测试源代码看这里

实现很简单,这里有几个核心点需要注意。

核心1:reactive

接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()。
响应式转换是“深层的”:会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现,返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象。

核心2:effect

effect 作为 reactive 的核心,主要负责收集依赖,更新依赖。

核心3:触发依赖收集

  1. for (const i in state) {
  2. // 访问触发依赖收集
  3. state[i];
  4. }

在底层源码中,track 收集依赖(get操作)。

  1. /**
  2. * @description:
  3. * @param {target} 目标对象
  4. * @param {type} 收集的类型, 函数的定义在下方
  5. * @param {key} 触发 track 的 object 的 key
  6. */
  7. export function track(target: object, type: TrackOpTypes, key: unknown) {
  8. // activeEffect为空代表没有依赖,直接return
  9. if (!shouldTrack || activeEffect === undefined) {
  10. return
  11. }
  12. // targetMap 依赖管理中心,用于收集依赖和触发依赖
  13. // 检查targetMap中有没有当前target
  14. let depsMap = targetMap.get(target)
  15. if (!depsMap) {
  16. // 没有则新建一个
  17. targetMap.set(target, (depsMap = new Map()))
  18. }
  19. // deps 来收集依赖函数,当监听的 key 值发生变化时,触发 dep 中的依赖函数
  20. let dep = depsMap.get(key)
  21. if (!dep) {
  22. depsMap.set(key, (dep = new Set()))
  23. }
  24. if (!dep.has(activeEffect)) {
  25. dep.add(activeEffect)
  26. activeEffect.deps.push(dep)
  27. // 开发环境会触发onTrack, 仅用于调试
  28. if (__DEV__ && activeEffect.options.onTrack) {
  29. activeEffect.options.onTrack({
  30. effect: activeEffect,
  31. target,
  32. type,
  33. key
  34. })
  35. }
  36. }
  37. }
  38. // get、 has、 iterate 三种类型的读取对象会触发 track
  39. export const enum TrackOpTypes {
  40. GET = 'get',
  41. HAS = 'has',
  42. ITERATE = 'iterate'
  43. }

4. 嵌入现有项目试验一下

image.png
在 React 项目中实现了一个 hooks。

之前定义状态

image.png

之前更新状态

image.png

现在定义状态

image.png

现在更新状态

image.png

5. 风险点

如果你认真的去看,其实这代码是存在风险点的。
代码中我们使用 for 循环 get 属性,进行依赖的收集,这恰恰就是问题,每次更新状态都会去触发 for 循环,你懂的,如果存在大量的key就无法避免的带来了性能问题。

在Vue2.0会用一个ob属性进行标识是否被依赖收集过,如果被收集过,就不会再被收集!vue3收集方式和2.0有点区别,vue3.0 当引用了才会被收集,比如说当我们调用this.obj ,obj下面的属性才会被收集,如果被收集过,不会被收集,但是对于没有引用过的,就会被再收集。而且采用了WeakMap,一旦不需要,就会自行消失。 好吧 ,我们回到最开始的问题 , 我们在react jsx引用的属性不能像vue3 处理 template模版一样,可以收集依赖,所以第一次更新之后,响应式对象不需要了被回收了,所以第二次就无法更新了,所以每一次需要我先遍历一次。

6. 结束语

如果文章中什么不对或者写的不好的地方,请大家多多指正,谢谢!码字不易,点个赞加个关注吧!
靓仔.gif

参考