vue3 源码解析(1)— reactive 响应式实现

前言

本文是 vue3 源码分析系列的第一篇文章,主要介绍 vue3 的响应式原理,基于项目代码的v3.2.10 版本。本文将通过一个简单的例子,演示vue3 如何使用 reactive 函数和effect 函数实现数据代理、依赖跟踪和自动更新机制。

reactive 的基本用法

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>reactive</title>
</head>
<body>
<div id="app"></div>
<!--响应式模块的代码-->
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>
  let { reactive, effect } = VueReactivity;
  const user = {
    name: 'Alice',
    age: 25,
    address: {
      city: 'New York',
      state: 'NY'
    }
  };
  let state = reactive(user)

  effect(() => {
    app.innerHTML = state.address.city
  });
  setTimeout(() => {
    state.address.city = 'California'
  }, 1000);
</script>
</body>
</html>

通过例子可以看到1s之后改变数据视图也跟随变,在 vue3 中是那如何实现这一效果的呢?我们先从例子中的 reactive 函数出发。

reactive

reactive 函数是vue3提供的一个核心函数,它可以接收一个对象作为参数,返回一个对象的响应式代理。

function reactive(target) {
  // 如果target不是一个对象,直接返回
  if (target && typeof target !== 'object') {
    console.warn(`value cannot be made reactive: ${String(target)}`)
    return target
  }
  // 调用createReactiveObject函数,根据对象的类型,创建不同的代理处理器
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

reactive 函数首先会判断 target 是否是一个对象,如果不是就直接返回不进行代理。这是因为 Proxy 对象只能代理对象类型的值,不能代理基本类型的值。如果 target 是一个对象,就会调用 createReactiveObject 函数,根据对象的类型,创建不同的代理处理器。

createReactiveObject

createReactiveObject 函数是一个内部函数,它根据对象的类型,创建不同的代理处理器。

function createReactiveObject(
  target,
  isReadonly,
  baseHandlers,
  collectionHandlers
) {
  // 根据target的类型,选择合适的代理处理器
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // 创建一个Proxy对象,返回给reactive函数
  const proxy = new Proxy(target, handlers)
  // 返回代理对象
  return proxy
}

createReactiveObject 函数会根据 target 的类型,选择合适的代理处理器。接下来,我们来看看代理处理器的定义。

代理处理器

vue3提供了两种代理处理器:baseHandlers 和 collectionHandlers。baseHandlers 用于代理普通对象,collectionHandlers 用于代理数组。我们先来看看 baseHandlers 的定义。

baseHandlers

baseHandlers是一个用于代理普通对象的代理处理器,它包含了get、set、deleteProperty等方法,用于拦截对象的属性访问和修改。

const baseHandlers: ProxyHandler<any> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

可以看到,baseHandlers 是一个对象,它的属性是一些函数,这些函数就是代理处理器的方法。接下来,我们来看看这些方法的具体实现。

get

get 方法是一个用于拦截对象的属性读取的方法,它接收三个参数:target、key和receiver。get方法的定义如下:

function get(target: Target, key: string | symbol, receiver: object) {
  // 获取target的属性值
  const targetIsArray = isArray(target)
  const res = targetIsArray ? target[key as number] : Reflect.get(target, key, receiver)

  // 如果key是一个symbol,或者是一个不可变的属性,直接返回属性值
  if (isSymbol(key) || !isWriteable(key)) {
    return res
  }
  // 调用track函数,建立依赖关系
  track(target, TrackOpTypes.GET, key)
  // 如果属性值是一个对象,返回一个响应式代理³[3]
  return isObject(res)
    ? isReadonly
      ? // need to lazy access readonly and reactive here to avoid circular
        // dependency
        readonly(res)
      : reactive(res)
    : res
}

get 方法会判断 key 是否是一个 symbol,或者是一个不可变的属性,如果是,就直接返回属性值。这是为了防止对一些内置的或者只读的属性进行代理。然后,get 方法会调用 track 函数,建立依赖关系。如果属性值是一个对象,就返回一个响应式代理。这是为了实现深层次的响应式。接下来,我们来看看 track 函数的定义。

track

track 函数是一个用于建立依赖关系的函数,它接收三个参数:target、type和key。track函数的定义如下:

function track(target: object, type: TrackOpTypes, key: unknown) {
  // 如果当前没有激活的effect函数,直接返回
  if (!activeEffect) {
    return
  }
  // 获取targetMap,如果不存在,就创建一个新的WeakMap对象
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 获取keyMap,如果不存在,就创建一个新的Map对象
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // 如果dep中没有当前的effect函数,就添加到dep中,并将target和key添加到effect函数的依赖集合中
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

track 函数首先会判断当前是否有激活的 effect 函数,如果没有,就直接返回,不进行依赖的建立。这是为了防止对一些不需要响应式的属性进行跟踪。接着,track 函数会获取targetMap,targetMap 是一个全局变量,用于存储对象和属性的映射关系。然后,track 函数会获取 keyMap,keyMap 是一个 Map 对象,用于存储属性和函数的映射关系。最后,track 函数会将 target 和 key 添加到 effect 函数的依赖集合中。这样,就完成了依赖的建立。接下来,我们来看看 effect 函数的定义。

effect

effect 函数是一个用于创建响应式函数的函数,它接收一个函数作为参数,返回一个包装后的函数。effect函数的定义如下:

function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions): ReactiveEffect<T> {
  // 创建一个新的effect函数,调用run方法执行原始的函数
  const _effect = new ReactiveEffect(fn, NOOP, () => {
    if (_effect.dirty) {
      _effect.run()
    }
  })
  // 如果options中没有设置lazy为true,就立即执行effect函数
  if (!options || !options.lazy) {
    _effect.run()
  }
  
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

effect 函数会创建一个新的 effect 函数,调用 run 方法执行原始的函数。run 方法是一个内部方法,它会设置 activeEffect 为当前的 effect 函数,并使用 try…finally 语句保证 activeEffect 的恢复。函数 ReactiveEffect 不是本文的重点。在 computed 章节会详细解释。接下来,我们来看看 set 方法的定义。

set

set 方法是一个用于拦截对象的属性修改的方法,它接收四个参数:target、key、value和receiver。set方法的定义如下:

function set(target: object, key: string | symbol, value: unknown, receiver: object): boolean {
  // 获取target的旧值
  const oldValue = target[key as any]
  // 判断target是否是一个数组,且key是否是一个合法的下标
  const hadKey =
    isArray(target) && isIntegerKey(key)
      ? Number(key) < target.length
      : hasOwn(target, key)
  // 设置target的属性值,如果成功,返回true,否则返回false
  const result = Reflect.set(target, key, value, receiver)
  // 如果target是一个原始对象,直接返回结果
  if (target === toRaw(receiver)) {
    if (!hadKey) {
      // 如果target之前没有这个属性,就触发添加操作
      trigger(target, TriggerOpTypes.ADD, key, value)
    } else if (hasChanged(value, oldValue)) {
      // 如果target的属性值发生了变化,就触发更新操作
      trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }
    return result
  } else {
    // 如果target是一个代理对象,就触发更新操作
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    return result
  }
}

set 方法会根据 target 是否有这个属性,以及属性值是否发生了变化,触发不同的操作。如果 target 之前没有这个属性,就触发添加操作;如果 target 的属性值发生了变化,就触发更新操作。接下来,我们来看看 trigger 函数的定义。

trigger

trigger 函数是一个用于触发响应式更新的函数,它接收五个参数:target、type、key、newValue和oldValue。trigger函数的定义如下:

function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown
) {
  // 获取targetMap,如果不存在,直接返回
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  // 创建一个新的Set对象,用于存储需要执行的函数
  const effects = new Set<ReactiveEffect>()
  // 定义一个函数,用于将dep中的函数添加到effects中
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        effects.add(effect)
      })
    }
  }
  // 根据type和key,选择需要添加的函数
  if (type === TriggerOpTypes.CLEAR) {
    // 如果type是清空操作,就将所有的函数添加到effects中
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    // 如果key是length,且target是一个数组,就将大于新值的下标对应的函数添加到effects中
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // 否则,就将key对应的函数添加到effects中
    add(depsMap.get(key))
  }
  // 遍历effects中的函数,依次执行,完成更新
  effects.forEach(effect => {
    effect()
  })
}

