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


    前言

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

    reactive 的基本用法

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>reactivetitle>
    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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    通过例子可以看到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
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    代理处理器

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

    baseHandlers

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

    const baseHandlers: ProxyHandler<any> = {
      get,
      set,
      deleteProperty,
      has,
      ownKeys
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以看到,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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    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()
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    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)
        }
      }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    可以看到,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中的函数,依次执行,完成更新。

  • 相关阅读:
    NestedConfigurationProperty的作用
    JVM内部世界(内存划分,类加载,垃圾回收)
    密码学【一】
    【CC3200AI 实验教程 1】疯壳·AI语音人脸识别(会议记录仪/人脸打卡机)-开发环境搭建
    电脑卡怎么办?4招帮你解决电脑卡顿的烦恼!
    Linux命令
    vscode使用delve调试golang程序
    【自然语言处理】seq2seq模型—机器翻译
    Go解密之路——GPM
    git命令
  • 原文地址:https://blog.csdn.net/AXBNMD/article/details/133919403