从需求出发阅读 Vue 源码之响应式基础(Vue 3)

tao
发布于2024-04-19 | 更新于2024-04-19

在 JavaScript 中有两种劫持 property 访问的方式:getter / setter 和 Proxy。Vue 2 使用 getter / setter 完全是出于支持旧版本浏览器的限制。而在 Vue 3 中则使用了 Proxy 来创建响应式对象,仅将 getter / setter 用于 ref。

从数据变动时自动更新页面渲染的需求出发,我们在从需求出发阅读 Vue 源码之响应式基础(Vue 2)这篇笔记中,了解到 Vue 2 使用了 getter / setter 劫持 property 访问,现在还是针对同样的需求看一下 Vue 3 是如何做的。

先来把测试程序修改一下,让程序在挂载后延迟 2 秒更改 message 的值:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue Reactivity</title>
    <script src="../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <div>{{ message }}</div>
    </div>
  </body>

  <script>
    const { createApp } = Vue

    const app = createApp({
      data: () => ({
        message: 'hello vue!',
      }),
      mounted() {
        setTimeout(() => {
          this.message += ' hello world!'
        }, 2000)
      },
    })

    app.mount('#app')
  </script>
</html>

打开 packages/runtime-core/src/componentOptions.ts,我们曾在从需求出发阅读 Vue 源码之声明式渲染(Vue 3)中提到过,第 610 行 applyOptions 函数会在 728 行执行 reactive 函数以此来实现响应式。

import {
  reactive,
  ......
} from '@vue/reactivity'

export function applyOptions(instance: ComponentInternalInstance) {
  ......
  instance.data = reactive(data)
  ......
}

打开 packages/reactivity/src/reactive.ts,第 83 行定义并导出了 reactive 函数,在函数中执行了 createReactiveObject 并传入了目标对象和处理器对象等参数。

......

import {
  mutableHandlers,
  ......
} from './baseHandlers'
import {
  mutableCollectionHandlers,
  ......
} from './collectionHandlers'

......

export const reactiveMap = new WeakMap<Target, any>()

......

export function reactive(target: object) {
  ......
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

createReactiveObject 函数中创建 Proxy 对象时对目标对象的类型作了判断,对于 Object、Array 使用 baseHandlers 处理器,对于 Map、Set、WeakMap、WeakSet 则使用 collectionHandlers 处理器。Proxy 对象创建成功后,会存储在 reactiveMap 中,键名是目标对象。

import { ...... toRawType ...... } from '@vue/shared'
// export const objectToString = Object.prototype.toString
// export const toTypeString = (value: unknown): string =>
//   objectToString.call(value)
// export const toRawType = (value: unknown): string => {
  // // extract "RawType" from strings like "[object RawType]"
  // return toTypeString(value).slice(8, -1)
// }

......

export const enum ReactiveFlags {
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  IS_SHALLOW = '__v_isShallow',
  RAW = '__v_raw'
}

......

const enum TargetType {
  INVALID = 0,
  COMMON = 1,
  COLLECTION = 2
}

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}
......
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  ......
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only specific value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}
......

接下来打开 packages/reactivity/src/baseHandlers.ts 来看一下处理器对象的实现,第 88 行定义了基础响应式处理器类,在基础响应式处理器类中定义了取值函数,在取 data 对象属性值时,会用 track 函数进行跟踪依赖,并判断值是否为是对象,是则继续递归地设置响应性,最后返回取到的对象属性值。

......

class BaseReactiveHandler implements ProxyHandler<Target> {
  constructor(
    protected readonly _isReadonly = false,
    protected readonly _shallow = false
  ) {}

  get(target: Target, key: string | symbol, receiver: object) {
    const isReadonly = this._isReadonly,
      shallow = this._shallow

    ......

    const res = Reflect.get(target, key, receiver)

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    ......

    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
  }
}

打开 packages/reactivity/src/effect.ts,第 247 行定义并导出了 track 函数,第 255 行执行了 createDep 函数,这里能看出一个属性 key 对应一个 dep 依赖集合,和 Vue 2 有相似之处。第 262 行执行了 trackEffects 函数跟踪副作用。

......
const targetMap = new WeakMap<object, KeyToDepMap>()
......
export let activeEffect: ReactiveEffect | undefined
......
export let shouldTrack = true
......
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()))
    }

    ......

    trackEffects(dep, ......)
  }
}
......

打开 packages/reactivity/src/dep.ts,第 21 行定义并导出了 createDep 函数。

