【框架实现】初级vue3响应性的实现

437 阅读5分钟

构建reactive函数,获取proxy实例

整个reactive函数,本质是返回了一个proxy实例,我们先去实现这个reactive函数,得到proxy实例;

1.创建packages/reactivity/src/reactive.ts模块:

import { mutableHandlers } from "./baseHandlers";

export const reactiveMap = new WeakMap<object, any>();

export function reactive(target: object) {
  return createReactiveObject(target, mutableHandlers, reactiveMap);
}
function createReactiveObject(
  target: object,
  baseHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<object, any>
) {
  const existingProxy = proxyMap.get(target);
  // 已经被代理了直接返回
  if (existingProxy) {
    return existingProxy;
  }
  // 没有被代理则生成proxy实例
  const proxy = new Proxy(target, baseHandlers);
  // 缓存proxy
  proxyMap.set(target, proxy);
  return proxy;
}

2.创建packages/reactivity/src/baseHandlers.ts模块;

export const mutableHandlers: ProxyHandler<object> = {};

3.创建packages/reactivity/src/index.ts;

export { reactive } from "./reactive";

5.创建packages/vue/src/index.ts;

export { reactive } from "@vue/reactivity";

在终端执行npm run build;生成dist目录下的文件,此时我们的reactive函数就被成功导出了;

image.png

接下来我们去创建对应的测试实例;创建packages/vue/examples/reactivity/reactive.html;

// 导入刚才打的包
<script src="../../dist/vue.js"></script>

<script>
    const { reactive } = Vue;

    const obj = reactive({
      name: "张三",
    });
</script>

运行之后打印:

image.png

实现createGetter和createSetter方法

在packages/reactivity/src/baseHandlers.ts中修改:

import { track, trigger } from "./effect";
const get = createGetter();
const set = createSetter();

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
};

function createGetter() {
  return function get(target: object, key: string | symbol, receiver: object) {
    const res = Reflect.get(target, key, receiver);

    // 依赖收集
    track(target, key);
    return res;
  };
}

function createSetter() {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ) {
    const result = Reflect.set(target, key, value, receiver);
    // 触发依赖
    trigger(target, key, value);
    return result;
  };
}

新建packages/reactivity/src/effect.ts

/**
 * 收集依赖
 * @param target
 * @param key
 */
export function track(target: object, key: unknown) {
  console.log("收集依赖");
}

/**
 * 触发依赖
 * @param target
 * @param key
 * @param newValue
 */
export function trigger(target: object, key: unknown, newValue: unknown) {
  console.log("触发依赖");
}

接下来测试一下,是否会触发track和trigger方法;记得要重新build;

<script>
    const { reactive } = Vue;

    const obj = reactive({
      name: "张三",
    });

    console.log("obj--->", obj.name);
    obj.name = "李四";
</script>

track和trigger方法被成功打印出来了; image.png

小插曲:给项目加上热更新

给package.json新加一条指令;

"scripts": {
  "dev": "rollup -c -w",
}

构建effect函数,生成ReactiveEffect实例

创建好reactive实例之后,接下来我们需要触发effect;

effect(() => {
  document.querySelector("#app").innerText = obj.name;
});

在effect中,生成了ReactiveEffect实例,并且触发了getter;接下来实现effect方法;

1.在packages/reactivity/src/effect.ts中,创建effect方法;

export function effect<T = any>(fn: () => T) {
  const _effect = new ReactiveEffect(fn);
  _effect.run();
}

export let activeEffect: ReactiveEffect | undefined;

export class ReactiveEffect<T = any> {
  constructor(public fn: () => T) {}
  run() {
    activeEffect = this;
    return this.fn();
  }
}

2.导出effect方法;

image.png

image.png

去修改我们的测试实例;

<script>
    const { reactive, effect } = Vue;

    const obj = reactive({
      name: "张三",
    });

    effect(() => {
      document.querySelector("#app").innerText = obj.name;
    });
</script> 

此时页面中展示出来了张三

image.png

实现track和trigger方法

依赖收集和依赖触发原理

  • track用来依赖收集;
  • trigger用来依赖触发;

响应性是:当响应性数据触发setter时执行fn函数;如果想要实现这个目的,那么必须在getter时去收集当前的fn函数,以便在setter时可以执行对应的fn函数;

export const reactiveMap = new WeakMap<object, any>();

WeakMap的key必须是一个对象,并且key是一个弱引用的;举个例子

const obj = reactive({
      name: "张三",
    });

effect(() => {
  document.querySelector("#app").innerText = obj.name;
});

1.WeakMap:
  1.key:响应性对象 (此时为obj)
  2.value:Map对象 
    1.key:响应性对象的指定属性 (name)
    2.value:指定对象的指定属性的执行函数 (fn函数)

image.png

实现track依赖收集函数

依赖收集的本质就是实现activeEffect和targetMap的绑定:

type KeyToDepMap = Map<any, ReactiveEffect>;
const targetMap = new WeakMap<any, KeyToDepMap>();

/**
 * 收集依赖
 * @param target
 * @param key
 */
