vue3源码分析 -- reactive

案例

通过以下这个案例来进行理解:

复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const { reactive, effect } = Vue
      
      const obj = reactive({
        name: 'first',
        age: 18
      })
      
      effect(() => {
        document.querySelector('#app').innerHTML = obj.name
      })

      setTimeout(() => {
        obj.name = 'last'
      }, 2000)
    </script>
  </body>
</html>

首先引入 reactive 和 effect 两个函数,创建了一个响应式对象obj,并使用 effect 注册了一个副作用函数,该函数将 obj.name 的值渲染到页面上,2 秒后,我们修改了 obj.name 的值,页面也随之更新


createReactiveObject方法

reactive函数在packages/reactivity/src/reactive.ts文件下,它的主要作用是将普通对象转换为响应式对象

reactive函数实际执行的是 createReactiveObject 方法,该方法也在该文件下

  • target:我们需要传入的对象
  • isReadonly:要创建的代理是否为只可读
  • baseHandlers 是对基本类型的劫持,即 [Object, Array]
  • collectionHandlers 是对集合类型的劫持,即 [Set, Map, WeakMap, WeakSet]
  • proxyMap:WeakMap 类型,用于缓存已经创建的代理对象

如果 target 不是对象类型,直接返回原值,只有对象类型的数据才能被响应式系统处理;如果 target 已经是 Proxy 类型,并且 isReadonly 为 false,直接返回目标对象本身,因为 Proxy 对象已经被处理过,无需重复处理

该函数实际做了 proxyMap 缓存处理,最终返回一个 proxy 实例对象 。这里主要关注new Proxy这段代码,第一个参数target为传进来的对象,即{ name: 'first', age: 18 },第二个baseHandlers参数即传入的 mutableHandlers 对象 ,该对象定义在packages/reactivity/src/baseHandlers.ts中:

该对象定义了 get、set 等方法,从而对传入的数据进行依赖收集和依赖触发 ,先看下结果后面再对这部分逻辑进行分析,reactive函数执行完毕,obj 得到了一个 proxy 的实例对象 ,接下来执行effect方法

effect方法

该方法定义在packages/reactivity/src/effect.ts文件中,它用于创建副作用函数,能够自动追踪其内部依赖的响应式数据变化,并在数据变更时重新执行 ,它是watchcomputed等 API 的底层实现基础

