vue3 源码学习(2) setup是如何被执行的

146 阅读9分钟

上一篇分析了 createApp 创建app经过了哪些过程

接下来我们看看setup 方法是如何被执行的, 当mount方法被调用时,组件开始渲染

createApp().mount('#app')

也就会进入 app.mount 内部, 调用 mount 方法

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  const { mount } = app // 取出原本的 mount 
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component // 取出组件的对象数据
    if (!isFunction(component) && !component.render && !component.template) { 
      component.template = container.innerHTML 
    }
    container.innerHTML = '' 
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) { 
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '') 
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

我们进入 mount 方法内部, 在其内部首先会判断 isMounted , 根据这个变量判断是否被挂载过, 发现从来没挂载过, 这时候开始创建 vnode, rootComponent 参数就是vue组件的对象信息, rootProps 看变量名也知道了是props

import { createVNode } from './vnode'
  mount( 
	rootContainer: HostElement,
	isHydrate?: boolean, 
	isSVG?: boolean
  ): any {
	if (!isMounted) {
	  const vnode = createVNode(
		rootComponent as ConcreteComponent,
		rootProps
	  )
	  vnode.appContext = context

	  render(vnode, rootContainer, isSVG)

	  isMounted = true
	  app._container = rootContainer 
		  ;(rootContainer as any).__vue_app__ = app  
	  return vnode.component!.proxy
	} 
  }

进入 createVNode 方法,发现实际调用的是 _createVNode, 进入函数首先判断是节点类型,若类型不存在或是非动态组件,将type改写为 Comment 类型

然后判断是否为 vnode , 判断依据是是否存在 __v_isVNode 属性 ,这是本身就是vnode的情况, 那就会克隆一份再返回;

接着判断是否是类组件, 依据的是 __vccOpts 属性, 如是那会修改type变量;

然后判断 props, 如果有的话会对props 进行标准化处理, 特别是对props其中的 style

接下来的 shapeFlag 变量就是判断出 vnode的类型

最后再调用 createBaseVNode 方法,在这个方法内会真正的去创建一个 vnode


export const createVNode = (
  _createVNode
) as typeof _createVNode



export function isVNode(value: any): value is VNode {
  return value ? value.__v_isVNode === true : false 
}

export function isClassComponent(value: unknown): value is ClassComponent {
  return isFunction(value) && '__vccOpts' in value
}

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, // 节点类型, 或者是组件对象
  props: (Data & VNodeProps) | null = null, // 节点props
  children: unknown = null, // sub Stree
  patchFlag: number = 0, // 补丁标记, 为了优化
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode {

  if (!type || type === NULL_DYNAMIC_COMPONENT) {
    type = Comment
  }

  if (isVNode(type)) { // 本身就是 vNode的情况
    // createVNode receiving an existing vnode. This happens in cases like
    // <component :is="vnode"/>
    // #2078 make sure to merge refs during the clone instead of overwriting it
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)  // 克隆一份 vode
    if (children) { // 再处理 vnode 的子元素
      normalizeChildren(cloned, children)
    }
    return cloned
  }

  // class component normalization.
  if (isClassComponent(type)) { // 类 组件
    type = type.__vccOpts
  }

  // class & style normalization.
  if (props) {
    // for reactive or proxy objects, we need to clone it to enable mutation.
    props = guardReactiveProps(props)!
    let { class: klass, style } = props
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass)
    }
    if (isObject(style)) {
      // reactive state objects need to be cloned since they are likely to be
      // mutated
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }

  // encode the vnode type information into a bitmap  // 用来标记VNode种类的标志位
  const shapeFlag = isString(type) // 如果是 string 那就是 element 类型
    ? ShapeFlags.ELEMENT  // 如果不是 string  判断是否是 suspense 组件 如果是 suspense就 标记 ShapeFlags.SUSPENSE
    : ( __FEATURE_SUSPENSE__ && isSuspense(type)  // -如果不是 suspense , 那就判断是否是传送门组件
    ? ShapeFlags.SUSPENSE  // 如果是传送门组件 ,给标记 ShapeFlags.TELEPORT, 如果不是
    : ( isTeleport(type) // 判断是 type 否是 object,  那就是有状态组件
    ? ShapeFlags.TELEPORT // 如果 type 不是 object , 继续判断 是否是函数
    : ( isObject(type)   // 如果 tyoe 是 function , 那就是  functional 组件
    ? ShapeFlags.STATEFUL_COMPONENT  // 如果都不是,就标记为 0 咯
    : ( isFunction(type)
    ? ShapeFlags.FUNCTIONAL_COMPONENT
    : 0 ))))

  return createBaseVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    shapeFlag,
    isBlockNode,
    true
  )
}