export function track(target: object, key: unknown) {
  // activeEffect是fn函数
  if (!activeEffect) return;
  console.log("收集依赖");
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  depsMap.set(key, activeEffect);
  console.log("targetMap--->", activeEffect, targetMap);
}

此时targetMap的值为: image.png

实现trigger触发依赖函数

所有的依赖关系都保存到了targetMap之中,所以依赖触发也就是从targetMap中读取对应的effect,然后执行对应的fn函数;

/**
 * 触发依赖
 * @param target
 * @param key
 * @param newValue
 */
export function trigger(target: object, key: unknown, newValue: unknown) {
  console.log("触发依赖");
  const depsMap = targetMap.get(target);

  if (!depsMap) {
    return;
  }
  const effects = depsMap.get(key) as ReactiveEffect;

  if (!effects) {
    return;
  }
  effects.fn();
}

修改一个测试实例;

<script>
    const { reactive, effect } = Vue;

    const obj = reactive({
      name: "张三",
    });

    effect(() => {
      document.querySelector("#app").innerText = obj.name;
    });

    setTimeout(() => {
      obj.name = "李四";
    }, 4000);
</script>

2024-01-04 10.12.20.gif

初级vue3的响应性如何应对多个effect?

问题描述

我们有这样一个测试实例,它有多个effect,那我们的初级vue3的响应性如何应对呢?

<body>
    <div id="app">
      <p id="p1"></p>
      <p id="p2"></p>
    </div>
  </body>
  <script>
    const { reactive, effect } = Vue;

    const obj = reactive({
      name: "张三",
    });

    effect(() => {
      document.querySelector("#p1").innerText = obj.name;
    });
    effect(() => {
      document.querySelector("#p2").innerText = obj.name;
    });

    setTimeout(() => {
      obj.name = "李四";
      console.log(name);
    }, 3000);
</script>

2024-01-04 10.53.37.gif

在我们的初级vue3响应性代码里,收集依赖时一个key只对应了一个effect,而在我们的测试实例中需要的是一个key对应两个effect;所以初级vue3响应性代码已经不能满足需求了,需要升级。

export function track(target: object, key: unknown) {
  // 只能完成一个key对应一个effect
  depsMap.set(key, activeEffect);
}

如何一个key对应多个effect呢?大家是不是都想到用数组;如下图所示,我们可以构建一个Set(值不会重复的"数组")类型的对象,作为Map的value;

image.png

解决方案

新建packages/reactivity/src/dep.ts;

import { ReactiveEffect } from "./effect";

export type Dep = Set<ReactiveEffect>;

export const createDep = (effects?: ReactiveEffect[]): Dep => {
  return new Set<ReactiveEffect>(effects);
};

修改effect.ts;

import { Dep, createDep } from "./dep";
import { isArray } from "@vue/shared";

type KeyToDepMap = Map<any, Dep>;

export function track(target: object, key: unknown) {
  let dep = depsMap.get(key);
  if (!dep) {
    // 如果dep不存在,建立起联系
    depsMap.set(key, (dep = createDep(dep)));
  }
  trackEffects(dep);
}
/**
 * 利用dep依次跟踪指定key的所有effect
 */
export function trackEffects(dep: Dep) {
  // aciveEffect强行放入dep?
  dep.add(activeEffect!);
}

export function trigger(target: object, key: unknown, newValue: unknown) {
  const dep: Dep | undefined = depsMap.get(key);

  if (!dep) {
    return;
  }
  // 依次触发
  triggerEffects(dep);
}
/**
 *
 * @param dep 依次触发dep中保存的依赖
 */
export function triggerEffects(dep: Dep) {
  // 转化成数组
  const effects = isArray(dep) ? dep : [...dep];

  // 依次触发依赖
  for (const effect of effects) {
    triggerEffect(effect);
  }
}
// 触发指定依赖
export function triggerEffect(effect: ReactiveEffect) {
  effect.run();
}

再试一下上面的测试实例,发现多个effect也能成功运行啦!

2024-01-04 11.32.02.gif

初级vue3响应性还有哪些局限呢?

初级vue3响应性能接受基本数据类型吗?

看一下我们的测试实例;

<script>
    const { reactive, effect } = Vue;

    const obj = reactive("张三");

    console.log("obj--->", obj);
</script>

发现报错了!我们的响应性是由Proxy实现的,Proxy第一个参数target必须是一个对象,如果我们的target不是一个对象类型,此时就会报错;

image.png

如果我们对reactive的对象进行解构,它还会有相应性吗?

<script>
    const { reactive, effect } = Vue;

    const obj = reactive({
      name: "张三",
    });

    let { name } = obj;

    effect(() => {
      document.querySelector("#app").innerText = name;
    });

    setTimeout(() => {
      name = "李四";
    }, 4000);
</script>

可以看到李四被打印出来了,视图还是没有变;解构之后不是Proxy,因此失去了响应性;

2024-01-04 11.45.43.gif

ps: 本文代码仓库地址:gitee.com/minjie05/vu…