3

前言

在「Vue3」中,创建一个组件实例由 createApp 「API」完成。创建完一个组件实例,我们需要调用 mount() 方法将组件实例挂载到页面中:

createApp({
    ...
}).mount("#app");

在源码中整个组件的创建过程:

mountComponent() 实现的核心是 setupComponent(),它可以分为两个过程

  • 开始安装,它会初始化 propsslots、调用 setup()、验证组件和指令的合理性。
  • 结束安装,它会初始化 computeddatawatchmixin 和生命周期等等。

那么,接下来我们仍然从源码的角度,详细地分析一下这两个过程。

1 开始安装

setupComponent() 的定义:

// packages/runtime-core/src/component.ts
function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR

  const { props, children, shapeFlag } = instance.vnode
  const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT  // {A}
  initProps(instance, props, isStateful, isSSR) // {B}
  initSlots(instance, children) // {C}

  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined // {D}
  isInSSRComponentSetup = false
  return setupResult
}

抛开 SSR 的逻辑,B 行和 C 行会先初始化组件的 propsslots。然后,在 A 行判断 shapeFlagtrue 时,调用 setupStatefulComponent()

这里又用到了 shapeFlag,所以需要强调的是 shapeFlagpatchFlag 具有一样的地位(重要性)。

setupStatefulComponent() 则会处理组合 Composition API,即调用 setup()

1.1 setupStatefulComponent

setupStatefulComponent() 定义(伪代码):

// packages/runtime-core/src/component.ts
setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions
  // {A} 验证逻辑
  ...
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  ...
  const { setup } = Component
  if (setup) {
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    currentInstance = instance // {B}
    pauseTracking() // {C}
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    ) // {D}
    resetTracking() // {E}
    currentInstance = null

    if (isPromise(setupResult)) {
      ...
    } else {
      handleSetupResult(instance, setupResult, isSSR) // {F}
    }
  } else {
    finishComponentSetup(instance, isSSR)
  }
}

首先,在 B 行会给当前实例 currentInstance 赋值为此时的组件实例 instance,在回收 currentInstance 之前,我们会做两个操作暂停依赖收集恢复依赖收集

暂停依赖收集 pauseTracking()

// packages/reactivity/src/effect.ts
function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}

恢复依赖收集 resetTracking()

// packages/reactivity/src/effect.ts
resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}

本质上这两个步骤是通过改变 shouldTrack 的值为 truefalse 来控制此时是否进行依赖收集。之所以,shouldTrack 可以控制是否进行依赖收集,是因为在 track 的执行开始有这么一段代码:

// packages/reactivity/src/effect.ts
function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  ...
}

那么,我们就会提出疑问为什么这个时候需要暂停依赖收?这里,我们回到 D 行:

const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    ) // {D}

DEV 环境下,我们需要通过 shallowReadonly(instance.props) 创建一个基于组件 props 的拷贝对象 Proxy,而 props 本质上是响应式地,这个时候会触发它的 track 逻辑,即依赖收集,明显这并不是开发中实际需要的订阅对象,所以,此时要暂停 props 的依赖收集,过滤不必要的订阅

相比较,「Vue2.x」泛滥的订阅关系而言,这里不得不给「Vue3」对订阅关系处理的严谨思维点赞!

通常,我们 setup() 返回的是一个 Object,所以会命中 F 行的逻辑:

handleSetupResult(instance, setupResult, isSSR)

1.2 handleSetupResult

handleSetupResult() 定义:

// packages/runtime-core/src/component.ts
function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  if (isFunction(setupResult)) {
    instance.render = setupResult as InternalRenderFunction
  } else if (isObject(setupResult)) {
    if (__DEV__ && isVNode(setupResult)) {
      warn(
        `setup() should not return VNodes directly - ` +
          `return a render function instead.`
      )
    }
    instance.setupState = proxyRefs(setupResult)
    if (__DEV__) {
      exposeSetupStateOnRenderContext(instance)
    }
  } else if (__DEV__ && setupResult !== undefined) {
    warn(
      `setup() should return an object. Received: ${
        setupResult === null ? 'null' : typeof setupResult
      }`
    )
  }
  finishComponentSetup(instance, isSSR)
}