首先长长的定义了一个 vnode 变量, 有我们常见的vnode上的各种属性,还有 __v_isVNode 这样用于内部判断的私有属性

再通过 needFullChildrenNormalization 参数判断是否需要标准化 children , 因为在上面的代码里我们看到调用 createBaseVNode 时就是传入的true, normalizeChildren 内部呢就是判断children 类型然后对vnode的 shapeFlag 进行附加, 这里值得一提的是, vue内部大量使用了位运算,举个例子, 比如定义三个类型 a, b, c, a = 1, b = 1 << 1 (也就是二进制 10),c = 1 << 2(二进制 100) 这时候如果一开始 type = c , 但是我想让 type变量同时存储 a 类型和 c 类型,正常情况下,我们会这么做: type = [a, c] 对吧, 这样可以,但是后续的类型判断很麻烦, 也很慢, 必须遍历 type 数组, 位运算的好处呢, 就是可以解决这样的问题, 如果想让 type 增加 a类型, 我只需要type |= a , 这样 type 就会从一开始的二进制 100 变为 101, 然后在判断类型的时候就可以使用 type & a 快速的进行判断得出结果,这里 normalizeChildren 函数内 vnode.shapeFlag |= type 就是如此,进行类型的增加

最后再返回这个 vnode

function createBaseVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag = 0,
  dynamicProps: string[] | null = null,
  shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
  isBlockNode = false,
  needFullChildrenNormalization = false
) {
  const vnode = {
    __v_isVNode: true,
    __v_skip: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null,
    children,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
  } as VNode

  if (needFullChildrenNormalization) { // 需要标准化 children
    normalizeChildren(vnode, children)
    // normalize suspense children
    if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { // 判断 SUSPENSE 组件
      ;(type as typeof SuspenseImpl).normalize(vnode)
    }
  } else if (children) {
    // compiled element vnode - if children is passed, only possible types are
    // string or Array.
    vnode.shapeFlag |= isString(children) // 文本节点或者 子组件数组
      ? ShapeFlags.TEXT_CHILDREN
      : ShapeFlags.ARRAY_CHILDREN
  }

  // track vnode for block tree
  if (
    isBlockTreeEnabled > 0 &&
    // avoid a block node from tracking itself
    !isBlockNode &&
    // has current parent block
    currentBlock &&
    // presence of a patch flag indicates this node needs patching on updates. 补丁标志的存在表示该节点在更新时需要打补丁
    // component nodes also should always be patched, because even if the
    // component doesn't need to update, it needs to persist the instance on to
    // the next vnode so that it can be properly unmounted later.
    (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
    // the EVENTS flag is only for hydration and if it is the only flag, the
    // vnode should not be considered dynamic due to handler caching.
    vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
  ) {
    currentBlock.push(vnode) // 当前依赖收集 block 加入这个 vnode ,然后这些存在 patch flag 标记的 vnode 会被追加到 currentBlock 中收集
  }
  return vnode
}




export function normalizeChildren(vnode: VNode, children: unknown) {
  let type = 0
  const { shapeFlag } = vnode // 取出节点类型
  if (children == null) {
    children = null
  } else if (isArray(children)) { // 如果本身就是数组, 那就是 arrary_children type
    type = ShapeFlags.ARRAY_CHILDREN
  } else if (typeof children === 'object') { // 子组件是对象
    if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.TELEPORT)) { // shapeFlag 是 ELEMENT 或者 TELEPORT. 确实判断会方便很多
      // Normalize slot to plain children for plain element and Teleport 为普通元素和传送将插槽规范化为普通子元素
      const slot = (children as any).default // 取出 default
      if (slot) {
        // _c marker is added by withCtx() indicating this is a compiled slot _c标记是由withCtx()添加的,表示这是一个编译后的槽位
        slot._c && (slot._d = false)
        normalizeChildren(vnode, slot())
        slot._c && (slot._d = true)
      }
      return
    } else {
      type = ShapeFlags.SLOTS_CHILDREN // 插槽子元素
      const slotFlag = (children as RawSlots)._
      if (!slotFlag && !(InternalObjectKey in children!)) {
        // if slots are not normalized, attach context instance
        // (compiled / normalized slots already have context)
        ;(children as RawSlots)._ctx = currentRenderingInstance
      } else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) {
        // a child component receives forwarded slots from the parent.
        // its slot type is determined by its parent's slot type.
        if (
          (currentRenderingInstance.slots as RawSlots)._ === SlotFlags.STABLE
        ) {
          ;(children as RawSlots)._ = SlotFlags.STABLE
        } else {
          ;(children as RawSlots)._ = SlotFlags.DYNAMIC
          vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS
        }
      }
    }
  } else if (isFunction(children)) { // 函数子组件,构造 default 
    children = { default: children, _ctx: currentRenderingInstance }
    type = ShapeFlags.SLOTS_CHILDREN
  } else { // 到这里应该就是正常子组件了吧 又好像是文本类型子组件
    children = String(children)
    // force teleport children to array so it can be moved around
    if (shapeFlag & ShapeFlags.TELEPORT) {  // 判断 是传送 组件
      type = ShapeFlags.ARRAY_CHILDREN  // 类型改为数组子元素
      children = [createTextVNode(children as string)] // 构造成数组
    } else { // 否则就是纯的文本子组件
      type = ShapeFlags.TEXT_CHILDREN
    }
  }
  vnode.children = children as VNodeNormalizedChildren
  vnode.shapeFlag |= type // vnode.shapeFlag = vnode.shapeFlag | type 
}

