前言

在「Vue3」中,创立一个组件实例由 createApp 「API」实现。创立完一个组件实例,咱们须要调用 mount() 办法将组件实例挂载到页面中:

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

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

mountComponent() 实现的外围是 setupComponent(),它能够分为两个过程

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

那么,接下来咱们依然从源码的角度,具体地剖析一下这两个过程。

1 开始装置

setupComponent() 的定义:

// packages/runtime-core/src/component.tsfunction 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.tssetupStatefulComponent(  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.tsfunction pauseTracking() {  trackStack.push(shouldTrack)  shouldTrack = false}

复原依赖收集 resetTracking()

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

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

// packages/reactivity/src/effect.tsfunction 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.tsfunction 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.tsfunction 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.tsfunction 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,所以,有一些文章并没有同步这里,目前正在整顿中。

往期文章回顾

深度解读 Vue3 源码 | 内置组件 teleport 是什么“来头”?

深度解读 Vue 3 源码 | compile 和 runtime 联合的 patch 过程

深度解读 Vue 3 源码 | 从编译过程,了解动态节点晋升

❤️爱心三连击

写作不易,如果你感觉有播种的话,能够爱心三连击!!!