......
export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
  dep.w = 0
  dep.n = 0
  return dep
}
......

回到 packages/reactivity/src/effect.ts,第 266 行定义并导出了 trackEffects 函数,函数执行时会在 dep 集合中加入 activeEffect 当前副作用,并在 activeEffectdeps 数组中添加 dep,这一步和 Vue 2 的依赖收集也有相似之处。

......
export function trackEffects(
  dep: Dep,
  ......
) {
  ......

  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    ......
  }
}
......

那当前副作用 activeEffect 是什么时候产生的呢?这个在从需求出发阅读 Vue 源码之声明式渲染(Vue 3)已有提到,打开 packages/runtime-core/src/renderer.ts,在执行第 1182 行的 mountComponent 函数时在第 1242 行执行了 setupRenderEffect 函数,又在setupRenderEffect 函数中第 1545 行创建了 ReactiveEffect 类实例(一个应用(app)对应了一个ReactiveEffect 类实例,和 Vue 2 也有相似之处)。随后,在执行 effectrun 方法时将当前实例对象赋值给了 activeEffect

......
    const effect = (instance.effect = new ReactiveEffect(
      ......
    ))

    const update: SchedulerJob = (instance.update = () => effect.run())
    ......
    update()
......

packages/reactivity/src/effect.ts 中第 100 行将当前 ReactiveEffect 类实例赋值给了当前副作用 activeEffect

......
export let activeEffect: ReactiveEffect | undefined
......
export class ReactiveEffect<T = any> {
  ......
  run() {
    ......
      activeEffect = this
    ......
}
......

回到 packages/reactivity/src/baseHandlers.ts,第 165 行定义了 MutableReactiveHandler 类,这个类继承了 BaseReactiveHandler 类,扩展了 set 等方法,在 set 方法中除了设置值之外,在对应值发生变化时会执行 trigger 函数触发副作用。

......
class MutableReactiveHandler extends BaseReactiveHandler {
  constructor(shallow = false) {
    super(false, shallow)
  }

  set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {

    ......

    const result = Reflect.set(target, key, value, receiver)
    
    ...... if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
    }
    ......

    return result
  }

  ......
}
......

打开 packages/reactivity/src/effect.ts,第 305 行定义并导出了函数 trigger,在函数中获取上文 depsMap 中已添加的依赖项,并使用 triggerEffects 触发副作用。

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) {
    // never been tracked
    return
  }

  let deps: (Dep | undefined)[] = []
  ......
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }
  ......

  if (deps.length === 1) {
    if (deps[0]) {
      ......
        triggerEffects(deps[0])
      ......
    }
  }
}

第 393 行定义并导出了 triggerEffects 函数,并在其中继续调用 triggerEffect 触发副作用。

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  ......
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    ......
      triggerEffect(effect, ......)
    ......
  }
  ......
}

在第 411 行定义了函数 triggerEffect,最终其会执行上文提到的副作用 effectrun 方法去更新页面渲染,至于在更新过程中的异步更新队列、diff 算法、nextTick 等我们在之后的笔记中再去记录。

function triggerEffect(
  effect: ReactiveEffect,
  ......
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    ......
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

在这一 track 跟踪和 trigger 触发的整个过程也应用到了发布订阅设计模式

接下来,在 packages/reactivity/src/baseHandlers.ts 中使用 MutableReactiveHandler 类创建实例并赋值给 mutableHandlers 导出,到这里处理器对象就创建成功了。

......
export const mutableHandlers: ProxyHandler<object> =
  /*#__PURE__*/ new MutableReactiveHandler()
......

打开 packages/runtime-core/src/componentOptions.ts,第 610 是 applyOptions 函数,第 620 行和第 801 行分别判断是否传入了 beforeCreate 函数和 created 函数,是的话则执行,而 mounted 函数则需通过 registerLifecycleHook 注册。

......
import {
  ......
  onMounted,
  ......
} from './apiLifecycle'
......
export function applyOptions(instance: ComponentInternalInstance) {
  ......
  function registerLifecycleHook(
    register: Function,
    hook?: Function | Function[]
  ) {
    if (isArray(hook)) {
      hook.forEach(_hook => register(_hook.bind(publicThis)))
    } else if (hook) {
      register(hook.bind(publicThis))
    }
  }

  ......
  registerLifecycleHook(onMounted, mounted)
  ......
}
......

打开 packages/runtime-core/src/apiLifecycle.ts,第 74 行通过调用 createHook 方法返回一个函数赋值给 onMounted,执行 onMounted 时实则是将修改实例 instance 属性 m 值,如果 instance.m 不存在则置为空数组,然后将经过包裹的 wrappedHook 方法添加到 instance.m 数组中等待执行,包裹的目的为暂停依赖收集、设置当前实例和错误处理。

......
export function injectHook(
  type: LifecycleHooks,
  hook: Function & { __weh?: Function },
  target: ComponentInternalInstance | null = currentInstance,
  prepend: boolean = false
): Function | undefined {
  if (target) {
    const hooks = target[type] || (target[type] = [])
    // cache the error handling wrapper for injected hooks so the same hook
    // can be properly deduped by the scheduler. "__weh" stands for "with error
    // handling".
    const wrappedHook =
      hook.__weh ||
      (hook.__weh = (...args: unknown[]) => {
        if (target.isUnmounted) {
          return
        }
        // disable tracking inside all lifecycle hooks
        // since they can potentially be called inside effects.
        pauseTracking()
        // Set currentInstance during hook invocation.
        // This assumes the hook does not synchronously trigger other hooks, which
        // can only be false when the user does something really funky.
        setCurrentInstance(target)
        const res = callWithAsyncErrorHandling(hook, target, type, args)
        unsetCurrentInstance()
        resetTracking()
        return res
      })
    if (prepend) {
      hooks.unshift(wrappedHook)
    } else {
      hooks.push(wrappedHook)
    }
    return wrappedHook
  }
  ......
}

export const createHook =
  <T extends Function = () => any>(lifecycle: LifecycleHooks) =>
  (hook: T, target: ComponentInternalInstance | null = currentInstance) => ...... 
  injectHook(lifecycle, (...args: unknown[]) => hook(...args), target)

......

export const onMounted = createHook(LifecycleHooks.MOUNTED)
// export const enum LifecycleHooks {
//   ......
//   MOUNTED = 'm',
//   ......
// }

......

对于 beforeMount 也是类似处理,这样在 packages/runtime-core/src/renderer.ts 第 1310 行判断 bm 是否存在,是则调用执行,第 1390 行判断 m 是否存在,是则放到异步队列里调用执行。

......
  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    ......
  ) => {
    const componentUpdateFn = () => {
        ......
        const { bm, m, parent } = instance
        ......
      
        if (bm) {
          invokeArrayFns(bm)
        }
        
        ......
 
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }
        
        ......
    }
  }
......

至此,当我们让测试程序在挂载后延迟 2 秒更改 message 的值时,页面也会同步反映出变化。

现在让我们改一下测试程序,延时 2 秒后为 car 对象的 color 属性赋值:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Vue Reactivity</title>
    <script src="../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <!-- <div>{{ message }}</div> -->
      <div>{{ car.color }}</div>
    </div>
  </body>

  <script>
    const { createApp } = Vue

    const app = createApp({
      data: () => ({
        // message: 'hello vue!',
        car: {},
      }),
      mounted() {
        setTimeout(() => {
          // this.message += ' hello world!'
          this.car.color = 'red'
        }, 2000)
      },
    })

    app.mount('#app')
  </script>
</html>

这在 Vue 2 中是观察不到页面变化的,但是由于 Vue 3 使用了 Proxy 拦截访问,所以当 data 选项中设置了对象值,而对象属性没有初始化时,修改属性值也是响应性的,在 IE11 中 Proxy 这个特性无法用 polyfill 来兼容,因此 Vue 3 是不能在 IE 11 中使用的。

讽刺的是,通过在 Vue 3 中支持 IE11,我们给了它更多的生命力。考虑到我们的用户基础,放弃对 IE11 的支持可能会让它更快地被淘汰。

微软官方如今也停用 IE 11,推荐用户安装 Edge。

接下来再次修改测试程序,延时 2 秒后根据数组下标去修改数组,同样也是可以直接反映到页面的,而不用像 Vue 2 一样使用特定的数组方法或者替换整个数组去触发更新。

值得注意的是,在 Vue 2 中没有对 Map、Set、WeakMap、WeakSet 这些数据结构做响应式处理,如果想要修改他们并更新页面必须做替换。还记得上文判断过目标对象类型然后选择不同处理器对象吗?在 Vue 3 中对这些数据结构的 get 、set 方法也做了处理,具体可以参考 packages/reactivity/src/collectionHandlers.ts,第 370 行返回了 mutableCollectionHandlers 对象,实现思路和 baseHandlers 是类似的,有兴趣的小伙伴可以自行阅读。

至此,Vue 3 响应式基础完成。