此时又回到了mount 方法, 挂载上 context 上下文, 这里的 context 是在 createApp 调用时就被创建好的。 接着就调用render 方法,这个render 方法还记得吗是在创建 renderer 的时候就被创建好,保存在闭包里的, 我们接着看看

import { createVNode } from './vnode'
  mount( 
	rootContainer: HostElement,
	isHydrate?: boolean, 
	isSVG?: boolean
  ): any {
	if (!isMounted) {
	  const vnode = createVNode(
		rootComponent as ConcreteComponent,
		rootProps
	  )
	  vnode.appContext = context

	  render(vnode, rootContainer, isSVG)

	  isMounted = true
	  app._container = rootContainer 
		  ;(rootContainer as any).__vue_app__ = app  
	  return vnode.component!.proxy
	} 
  }

render 方法比较简单,接收 vnode 和 dom对象, 如果vode 为null它就认为是需要卸载组件, 否则就会进入patch 函数, 并且将 vnode 挂到 dom 的 _vnode 属性上

const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) { // 新 vnode 不存在
      if (container._vnode) { // 有旧 vnode
        unmount(container._vnode, null, null, true) // 执行现在  vnode 
      }
    } else {
      patch(container._vnode || null, vnode, container, null, null, null, isSVG) // 进行patch 打补丁, 第一次进入还没挂载过所以 container._vnode 就是空的
    }
    flushPostFlushCbs()
    container._vnode = vnode  // 当前vNode 继续挂到 dom 上
  }

patch 具体如下, 会判断 n1 n2 两个 vnode 引用是否相同、属性是否相同、类型是否相同等等 然后根据 type 类型进入不同的实际的处理流程,这里我们传入的是 组件节点, 因此会调用 processElement 方法

 const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    if (n1 === n2) { // n1 n2 两个 vNode 完全相等的情况, 那就不用 patch 了
      return
    }

    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) { // 当n1 已存在,代表是更新操作, 并且  n1.type === n2.type && n1.key === n2.key 粗浅判断
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false 
      n2.dynamicChildren = null 
    }

    const { type, ref, shapeFlag } = n2
    switch (type) { // 组件类型判断
      case Text:
        processText(n1, n2, container, anchor)
        break
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      case Static:
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) { // 是 普通元素
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) { //组件节点
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) { // 传送组件
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { // 悬念组件
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        }
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    }
  }