handleSetupResult() 的分支逻辑较为简单,主要是验证 setup() 返回的结果,以下两种情况都是不合法的

  • setup() 返回的值是 render() 的执行结果,即 VNode
  • setup() 返回的值是 nullundefined或者其他非对象类型。

1.3 小结

到此,组件的开始安装过程就结束了。我们再来回顾一下这个过程会做的几件事,初始化 propsslot以及处理 setup() 返回的结果,期间还涉及到一个暂停依赖收集的微妙处理。

需要注意的是,此时组件并没有开始创建,因此我们称之为这个过程为安装。并且,这也是为什么官方文档会这么介绍 setup()

一个组件选项,在创建组件之前执行,一旦 props 被解析,并作为组合 API 的入口点

2 结束安装

finishComponentSetup() 定义(伪代码):

// packages/runtime-core/src/component.ts
function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions
  ...
  if (!instance.render) { // {A}
    if (compile && Component.template && !Component.render) {
      ...
      Component.render = compile(Component.template, {       
        isCustomElement: instance.appContext.config.isCustomElement || NO,
        delimiters: Component.delimiters
      })
      ...
    }

    instance.render = (Component.render || NOOP) as InternalRenderFunction // {B}
    if (instance.render._rc) {
      instance.withProxy = new Proxy(
        instance.ctx,
        RuntimeCompiledPublicInstanceProxyHandlers
      )
    }
  }

  if (__FEATURE_OPTIONS_API__) { // {C}
    currentInstance = instance
    applyOptions(instance, Component)
    currentInstance = null
  }
  ...
}

整体上 finishComponentSetup() 可以分为三个核心逻辑:

  • 绑定 render 函数到当前实例 instance 上(行 A),这会两种情况,一是手写 render 函数,二是模板 template 写法,它会调用 compile 编译模板生成 render 函数。
  • 为模板 template 生成的 render 函数(行 B),单独使用一个不同的 has 陷阱。因为,编译生成的 render 函数是会存在 withBlock 之类的优化,以及它会有一个全局的白名单来实现避免进入 has 陷阱。
  • 应用 options(行 C),即对应的 computedwatchlifecycle 等等。

2.1 applyOptions

applyOptions() 定义:

// packages/runtime-core/src/componentOptions.ts
function applyOptions(
  instance: ComponentInternalInstance,
  options: ComponentOptions,
  deferredData: DataFn[] = [],
  deferredWatch: ComponentWatchOptions[] = [],
  asMixin: boolean = false
) {
  ...
}

由于, applyOptions() 涉及的代码较多,我们先不看代码,看一下整体的流程:

applyOptions() 的流程并不复杂,但是从流程中我们总结出两点平常开发中忌讳的点:

  • 不要在 beforeCreate 中访问 mixin 相关变量。
  • 由于本地 mixin 后于全局 mixin 执行,所以在一些变量命名重复的场景,我们需要确认要使用的是全局 mixin 的这个变量还是本地的 mixin
对于 mixin 重名时选择本地还是全局的处理,有兴趣的同学可以去官方文档了解。

我们再从代码层面看整个流程,这里分析几点常关注的属性是怎么初始化的:

2.1.1 注册事件(methods)

if (methods) {
  for (const key in methods) {
    const methodHandler = (methods as MethodOptions)[key]
    if (isFunction(methodHandler)) {
      ctx[key] = methodHandler.bind(publicThis) // {A}
      if (__DEV__) {
        checkDuplicateProperties!(OptionTypes.METHODS, key)
      }
    } else if (__DEV__) {
      warn(
        `Method "${key}" has type "${typeof methodHandler}" in the component definition. ` +
          `Did you reference the function correctly?`
      )
    }
  }
}

