Vue3 使用 Proxy 来实现响应式系统,Proxy 可以允许我们定义对象或者函数的自定义行为,例如属性查找,赋值,枚举,函数调用等。Vue3 借助 Proxy 的 handler 拦截操作,收集依赖,数据变更触发依赖更新,实现了响应系统核心。

在 Vue3 中,可以使用 reactive() 创建一个响应状态

import { reactive } from 'vue'

// reactive state
const state = reactive({
  desc: 'Hello World!',
  count: 0
});

创建响应式对象

我们在源码 core/packages/reactivity/src/reactive.ts 文件中看到了 reactive 代码实现,通过 reactReactiveObject 创建响应式数据。

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>,
) {
  // ...
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,
  )
  proxyMap.set(target, proxy)
  return proxy
}

baseHandlers 和 collectionHandlers 分别用来处理普通对象和集合类型(如 Map、Set 等)的代理逻辑。

baseHandlers

baseHandlers 处理普通对象的代理逻辑。它定义了对象属性的 getsetdeleteProperty 等操作的行为。以下是一个简化的示例:

export const mutableHandlers: ProxyHandler<object> = {
  get(target, key, receiver) {
    // 处理属性读取
    const res = Reflect.get(target, key, receiver);
    // 依赖收集逻辑
    track(target, key);
    return res;
  },
  set(target, key, value, receiver) {
    // 处理属性设置
    const result = Reflect.set(target, key, value, receiver);
    // 触发更新逻辑
    trigger(target, key);
    return result;
  },
  deleteProperty(target, key) {
    // 处理属性删除
    const result = Reflect.deleteProperty(target, key);
    // 触发更新逻辑
    trigger(target, key);
    return result;
  }
};

collectionHandlers

collectionHandlers 处理集合类型(如 Map、Set 等)的代理逻辑。它定义了集合的 getsetadddelete 等操作的行为。以下是一个简化的示例:

export const mutableCollectionHandlers: ProxyHandler<object> = {
  get(target, key, receiver) {
    // 处理集合的属性读取
    const res = Reflect.get(target, key, receiver);
    // 依赖收集逻辑
    track(target, key);
    return res;
  },
  set(target, key, value, receiver) {
    // 处理集合的属性设置
    const result = Reflect.set(target, key, value, receiver);
    // 触发更新逻辑
    trigger(target, key);
    return result;
  },
  add(target, value) {
    // 处理集合的添加操作
    const result = target.add(value);
    // 触发更新逻辑
    trigger(target, value);
    return result;
  },
  delete(target, key) {
    // 处理集合的删除操作
    const result = target.delete(key);
    // 触发更新逻辑
    trigger(target, key);
    return result;
  }
};

上述的简化代码看出,在访问属性执行了 track 动作,操作(新增,删除,更新)属性执行了 trigger 动作,那么这两个动作分别做了哪些事情。

依赖收集

track

track 函数用于跟踪对响应式属性的访问。它会检查当前正在运行的 activeEffect 并将其记录为依赖项,这样当响应式属性发生变化时,相关的 activeEffect 就会被重新运行。

/**
 * 跟踪对响应式属性的访问。
 * 该函数会检查当前正在运行的 effect,并将其记录为依赖项,
 * 记录所有依赖于该响应式属性的 effects。
 * 
 * @param target - 代理的对象。
 * @param type - 属性的访问类型。
 * @param key - 响应式属性的标识符。
 */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep(() => depsMap!.delete(key))))
    }
    trackEffect(
      activeEffect,
      dep,
      __DEV__
        ? {
            target,
            type,
            key,
          }
        : void 0,
    )
  }
}

trackEffect

/**
 * 这个函数会检查当前正在运行的 effect,并将其记录到依赖于 reactive 的 dep 中。
 *
 * @param effect - 当前正在运行的 ReactiveEffect 实例。
 * @param dep - 记录所有依赖于 reactive property 的 effects 的依赖集合。
 */
export function trackEffect(
  effect: ReactiveEffect,
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
  // 如果 dep 中没有记录当前 effect 的 _trackId
  if (dep.get(effect) !== effect._trackId) {
    // 将当前 effect 的 _trackId 设置到 dep 中
    dep.set(effect, effect._trackId)
    
    // 获取 effect 的 deps 数组中当前索引位置的旧 dep
    const oldDep = effect.deps[effect._depsLength]
    
    // 如果旧 dep 不等于当前 dep
    if (oldDep !== dep) {
      // 如果旧 dep 存在,清理旧 dep 中的 effect
      if (oldDep) {
        cleanupDepEffect(oldDep, effect)
      }
      // 将当前 dep 存储到 effect 的 deps 数组中,并增加 _depsLength
      effect.deps[effect._depsLength++] = dep
    } else {
      // 如果旧 dep 等于当前 dep,仅增加 _depsLength
      effect._depsLength++
    }
  }
}

触发更新

trigger 过程主要用于通知依赖于某个响应式数据的副作用函数(effect)重新运行。

trigger