(可以这样理解:副作用函数就像是一个"观察者",它观察着某些数据的变化,一旦数据变化,它就会采取行动

fn 是需要被追踪的副作用函数,若 fn 已被 effect 包装过,则提取其原始函数。声明一个构造函数ReactiveEffect的实例对象_effect,执行构造函数中的run方法

ReactiveEffect构造函数如下:

  • active:表示该副作用是否活跃,默认为true
  • deps:一个数组,用于存储该副作用函数所依赖的依赖项
  • parent:表示该副作用的父副作用,默认为undefined

run()执行流程:若activefalse,直接执行fn但不收集依赖。设置activeEffect = this,标记当前活跃的副作用,运行 fn() ,触发响应式数据的 get 拦截器,完成依赖收集

stop():标记active = false,若当前正在执行自身(activeEffect === this),标记deferStop,待执行结束后清理;调用cleanupEffect(this)从所有依赖集合中移除当前副作用

复制代码
export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined
  // 省略
  
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }

  run() {
    if (!this.active) {
      return this.fn()
    }
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    try {
      this.parent = activeEffect
      activeEffect = this
      shouldTrack = true
      // 省略
      // 执行 fn 函数 即传入的匿名函数
      //  () => {
      //     document.querySelector('#app').innerHTML = obj.name
      //  }
      return this.fn()
    } finally {
      // 省略
      if (this.deferStop) {
        this.stop()
      }
    }
  }

  stop() {
    // stopped while running itself - defer the cleanup
    if (activeEffect === this) {
      this.deferStop = true
    } else if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

这里接收一个 fn 方法即传入的匿名函数,然后设置 active、deps、parent 等属性:

之后执行_effect.run(),即执行构造函数ReactiveEffectrun方法,需要关注activeEffect = this,此时被赋值为:

然后执行fn函数,即执行传入的匿名函数,之后执行document.querySelector('#app').innerHTML = obj.name触发objget方法

get/set

get方法上述中被定义在packages/reactivity/src/baseHandlers.ts文件中,实际触发的是createGetter函数,主要关注**track(target, TrackOpTypes.GET, key)** 这段代码,它是对数据的依赖收集, 也是get方法的核心

复制代码
const get = /*#__PURE__*/ createGetter()

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    // 省略

    const targetIsArray = isArray(target)

    // 省略
    // Reflect API
    // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
    // Reflect.get() 等同于 res = target[key]
    // Reflect 用来替代直接调用 Object 的方法
    const res = Reflect.get(target, key, receiver)

    // 省略

    if (!isReadonly) {
      // 核心,添加依赖收集
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

track函数被定义在packages/reactivity/src/effect.ts文件中,这里的targetMapWeakMap对象,该对象是一个弱引用类型

以传入的对象{ name: 'first', age: 18}作为keyvalue 值为Map对象,之后设置depsMapkey 当前为namevalueSet对象,最后执行trackEffects(dep, eventInfo)

trackEffects是用于依赖收集的核心函数,将当前活跃的副作用函数 activeEffect 与依赖集合 dep 关联起来,从而实现当依赖数据变化时能够触发副作用函数的重新执行

此时再看下targetMap对象数据:

这样就完成了数据的依赖收集,之后就可以通过指定对象指定属性获取到对应的 fn 方法 。而依赖收集本质上就是targetMapReactiveEffect之间的关联

createGetter执行完毕返回对应的值,当前为 first:

两秒后执行obj.name = 'last',触发set方法,该方法定义在packages/reactivity/src/baseHandlers.ts中,主要关注trigger(target, TriggerOpTypes.SET, key, value, oldValue)这行代码,键存在且新值与旧值不同,触发 set 类型的依赖更新

trigger方法

该方法被定义在packages/reactivity/src/effect.ts文件中:

复制代码
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 根据传入的对象 获取 对应的 Map 对象
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      // 根据属性获取对应的 ReactiveEffect
      deps.push(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      // 省略
      // 当前为 set 类型
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  const eventInfo = __DEV__
    ? { target, type, key, newValue, oldValue, oldTarget }
    : undefined

  if (deps.length === 1) {
    if (deps[0]) {
      if (__DEV__) {
        triggerEffects(deps[0], eventInfo)
      } else {
        triggerEffects(deps[0])
      }
    }
  } else {
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

根据指定对象获取到对应的Map对象,此时depsMap为:

之后再根据指定属性获取对应的ReactiveEffect,再添加到deps中,此时deps为:

后面我们只需关注triggerEffects(deps[0], eventInfo) 这行代码, triggerEffects函数也packages/reactivity/src/effect.ts文件中:

可以看出triggerEffects函数实际先获取到 effect 数组,遍历数组执行每个 effect.run() ,实际执行的是 fn 方法 ,该方法是最初依赖收集时传入的匿名函数,之后再次触发getter方法,从而进行赋值,至此整个依赖触发完成

总结

1)reactive 函数与 createReactiveObject 方法

reactive函数实际执行了createReactiveObject方法,该方法主要创建了一个Proxy实例对象,给代理对象添加了gettersetter行为

gettersetter方法定义在mutableHandlers对象中,用于拦截对象属性的获取和设置操作

2)get 方法与依赖收集

get方法实际执行了createGetter方法,在这个过程中,track函数被调用,用于进行依赖收集

依赖收集的过程是构建一个WeakMaptargetMap)对象,完成指定对象obj的指定属性nameeffect的依赖收集工作

3)effect 函数与 ReactiveEffect 实例

effect函数实际创建了一个ReactiveEffect实例,该构造函数接收一个fn函数(即传进来的匿名函数),该回调函数必须暴露getter行为

ReactiveEffectrun函数中,给activeEffect赋值,并执行fn函数

4)依赖收集的具体过程

当访问obj.name时,getter被触发,激活track方法,该方法构建WeakMaptargetMap)对象,记录下当前活跃的副作用函数与obj.name的依赖关系

5)set 方法与依赖触发

set方法实际执行了createSetter方法,触发trigger函数进行依赖触发

trigger函数从之前targetMap依赖收集的对象中获取,根据key(即name)获取到对应的副作用函数,然后执行fn函数,从而完成一个依赖触发的过程

6)依赖触发的具体过程

当修改obj.name时,setter被触发,激活trigger方法,它通知所有依赖于obj.name的副作用函数重新执行,从而更新视图


reactive缺陷:

  • 解构后不支持响应性
  • 不支持基本类型,只能是对象
相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax