Vue3源码中双向绑定和依赖收集浅析

76 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

此文根据mini-vue进行学习.主要是简述了双向绑定和依赖收集的基本原理。

打开mini-vuepnpm install 安装依赖。

Reactive - 双向绑定

目的

将一个普通对象转换为proxy对象

  1. 根据mini-vue的源码文件路径 reactivity/src/reactive.ts 分析createReactiveObject(target, proxyMap, baseHandlers)
// target就是传入的对象
function createReactiveObject(target, proxyMap, baseHandlers) {
...
  const proxy = new Proxy(
    target,
    baseHandlers
  )
...
}

reactive.ts中import的三个函数(mutableHandlers/readonlyHandlers/shallowReadonlyHandlers)是从reactivity/src/baseHandlers.ts 文件中export的,同时这三个对象也是createReactiveObject中的第三个形参baseHandlers的入参,也就是new Proxy的第二个入参。

2.分析reactivity/src/baseHandlers.ts ,根据MDN对于Proxy的第二个参数handler的定义我们可以明确,导出的这三个对象内可以用get和set函数来拦截属性。

目前主要关注导出的对象mutableHandlers ,因为reactive(target)内部用的就是mutableHandlers。

reactive.ts 中可以看到通过baseHandlers.ts引入的三个对象,不同的函数引用的不同的对象作为proxy的第二个参数handler

import {
  mutableHandlers,
  readonlyHandlers,
  shallowReadonlyHandlers,} from "./baseHandlers";
...
export function reactive(target) {
  return createReactiveObject(target, reactiveMap, **mutableHandlers**);
}

export function readonly(target) {
  return createReactiveObject(target, readonlyMap, readonlyHandlers);
}

export function shallowReadonly(target) {
  ...
}
function createReactiveObject(target, proxyMap, baseHandlers) {
...
  const proxy = new Proxy(
    target,
    **baseHandlers**
  )
...

baseHandlers.ts 可以看到mutableHandlers是如何被构造的

const get = createGetter();
const set = createSetter();
createGetter(isReadonly = false, shallow = false) {
  // 返回get函数
  return function get(target, key, receiver){
    ...
    const res = Reflect.get(target, key, receiver);
    ...
    // 返回通过Reflect.get的值
    return res
  }
}
function createSetter() {
  // 返回set函数
  return function set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);

    // 在触发 set 的时候进行触发依赖
    trigger(target, "set", key);

    return result;
  };
}
// 导出给reactive中createReactiveObject作为第二个入参
export const mutableHandlers = {
  get,
  set,
};

可以看到mutableHandlers这个对象内部有get和set两个属性函数,这两个函数在设置返回值res/result时,采用的是Reflect上的get和set方法。

effect 核心 - 依赖收集

配合Reactive / Ref达到依赖收集(track)和触发依赖(trigger)

src/reactivity/src/effect.ts

effect的目的(为什么有effect)

effect的目的是什么?

下面是对应单测代码src/reactivity/``**tests**``/effect.spec.ts,同时也对应effect需要做到的功能。

effect会接受一个函数,当后续counter.num改变为7时,让dummy也跟着改变为7。

也就是说当effect内部包裹的函数内依赖的数据发生变化时,重新执行这个函数,更新数据。

it("should observe basic properties", () => {
    let dummy;
    const counter = reactive({ num: 0 });
    effect(() => (dummy = counter.num));
    expect(dummy).toBe(0);
    counter.num = 7;
    expect(dummy).toBe(7);
  });

effect是怎么做的

回过头来看effect的代码src/reactivity/src/effect.ts

关注其中的effect(fn, options = {})函数

export function effect(fn, options = {}) {
  const _effect = new ReactiveEffect(fn);
  // 把用户传过来的值合并到 _effect 对象上去
  // 缺点就是不是显式的,看代码的时候并不知道有什么值
  extend(_effect, options);
  // 实际上是执行用户传入的fn。run方法是声明在ReactiveEffect实例上的
  _effect.run();
  ...
}

_effect.run();执行的是ReactiveEffect类给实例对象用的方法

  • activeEffect在更新依赖时用于给下一次的get
export class ReactiveEffect {
  ...
  run() {
      ...
      // 利用全局属性来获取当前的 effect
      activeEffect = this as any;
      // 执行用户传入的 fn
      const result = this.fn();
      ...
      return result;
    }
    ...
  }

执行fn()之后,实际上对应就是调用了上面单测案例中的被effect接收的函数() => (dummy = counter.num) , 其中counter是一个响应式对象,当couter.name时实际上就会触发proxy实例中的get方法,其实对应的就是baseHandler.ts中的get方法

然后来看baseHandler.ts中的get

const get = createGetter();
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    ...
    const res = Reflect.get(target, key, receiver);

    // 问题:为什么是 readonly 的时候不做依赖收集呢
    // readonly 的话,是不可以被 set 的, 那不可以被 set 就意味着不会触发 trigger
    // 所有就没有收集依赖的必要了
    if (!isReadonly) {
      // 在触发 get 的时候进行依赖收集
      track(target, "get", key);
    }
    ...
    return res;
  };
}

可以看到在get方法中,只要是非readonly的,就会触发track(target, "get", key);来收集依赖 ,而track是通过import从 effect.ts中引入的。

接着来分析effect.ts中的track方法

  • 这里的targetMap是声明在外部的const targetMap = new WeakMap();也就是说是全局公用的,用于存放所有依赖关系。{对象 : depsMap( {对象属性 : dep( { 依赖函数1,依赖函数2等 } ) )} , 数据2:...}。

export function track(target, type, key) {
  ...
  console.log(`触发 track -> target: ${target} type:${type} key:${key}`); 
  // 1. 先基于 target 找到对应的 dep
  // 如果是第一次的话,那么就需要初始化
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    // 初始化 depsMap 的逻辑
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  // 作为容器收集依赖
  let dep = depsMap.get(key);

  if (!dep) {
    dep = createDep();
    depsMap.set(key, dep);
  }

  trackEffects(dep);
}
export function trackEffects(dep) {
  // 用 dep 来存放所有的 effect
  // activeEffect就是每次调用run的那个new ReactiveEffect(fn)生成的实例对象_effect
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    (activeEffect as any).deps.push(dep);
  }
}

通过判断activeEffect,将依赖收集到了dep中。依赖实际上就是ReactiveEffect实例

在triggle相关函数中将dep中所有收集的依赖遍历并执行了。

export function trigger(target, type, key) {
  // 1. 先收集所有的 dep 放到 deps 里面,
  // 后面会统一处理
  let deps: Array<any> = [];
  // dep
  const depsMap = targetMap.get(target);

  if (!depsMap) return;

  // 暂时只实现了 GET 类型
  // get 类型只需要取出来就可以
  const dep = depsMap.get(key);

  // 最后收集到 deps 内
  deps.push(dep);

  const effects: Array<any> = [];
  deps.forEach((dep) => {
    // 这里解构 dep 得到的是 dep 内部存储的 effect
    effects.push(...dep);
  });
  // 这里的目的是只有一个 dep ,这个dep 里面包含所有的 effect
  // 这里的目前应该是为了 triggerEffects 这个函数的复用
  triggerEffects(createDep(effects));
}
export function triggerEffects(dep) {
  // 执行收集到的所有的 effect 的 run 方法
  for (const effect of dep) {
    if (effect.scheduler) {
      // scheduler 可以让用户自己选择调用的时机
      // 这样就可以灵活的控制调用了
      effect.scheduler();
    } else {
      effect.run();
    }
  }
}

此处effect.run实际上就是再次执行了单元测试里effect(() => (dummy = counter.num));中effect内部的函数。

整体流程

对于单元测试的代码打上断点进行调试

it("should observe basic properties", () => {
    let dummy;
    const counter = reactive({ num: 0 });
    effect(() => (dummy = counter.num));
    expect(dummy).toBe(0);
    counter.num = 7;
    expect(dummy).toBe(7);
  });

effect(() => (dummy = counter.num));可以理解为整个reactivity的init流程

整体流程就是

  1. 执行effect.ts下的effect函数,传入箭头函数() => (dummy = counter.num)
    1. 实例化对象_effect = new ReactiveEffect(fn) (ReactiveEffect的实例上有方法run() )
    2. 执行_effect上的run()方法,在run方法内部会执行effect函数接收的那个箭头函数() => (dummy = counter.num)
  2. 在执行dummy = counter.num 时,由于counter是一个proxy对象(被reactive包装过),因此触发执行proxy拦截的get方法,也就是baseHandlers.ts里的createGetter(isReadonly = false, shallow = false)函数。
    1. 返回通过Reflect.get(target, key, receiver)拿到的数据。
    2. 由于该proxy实例时handler参数为mutableHandlers,因此未对createGetter的isReadonly设置为true。对于不是只读的对象(也就是说可以被set的proxy对象)需要进行依赖收集。
    3. 执行依赖收集函数track(target, "get", key)
  3. 进入effect.ts中的track(target, type, key)函数。然后执行trackEffects(dep); 将所有的activeEffect收集到dep中。

counter.num = 7;对应的就是reactivity流程中的update

整体流程就是

  1. 修改proxy对象的值,触发proxy拦截的set方法,也就是baseHandlers.ts里的createSetter()函数。
    1. Reflect.set(target, key, value, receiver),通过set函数返回Reflect.set来修改对象内部属性值。
    2. 执行trigger(target, "set", key);
  2. 执行effect.ts中的trigger函数触发依赖、更新依赖。从全局属性targetMap中取出对应对象的depsMap,然后再根据被修改属性取出对应的dep。把dep中的一个或多个ReactiveEffect放入effects数组中。然后执行triggerEffects(createDep(effects)); 将effects中所有effect进行执行, effect.run() 。 (因为effect实际上就是ReactiveEffect实例对象。)实际上就是执行了effect内部的fn() => (dummy = counter.num),在run方法执行的过程中会将当前ReactiveEffect实例放到全局数据activeEffect上,同时counter.num会再次触发proxy对象的get操作。
  3. 触发get操作之后又会再次触发track收集依赖,此时会更新targetMap中对应属性的dep。dep.add(activeEffect); ,若是新增的依赖,会将之前set时存放在全局变量activeEffect上的ReactiveEffect实例新增到dep上。