/**
 * 查找与目标对象(或特定属性)相关的所有依赖,并触发存储在这些依赖中的副作用。
 * @param target - 响应式对象。
 * @param type - 定义需要触发副作用的操作类型。
 * @param key - 可用于定位目标对象中的特定响应式属性。
 * @param newValue - 新值(可选)。
 * @param oldValue - 旧值(可选)。
 * @param oldTarget - 旧的 Map 或 Set 对象(可选)。
 */
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>,
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // 从未被追踪过
    return
  }
  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // 集合被清空
    // 触发目标对象的所有副作用
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === 'length' || (!isSymbol(key) && key >= newLength)) {
        deps.push(dep)
      }
    })
  } else {
    // 为 SET | ADD | DELETE 操作调度运行
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }
    // 也为 ADD | DELETE | Map.SET 操作的迭代键运行
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // 数组添加新索引 -> 长度变化
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
    }
  }
  // 触发所有收集到的副作用
  triggerEffects(deps, dirtyLevel, debuggerEventExtraInfo)
}

triggerEffects

/**
 * 触发依赖项的所有副作用。
 *
 * @param dep - 依赖项集合。
 * @param dirtyLevel - 当前的脏级别。
 * @param debuggerEventExtraInfo - 调试事件的额外信息(可选)。
 */
export function triggerEffects(
  dep: Dep,
  dirtyLevel: DirtyLevels,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
  // 暂停调度
  pauseScheduling()
  // 遍历依赖项中的所有副作用
  for (const effect of dep.keys()) {
    // dep.get(effect) 非常昂贵,我们需要懒计算并重用结果
    let tracking: boolean | undefined
    
    // 如果副作用的脏级别小于当前脏级别,并且跟踪状态未定义或为真
    if (
      effect._dirtyLevel < dirtyLevel &&
      (tracking ??= dep.get(effect) === effect._trackId)
    ) {
      // 如果副作用的脏级别为 NotDirty,则设置应调度标志
      effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
      // 更新副作用的脏级别
      effect._dirtyLevel = dirtyLevel
    }
    
    // 如果应调度标志为真,并且跟踪状态未定义或为真
    if (
      effect._shouldSchedule &&
      (tracking ??= dep.get(effect) === effect._trackId)
    ) {
      // 触发副作用
      effect.trigger()
      // 如果副作用未运行或允许递归,并且脏级别不为 MaybeDirty_ComputedSideEffect
      if (
        (!effect._runnings || effect.allowRecurse) &&
        effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
      ) {
        // 重置应调度标志
        effect._shouldSchedule = false
        // 如果副作用有调度器,则将其加入调度队列
        if (effect.scheduler) {
          queueEffectSchedulers.push(effect.scheduler)
        }
      }
    }
  }
  
  // 重置调度
  resetScheduling()
}

手写一个 reactvive 响应式系统

通过上述的概念,我们手写一个轻量的 reactive 响应式系统,当然 vue 里面包含了复杂的数据结构处理,边界情况。这里就不过多展开了。

// 当前激活的副作用函数
let activeEffect: (() => void) | undefined;

// Dep 类用来收集和通知副作用函数
class Dep {
  subscribers = new Set<() => void>(); // 存储副作用函数的集合

  // 依赖收集方法,将当前激活的副作用函数添加到订阅者集合中
  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect);
    }
  }

  // 通知方法,遍历所有订阅的副作用函数并执行
  notify() {
    this.subscribers.forEach((sub) => sub());
  }
}

// reactive 函数用来创建响应式对象
function reactive(target: any) {
  const depMap = new Map<string, Dep>(); // 存储属性名和 Dep 对象的映射

  return new Proxy(target, {
    get(obj, key) {
      if (!depMap.has(key as string)) {
        // 如果属性没有对应的 Dep 对象,则创建一个
        depMap.set(key as string, new Dep());
      }
      const dep = depMap.get(key as string);
      // 收集依赖
      dep?.depend();
      // 返回属性值
      return obj[key];
    },
    set(obj, key, value) {
      // 设置属性值
      obj[key] = value;
      // 获取属性对应的 Dep 对象
      const dep = depMap.get(key as string);
      // 如果属性有订阅者,则通知它们更新
      dep?.notify();
      return true;
    },
  });
}

// effect 函数用来创建副作用函数
function effect(fn: () => void) {
  const effectFn = () => {
    // 将当前副作用函数设置为激活状态
    activeEffect = effectFn;
    // 执行副作用函数
    fn();
    // 重置激活状态
    activeEffect = undefined;
  };
  // 立即执行副作用函数
  effectFn();
}

// 创建一个响应式对象 state
const state = reactive({
  count: 0,
});

// 定义一个副作用函数,它将在 state.count 发生变化时触发
effect(() => {
  console.log(`count is: ${state.count}`);
});

// 修改响应式对象的值,每次修改都会触发副作用函数
state.count++; // 触发副作用函数,输出 "count is: 1"
state.count++; // 再次触发副作用函数,输出 "count is: 2"

上述例子,当执行到副作用,会触发响应式对象的 get 回调,进行依赖收集,当变更 count 会触发响应式对象的 set 回调,触发 effect 执行。