Vue3 Reactive & Effect 手写源码

130 阅读6分钟

禁止转载,侵权必究!

Reactive & Effect

Vue3 对比 Vue2 的响应式变化

  • 在 Vue2 的时候使用 defineProperty 来进行数据的劫持, 需要对属性进行重写添加 getter 及 setter 性能差。
  • 当新增属性和删除属性时无法监控变化。需要通过setset、delete 实现
  • 数组不采用 defineProperty 来进行劫持 (浪费性能,对所有索引进行劫持会造成性能浪费)需要对数组单独进行处理 重写方法(push pop shift unshift splice sort reverse)

Vue3 中使用 Proxy 来实现响应式数据变化。从而解决了上述问题。

CompositionAPI

  • 在 Vue2 中采用的是 OptionsAPI, 用户提供的 data,props,methods,computed,watch 等属性 (用户编写复杂业务逻辑会出现反复横跳问题)
  • Vue2 中所有的属性都是通过 this 访问,this 存在指向明确问题
  • Vue2 中很多未使用方法或属性依旧会被打包,并且所有全局 API 都在 Vue 对象上公开。Composition API 对 tree-shaking 更加友好,代码也更容易压缩。
  • 组件逻辑共享问题, Vue2 采用 mixins 实现组件之间的逻辑共享; 但是会有数据来源不明确,命名冲突等问题。 Vue3 采用 CompositionAPI 提取公共逻辑非常方便

Reactivity 模块基本使用

pnpm install @vue/reactivity -w
<div id="app"></div>
<script type="module">
  import {
    reactive,
    effect,
  } from "/node_modules/@vue/reactivity/dist/reactivity.esm-browser.js";
  const state = reactive({ name: "jw", age: 30 });
  effect(() => {
    // 副作用函数 默认执行一次,响应式数据变化后再次执行
    app.innerHTML = state.name + "今年" + state.age + "岁了";
  });
  setTimeout(() => {
    state.age++;
  }, 1000);
</script>

reactive 方法会将对象变成 proxy 对象, effect 中使用 reactive 对象时会进行依赖收集,稍后属性变化时会重新执行 effect 函数~。

1.reactive 函数

@vue/shared

export function isObject(value: unknown): value is Record<any, any> {
  return typeof value === "object" && value !== null;
}
import { isObject } from "@vue/shared";
export const enum ReactiveFlags {
  IS_REACTIVE = "__v_isReactive",
}

export const mutableHandlers: ProxyHandler<object> = {
  get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      // 访问时会触发
      // 在get中增加标识,当获取IS_REACTIVE时返回true
      return true;
    }
    // 等会谁来取值就做依赖收集 Reflect:处理this问题
    const res = Reflect.get(target, key, receiver);
    return res;
  },
  set(target, key, value, receiver) {
    // 等会赋值的时候可以重新触发effect执行
    const result = Reflect.set(target, key, value, receiver);
    return result;
  },
};
const reactiveMap = new WeakMap(); // 缓存列表 key:只能是对象
function createReactiveObject(target: object, isReadonly: boolean) {
  // 不对非对象的类型来进行处理
  if (!isObject(target)) {
    return target;
  }
  if (target[ReactiveFlags.IS_REACTIVE]) {
    // 在创建响应式对象时先进行取值,看是否已经是响应式对象
    return target;
  }
  const exisitingProxy = reactiveMap.get(target); // 如果已经代理过则直接返回代理后的对象;
  if (exisitingProxy) {
    return exisitingProxy;
  }
  const proxy = new Proxy(target, mutableHandlers); // 对对象进行代理
  reactiveMap.set(target, proxy); // 缓存
  return proxy;
}

// 常用的就是reactive方法
export function reactive(target: object) {
  return createReactiveObject(target, false);
}
/*
export function shallowReactive(target: object) {
 return createReactiveObject(target, false)
}
export function readonly(target: object) {
 return createReactiveObject(target, true)
}
export function shallowReadonly(target: object) {
 return createReactiveObject(target, true)
}
*/

