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缺陷:

  • 解构后不支持响应性
  • 不支持基本类型,只能是对象
相关推荐
小莫爱编程11 分钟前
HTML,CSS,JavaScript
前端·css·html
陈大鱼头1 小时前
AI驱动的前端革命:10项颠覆性技术如何在LibreChat中融为一体
前端·ai 编程
Gazer_S1 小时前
【解析 ECharts 图表样式继承与自定义】
前端·信息可视化·echarts
剪刀石头布啊1 小时前
视觉格式化模型
前端·css
一 乐1 小时前
招聘信息|基于SprinBoot+vue的招聘信息管理系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·招聘系统
念九_ysl1 小时前
Vue3 + ECharts 数据可视化实战指南
前端·信息可视化·echarts
Gazer_S1 小时前
【Auto-Scroll-List 组件设计与实现分析】
前端·javascript·数据结构·vue.js
前端加油站2 小时前
前端开发人员必备的Mac应用
前端
庸俗今天不摸鱼2 小时前
【万字总结】前端全方位性能优化指南(四)——虚拟DOM批处理、文档碎片池、重排规避
前端·性能优化·dom
harry7592 小时前
React 18+ 安全访问浏览器对象终极指南:从原理到生产级解决方案
前端·javascript