事件的注册,主要就是遍历已经处理好的 methods 属性,然后在当前上下文 ctx 中绑定对应事件名的属性 key 的事件 methodHandler(行 A)。并且,在开发环境下会对当前上下文属性的唯一性进行判断。

2.1.2 绑定计算属性(computed)

if (computedOptions) {
    for (const key in computedOptions) {
      const opt = (computedOptions as ComputedOptions)[key]
      const get = isFunction(opt) 
        ? opt.bind(publicThis, publicThis)
        : isFunction(opt.get)
          ? opt.get.bind(publicThis, publicThis)
          : NOOP // {A}
      if (__DEV__ && get === NOOP) {
        warn(`Computed property "${key}" has no getter.`)
      }
      const set =
        !isFunction(opt) && isFunction(opt.set)
          ? opt.set.bind(publicThis)
          : __DEV__
            ? () => {
                warn(
                  `Write operation failed: computed property "${key}" is readonly.`
                )
              }
            : NOOP // {B}
      const c = computed({
        get,
        set
      }) // {C}
      Object.defineProperty(ctx, key, {
        enumerable: true,
        configurable: true,
        get: () => c.value,
        set: v => (c.value = v)
      }) {D}
      if (__DEV__) {
        checkDuplicateProperties!(OptionTypes.COMPUTED, key)
      }
    }
  }

绑定计算属性主要是遍历构建好的 computedOptions,然后提取每一个计算属性 key 对应的 getset(行 A),也是我们熟悉的对于 get强校验,即计算属性必须要有 get可以没有 set,如果没有 set(行 B),此时它的 set 为:

() => {
  warn(
    `Write operation failed: computed property "${key}" is readonly.`
  )
}
所以,这也是为什么我们修改一个没有定义 set 的计算属性时会提示这样的错误。

然后,在 C 行会调用 computed 注册该计算属性,即 effect 的注册。最后,将该计算属性通过 Object.defineProperty 代理到当前上下文 ctx 中(行 D),保证通过 this.computedAttrName 可以获取到该计算属性。

2.1.3 生命周期处理

生命周期的处理比较特殊的是 beforeCreate,它是优于 mixindatawatchcomputed 先处理:

if (!asMixin) {
  callSyncHook('beforeCreate', options, publicThis, globalMixins)
  applyMixins(instance, globalMixins, deferredData, deferredWatch)
}

至于其余的生命周期是在最后处理,即它们可以正常地访问实例上的属性(伪代码):

if (lifecycle) {
  onBeforeMount(lifecycle.bind(publicThis))
}

2.2 小结

结束安装过程,主要是初始化我们常见的组件上的选项,只不过我们可以不用 options 式的写法,但是实际上源码中仍然是转化成 options 处理,主要也是为了兼容 options 写法。并且,结束安装的过程比较重要的一点就是调用各个生命周期,而熟悉每个生命周期的执行时机,也可以便于我们平常的开发不犯错。

写在最后

这是「Vue3 源码解读」系列的第四篇文章,理论上也是第七篇。每写完一篇,我都在思考如何表达才能使得文章的阅读性变得更好,而这篇文章表达方式也是在翻译了两篇 Dr. Axel Rauschmayer 大佬文章后,我思考的几点文章中需要做的改变。最后,文章中如果存在不当的地方,欢迎各位同学提 Issue。

为什么是第七篇,因为我将会把这个系列的文章汇总成一个 Git Page,所以,有一些文章并没有同步这里,目前正在整理中。

❤️爱心三连击

写作不易,如果你觉得有收获的话,可以爱心三连击!!!


五柳
1.1k 声望1.4k 粉丝

你好,我是五柳,希望能带给大家一些别样的知识和生活感悟,春华秋实,年年长茂。