trigger 函数首先会获取 targetMap,如果不存在,就直接返回,不进行更新。这是为了防止对一个没有依赖的对象进行更新。接着,trigger 函数会创建一个新的 Set 对象,用于存储需要执行的函数。然后,trigger函数会定义一个函数,用于将 dep 中的函数添加到 effects 中。最后,trigger函数会遍历effects中的函数,依次执行。

collectionHandlers

collectionHandlers 是一个用于代理数组的代理处理器,它继承了 baseHandlers,但是重写了get方法,用于拦截数组的特殊方法,如push、pop、splice等。collectionHandlers 的定义如下:

const collectionHandlers: ProxyHandler<any> = extend({}, baseHandlers, {
  get(target: Target, key: string | symbol, receiver: object) {
    // 获取target的属性值
    const res = Reflect.get(target, key, receiver)
    // 如果key是一个symbol,或者是一个不可变的属性,直接返回属性值
    if (isSymbol(key) || !isWriteable(key)) {
      return res
    }
    // 如果key是一个数组的特殊方法,返回一个包装后的函数
    if (mutateMethods.has(key)) {
      return function(...args) {
        // 获取target的旧值
        const oldTarget = toRaw(target)
        // 获取target的旧长度
        const oldLength = oldTarget.length
        // 调用原始的方法,获取返回值
        const result = res.apply(target, args)
        // 获取target的新长度
        const newLength = target.length
        // 调用track函数,建立依赖关系
        track(target, TrackOpTypes.GET, key)
        // 根据key和参数,触发不同的操作
        switch (key) {
          case 'push':
          case 'unshift':
            // 如果key是push或unshift,就触发添加操作
            trigger(target, TriggerOpTypes.ADD, oldLength, args)
            break
          case 'pop':
          case 'shift':
            // 如果key是pop或shift,就触发删除操作
            trigger(target, TriggerOpTypes.DELETE, oldLength - 1, void 0)
            break
          case 'splice':
            // 如果key是splice,就根据参数,触发添加或删除操作
            if (args.length > 2) {
              trigger(target, TriggerOpTypes.ADD, oldLength, args.slice(2))
            }
            if (args.length > 0) {
              trigger(target, TriggerOpTypes.DELETE, oldLength - 1, void 0)
            }
            break
        }
        // 返回结果
        return result
      }
    } else {
      // 否则,调用baseHandlers的get方法,返回属性值
      return baseHandlers.get(target, key, receiver)
    }
  }
})