这里我们为了代码方便维护,我们将 mutableHandlers 抽离出去到 baseHandlers.ts 中。

这里必须要使用 Reflect 进行操作,保证 this 指向永远指向代理对象

  • 举例
let person = {
  name: "jw",
  get aliasName() {
    return "**" + this.name + "**";
  },
};
let p = new Proxy(person, {
  get(target, key, receiver) {
    console.log(key);
    // return Reflect.get(target,key,receiver)
    return target[key];
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver);
  },
});
// 取aliasName时,我希望可以收集aliasName属性和name属性
console.log(p.aliasName);
// 这里的问题出自于 target[key] ,target指代的是原对象并不是代理对象

2.编写 effect 函数

依赖收集就是将当前的 effect 变成全局的,稍合取值时拿到这个全局的 effect

effect.ts

export let activeEffect = undefined; // 当前正在执行的effect
export class ReactiveEffect {
  active = true; // 标记effect是否处于激活状态
  deps = []; // 收集effect中使用到的属性
  parent = undefined;
  constructor(public fn) {
    // 参数public fn => public fn; 参数 fn; this.fn = fn
  }
  run() {
    if (!this.active) {
      // 不是激活状态,就不需要考虑依赖收集,也就是不需要将这个effect放到全局变量上
      return this.fn();
    }
    try {
      this.parent = activeEffect; // 当前的effect就是他的父亲; 早期是用栈维护effect [e1,e2,e3]
      activeEffect = this; // 设置成正在激活的是当前effect
      return this.fn();
    } finally {
      // 无论什么情况都会执行
      activeEffect = this.parent; // 执行完毕后还原activeEffect
      this.parent = undefined;
    }
  }
}
export function effect(fn, options: any = {}) {
  const _effect = new ReactiveEffect(fn); // 创建响应式effect
  _effect.run(); // 响应式effect默认执行
}

3.依赖收集

默认执行 effect 时会对属性,进行依赖收集

baseHandlers.ts

get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
        return true;
    }
    const res = Reflect.get(target, key, receiver);
    track(target, key); // 依赖收集
    return res;
}

effect.ts

const targetMap = new WeakMap(); // 记录依赖关系 targetMap = {target:{name:[effect1,effect2], age: [effect1,effect2]}}
export function track(target, key) {
  if (!activeEffect) {
    // 取值没有发生在 effect中
    return false;
  }
  let depsMap = targetMap.get(target); // {对象:map} 将属性和对应的effect维护成映射关系,后续属性变化可以触发对应的effect函数重新run
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set())); // {对象:{ 属性 :[ dep,dep ]}}
  }
  let shouldTrack = !dep.has(activeEffect);
  if (shouldTrack) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep); // 让effect记住dep,清理使用
    // 属性与effect多对多的关系;一个属于对应多个effect; 一个effect对应多个属性
  }
}

将属性和对应的 effect 维护成映射关系,后续属性变化可以触发对应的 effect 函数重新 run

4.触发更新

baseHandlers.ts

set(target, key, value, receiver) {
    // 等会赋值的时候可以重新触发effect执行
    let oldValue = target[key]
    const result = Reflect.set(target, key, value, receiver); // true
    if (oldValue !== value) {
        trigger(target, key, value, oldValue)
    }
    return result;
}

effect.ts

export function trigger(target, key?, newValue?, oldValue?) {
  const depsMap = targetMap.get(target); // 获取对应的映射表
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key);
  if (dep) {
    // 这里将要执行的effect拷贝一份
    const effects = [...dep];
    effects.forEach((effect) => {
      if (effect !== activeEffect) effect.run(); // 防止在effect中修改数据造成死循环;
    });
  }
}

5.分支切换与 cleanup