processComponent 同样的也会根据 n1 是否存在判断是挂载组件还是更新组件,并且内部还有对 keep-alive 的处理, 因为我们是第一次挂在,所以又会进入 mountComponent 方法 (这调用层级也太多了吧😭)


  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) { // 挂载操作
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { // keep-alive 组件
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else { // 否则正常挂载元素
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else { // 更新操作
      updateComponent(n1, n2, optimized)
    }
  }

mountComponent 方法内, 先是取出 componentInstance, 如果没有就创建一个, 然后是针对 keep-alive 进行注入针对这个组件特殊实现的 renderer 。 还有对 suspense 组件的特殊处理

重要的是过程中调用了 setupComponent 并传入了 instance

  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 2.x compat may pre-creaate the component instance before actually 2.X compat可以在实际创建之前预先创建组件实例
    // mounting
    const compatMountInstance =
      __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
    const instance: ComponentInternalInstance = // 创建组件 实例咯
      compatMountInstance ||
      (initialVNode.component = createComponentInstance(
        initialVNode,
        parentComponent,
        parentSuspense
      ))


    // inject renderer internals for keepAlive
    if (isKeepAlive(initialVNode)) {
      ;(instance.ctx as KeepAliveContext).renderer = internals
    }

    // resolve props and slots for setup context
    if (!(__COMPAT__ && compatMountInstance)) {
      setupComponent(instance)
    }

    // setup() is async. This component relies on async logic to be resolved
    // before proceeding
    if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
      parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)

      // Give it a placeholder if this is not hydration
      // TODO handle self-defined fallback
      if (!initialVNode.el) {
        const placeholder = (instance.subTree = createVNode(Comment))
        processCommentNode(null, placeholder, container!, anchor)
      }
      return
    }

    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )
  }

setupComponent 内部对 props 和 slots 属性都进行了初始化, 并判断了是否为 stateful组件, 如果是那还会调用 setupStatefulComponent, 其内部呢就是取出了 instance.type 上的 setup 方法 通过 callWithErrorHandling 函数进行了执行,这个函数是为了更好的处理异常,setup返回结果被存储在了 setupResult 变量, 并且可以看到还有针对 setupResult 为异步promise 的判断,并针对其的处理

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR

  const { props, children } = instance.vnode
  const isStateful = isStatefulComponent(instance)
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children) // 插槽初始化

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



function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions
  // 0. create render proxy property access cache
  instance.accessCache = Object.create(null) // 访问缓存
  // 1. create public instance / render proxy
  // also mark it raw so it's never observed
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers)) // 
  // 2. call setup()
  const { setup } = Component
  if (setup) {
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null) // 秒啊, 用 function length 判断是否使用 context, 不使用就不创建context

    setCurrentInstance(instance) // 将当前 instance 挂至全局变量
    pauseTracking() // 暂停追踪 至于内部实现有待研究
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION, // 传入这个type 也只是用在setup有异常的时候
      [instance.props, setupContext]
    )
    resetTracking()
    unsetCurrentInstance()

    if (isPromise(setupResult)) { // 针对 setup 返回的是 promise 进行处理
      setupResult.then(unsetCurrentInstance, unsetCurrentInstance) // 解决异步使用setup的时候, 
    } else {
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    finishComponentSetup(instance, isSSR)
  }
}

如果非异步的 setup返回结果会进入到 handleSetupResult 方法, 如果返回的是对象, 那就会先被 proxy 包转成 ref , 然后挂到 instance.setupState 属性上


export function handleSetupResult( // 处理 setup函数的结果
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  if (isFunction(setupResult)) { // setup 可以返回一个渲染函数
    // setup returned an inline render function
    if (__NODE_JS__ && (instance.type as ComponentOptions).__ssrInlineRender) { // ssr 状态
      // when the function's name is `ssrRender` (compiled by SFC inline mode),
      // set it as ssrRender instead.
      instance.ssrRender = setupResult
    } else {
      instance.render = setupResult as InternalRenderFunction  // render function 赋值
    }
  } else if (isObject(setupResult)) {  // 如果是正常的对象对象返回
    // setup returned bindings.
    // assuming a render function compiled from template is present.
   
    instance.setupState = proxyRefs(setupResult) // 把结果包装成ref 挂在  setupState 上  
  } 
  finishComponentSetup(instance, isSSR)
}

ok 到这里总算是看到 setup 执行了,

我们来梳理下调用过程:

app.mount -> mount -> render -> _createVNode -> createBaseVNode -> render -> patch -> processComponent -> mountComponent -> setupComponent -> setupStatefulComponent -> handleSetupResult -> finishComponentSetup