03_01_实现 effect & reactive & 依赖收集 & 触发依赖


theme: nico

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

03_01_实现 effect & reactive & 依赖收集 & 触发依赖

一、reactivity happy path

首先我们知道reactivityhappy path(核心逻辑)就是: 通过reactive定义响应式变量,然后通过effect去收集响应式变量的依赖,然后实现依赖的自动收集和自动触发。

那我们先来编写第一个测试案例,通过单测来带大家看一看功能需求。

首先删掉之前的index.spec.ts,建立effect.spec.ts,实现reactivityhappy path

describe('effect', function () {
  it.skip('happy path', function () {
    // * 首先定义一个响应式对象
    const user = reactive({
      age: 10
    })

    // * get -> 收集依赖
    let nextAge;
    effect(() => {
      nextAge = user.age + 1;
    })

    // * effect默认会执行一次
    expect(nextAge).toBe(11);

    // * set -> 触发依赖
    user.age++;
    expect(nextAge).toBe(12);
  });
});

那么reactivityhappy path的单测书写完毕,因为核心逻辑的单测需要依赖于reactiveeffect
两个api,所以此处 it.skip,先跳过这个测试用例,我们先来实现reactive


二、reactive happy path

那在实现reactive之前,依旧先来写reactive核心逻辑的单测。

describe('reactive', function () {
  it('happy path', function () {
    const original = { foo: 1 };
    const observed = reactive(original);

    expect(observed).not.toBe(original);
    expect(observed.foo).toBe(original.foo);
  });
});

这个单测需要我们实现的功能主要有两点:

  1. 响应式对象observed和原始对象original不全等;
  2. observed也能取到foo属性的值,并且与original一致。

那带着这些需求,我们接着建立reactive.ts,来实现一下reactive的这个最核心的逻辑。

export function reactive(raw) {
  return new Proxy(raw, {
    // 此处使用proxy报错的话,需要进tsconfig.json中,配置"lib": ["DOM", "ES6"]。
    get(target, key) {
      const res = Reflect.get(target, key);

      // todo 依赖收集
      return res;
    },

    set(target, key, value) {
      const res = Reflect.set(target, key, value);

      // todo 触发依赖
      return res;
    }
  })
}

至此,reactivehappy path实现完毕,至于如何进行依赖收集触发依赖,我们放到后面再去慢慢考虑。

那现在,先来看一下单测有没有通过。

yarn test reactive

03_01_reactive核心逻辑单测

测试通过,那么接下来,我们继续完善reactive的逻辑代码。

接着,再去reactive.spec.tseffect.spec.ts中引入reactive

import { reactive } from '../reactive';

ps: 至于上述代码中采用Reflect.get而不是target[key]返回属性值,将在下一篇文章中详细做出阐述。


三、effect happy path

那只要再把effect完善,那reactivityhappy path的单测就不会报错了。

那么,现在,咱就去完善effect。建立effect.ts文件,并完善基础逻辑。

class ReactiveEffect {
  private _fn: any;

  constructor(fn) {
    this._fn = fn;
  }

  run() {
    this._fn();
  }
}

export function effect(fn) {
  const _effect = new ReactiveEffect(fn);

  _effect.run();
}

可以注意到,此处我们封装了ReactiveEffect类。是因为我们需要对传进来的依赖进行不同的操作,并且以后还会扩展出更多的功能,所以将其封装也是为了日后更好地维护。

此处多说两句:

封装概念通常由两部分组成,封装数据封装实现,也就是:

  1. 相关的数据(用于存储属性)
  2. 基于这些数据所能做的事(所能调用的方法);

封装的目的是将信息隐藏,即属性与方法的可见性。

从《设计模式》的角度出发,封装在更重要的层面体现为 封装变化

通过 封装变化 的方式,把系统中 稳定不变的部分容易变化的部分 隔离开来。在系统的演变过程中,我们只需要替换那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性和可扩展性。

继续回归正题。💬

此时,去掉effect happy pathitskip,然后注释掉set -> 触发依赖后的两行,先不看update的过程,运行一下测试。

yarn test

03_02_effect部分逻辑单测


四、重中之重 依赖收集和触发依赖

那现在的难点就来了,如何让user.age++的时候,nextAge也自动更新。
这其实就已经到了响应式系统的核心逻辑了,也就是 依赖收集触发依赖,也就是 tracktrigger 的实现。

1. 依赖收集 track

回到reactive.tsProxyget方法中增加track,当某个属性被读取时,进行依赖的收集。

我们这里所说的依赖,也叫副作用函数,即产生副作用的函数。也就是说,effect函数的执行,会直接或间接影响其它函数的执行,这时我们说effect函数产生了副作用。

const res = Reflect.get(target, key);

track(target, key);
return res;

由于依赖是被effect包裹的,所以就在effect.ts中来写track的逻辑。

export function track(target, key) {
  // * target -> key -> dep
}

从上可以看出,依赖对应的结构🌲应该如下:

03_03_依赖树形关系

  • target:被操作(读取)的代理对象( user );
  • key:被操作(读取)的字段名( age );
  • dep:用effect函数注册的副作用函数( () => { nextAge = user.age + 1; } ),也就是我们要收集的依赖。

那这里分别使用WeakMapMapSet来存储。

  • WeakMaptarget -> Map 构成;
  • Mapkey -> Set 构成。

其中WeakMap的键是原始对象target,值是一个Map实例;
Map的键是原始对象targetkey,值是一个由副作用函数组成的Set

03_04_WeakMap、Map和Set之间的关系

搞清楚他们的关系之后,有必要解释一下这里为什么用WeakMap,这就涉及到了WeakMapMap的区别。

简单地说,WeakMapkey是弱引用,不影响垃圾回收器的工作。但如果使用Map来代替WeakMap,那么即使用户侧的代码对target
没有任何引用,这个target也不会被回收,最终可能导致内存溢出。

理论完毕,接下来开始完善track的逻辑。

// * target -> key -> dep
export function track(target, key) {
  // * depsMap: key -> dep
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }

  // * dep
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }

  dep.add(activeEffect); // activeEffect是当前依赖,完整代码最后挂出
}

2. 触发依赖 trigger

再次回到reactive.tsProxyset方法中增加trigger,当某个属性被读取时,进行依赖的收集。

const res = Reflect.get(target, key);

track(target, key);
return res;

再接着完善trigger的逻辑,取出所有的依赖,依次执行。

export function trigger(target, key) {
  let depsMap = targetMap.get(target);
  let dep = depsMap.get(key);

  for (const effect of dep) {
    effect.run();
  }
}

感觉好像就这样,没啥问题了,那继续跑一下单测吧,看看是不是真的没问题了。

03_05_effect、reactive全流程单测

可以,全部通过,美滋滋啊~ 🍉

那么至此,我们就实现了 effect & reactive & 依赖收集 & 触发依赖happy path


ps: 当然这只是最简形态的reactive,就比如: 分支切换(三元表达式)、嵌套effect、++的情况,我们都完全还没有考虑进去,下下篇我们将来完善对这些情况的处理。

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容