在渲染时我们要避免副作用函数产生的遗留依赖问题 1.reactive.html script

const state = reactive({ flag: true, name: "jw", age: 30 });
effect(() => {
  // 副作用函数 (effect执行渲染了页面)
  console.log("render");
  document.body.innerHTML = state.flag ? state.name : state.age;
});
setTimeout(() => {
  state.flag = false;
  setTimeout(() => {
    console.log("修改name,原则上不更新");
    state.name = "zf";
  }, 1000);
}, 1000);

effect.ts

// 每次执行依赖收集前,先做清理操作
function cleanupEffect(effect) {
  const { deps } = effect; // 清理effect
  // 每次执行effect之前 我们应该清理掉effect中依赖的所有属性
  for (let i = 0; i < deps.length; i++) {
    deps[i].delete(effect); // 属性记录了effect  {key:new set()}
  }
  effect.deps.length = 0;
}
class ReactiveEffect {
  active = true;
  deps = []; // 收集effect中使用到的属性
  parent = undefined;
  constructor(public fn) { }
  run() {
    try {
      this.parent = activeEffect; // 当前的effect就是他的父亲
      activeEffect = this; // 设置成正在激活的是当前effect
      + cleanupEffect(this);
      return this.fn(); // 先清理在运行
    }
  }
}

这里要注意的是:触发时会进行清理操作(清理 effect),在重新进行收集(收集 effect)。在循环过程中会导致死循环。 demo.js

let effect = () => {};
let s = new Set([effect]);
s.forEach((item) => {
  s.delete(effect);
  s.add(effect);
}); // 这样就导致死循环了
  • 属性变化时执行对应 dep 中所有的 effect (循环 set)
  • 清理时找到对应的 dep 删除对应的 effect (set 中移除了 effect)
  • 重新收集时 dep 再次收集了 effect (set 中添加了 effect)

6.停止 effect

effect.ts

export class ReactiveEffect {
  stop() {
    if (this.active) {
      cleanupEffect(this); // 先将effect里面的依赖全部删除
      this.active = false;
    }
  }
}
export function effect(fn, options: any = {}) {
  const _effect = new ReactiveEffect(fn);
  _effect.run();

  const runner = _effect.run.bind(_effect);
  runner.effect = _effect;
  return runner; // 返回runner
}

7.调度执行

trigger 触发时,我们可以自己决定副作用函数执行的时机、次数、及执行方式 effect.ts

export class ReactiveEffect {
    ...
  + constructor(public fn, private scheduler) {} // 参数public fn => public fn; 参数 fn; this.fn = fn
    ...
}
export function effect(fn, options: any = {}) {
  + const _effect = new ReactiveEffect(fn, options.scheduler); // 创建响应式effect
  _effect.run(); // 让响应式effect默认执行
  const runner = _effect.run.bind(_effect);
  runner.effect = _effect;
  return runner; // 返回runner
}
export function trigger(target, key?, newValue?, oldValue?) {
  const depsMap = targetMap.get(target); // 获取对应的映射表
  if (!depsMap) {
    return;
  }
  let dep = depsMap.get(key);
  if (dep) {
    // 这里将要执行的effect
    const effects = [...dep];
    effects.forEach((effect) => {
      if (effect !== activeEffect) {
        + if (effect.scheduler) {
          // 如果有调度函数则执行调度函数
        +   effect.scheduler();
        + } else {
        +   effect.run();
        + }
      }
    });
 }
}

8.深度代理

baseHandlers.ts

get(target, key, receiver) {
  if (key === ReactiveFlags.IS_REACTIVE) {
    return true;
  }
  // 等会谁来取值就做依赖收集
  const res = Reflect.get(target, key, receiver);
  track(target, 'get', key);
  + if(isObject(res)){  // 当取值时返回的值是对象,则返回这个对象的代理对象,从而实现深度代理
  +   return reactive(res);
  + }
  return res;
}