可以看到,collectionHandlers 首先会获取 target 的属性值,然后判断key是否是一个 symbol,或者是一个不可变的属性,如果是,就直接返回属性值。接着,collectionHandlers 会判断key是否是一个数组的特殊方法,如果是,就返回一个包装后的函数。这个函数会在调用原始的方法之前,获取 target 的旧值和旧长度,在调用原始的方法之后,获取 target 的新长度,并根据 key 和参数,触发不同的操作。这是为了实现数组的响应式。最后,如果 key 不是一个数组的特殊方法,就调用baseHandlers 的get方法,返回属性值。这是为了复用 baseHandlers 的逻辑。这样,就完成了collectionHandlers 的定义。

总结

总结下以上的内容:

  • reactive:函数返回一个对象的响应式代理。reactive 函数会调用createReactiveObject函数,根据对象的类型,创建不同的代理处理器。reactive函数的参数必须是一个对象,否则会报错。

  • createReactiveObject:函数根据对象的类型,创建不同的代理处理器。如果对象是一个数组,会创建一个 collectionHandlers 对象;如果对象是一个普通对象,会创建一个 baseHandlers对象。代理处理器是一个包含 get、set、deleteProperty 等方法的对象,用于拦截对象的属性访问和修改。

  • effect:函数接收一个函数作为参数,返回一个包装后的函数。effect函数会调用track函数,将当前的函数和当前访问的属性建立依赖关系。effect函数还会调用trigger函数,当依赖的属性发生变化时,触发函数的重新执行。

  • track:将当前的函数和当前访问的属性建立依赖关系。track函数会使用一个全局变量activeEffect,存储当前的函数。track函数还会使用一个全局变量targetMap,存储对象和属性的映射关系。track函数会检查targetMap中是否有当前对象,如果没有,就创建一个新的Map对象,用于存储属性和函数的映射关系。track函数会检查属性Map中是否有当前属性,如果没有,就创建一个新的Set对象,用于存储依赖的函数。track函数会将activeEffect添加到Set中,完成依赖的建立。

  • trigger:当依赖的属性发生变化时,触发函数的重新执行。trigger函数会使用targetMap,根据对象和属性,找到对应的函数Set。trigger函数会遍历Set中的函数,依次执行,完成更新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值