Vue3 渲染流程

2,291 阅读12分钟

这里是 Vue3 文档 提供的一个 Hello World 代码:

<div id="app">
  <p>{{ message }}</p>
</div>
const HelloVueApp = {
  data() {
    return {
      message: 'Hello Vue!!'
    }
  }
}

Vue.createApp(HelloVueApp).mount('#app')

它可以在浏览器上渲染出“Hello Vue”这个字符串,当然对于开发者来说这不要太简单——在 HTML 中添加一个文本节点。但 Vue 作为一个框架,它要实现这个简单的功能要经过哪些步骤呢?

从上面代码可以看到,需要调用两个和 Vue 相关的函数 createAppmount ,这就是两个主要过程:

  1. 创建应用 createApp
  2. 挂载应用 mount

创建应用

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

  if (__DEV__) {
    injectNativeTagCheck(app)
  }

  const { mount } = app
  app.mount = (containerOrSelector: Element | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container)
    container.removeAttribute('v-cloak')
    container.setAttribute('data-v-app', '')
    return proxy
  }

  return app
})

函数 createApp 主要职责有二:

  1. 创建 App 实例
    • 调用 ensureRenderer 创建 Renderer 单例
    • 调用 Renderer 的 createApp 方法创建 App 实例
  2. 代理 App 的 mount 方法,在调用原方法前处理一下 App 容器(即 mount 函数的入参 #app

创建渲染器

function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}

可以看到 ensureRenderer 其实是一个简单的单例实现,保证渲染器(Renderer)只会被创建一次。传入 createRenderer 的参数 rendererOptions 是一些指令集合,由 DOM 操作和 prop patch 两个部分组成,比如 renderOptions 提供的 createText 方法就是调用 DOM 的 document.createTextNode 方法创建真实的 DOM 节点(等下会用到它)。

createRenderer

function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

函数 createRenderer 干啥啥不行透传第一名,是否就是一个冗余的函数呢?其实不是的,它存在一个“兄弟函数” createHydrationRenderer 用于创建 SSR 的 Renderer ,他们虽然都调用同一个 baseCreateRenderer ,但传参不同,并且 createHydrationRenderer 还会依赖注水(hydration)相关的逻辑。

所以这里用两个函数分治两种逻辑,有两个优点:

  1. tree-shaking,WEB 环境打包时不会打包 hydration 相关代码。
  2. 代码易读,不用通过参数或则注释来区分创建两种渲染器的区别。

baseCreateRenderer

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  
  // 给 renderOptions 中的指令起别名,添加 host 前缀
  const {
    insert: hostInsert,
    ...
    createText: hostCreateText,
    ...
  } = options
  
  // 非常重要的方法,把 VNode -> DOM
  const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
  ) => {
    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
      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,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized,
            internals
          )
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized,
            internals
          )
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
    }
  }
  
  // 和本文相关,处理文本节点的方法(patch 方法中会调用)
  const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
    if (n1 == null) {
      hostInsert(
        (n2.el = hostCreateText(n2.children as string)),
        container,
        anchor
      )
    } else {
      const el = (n2.el = n1.el!)
      if (n2.children !== n1.children) {
        hostSetText(el, n2.children as string)
      }
    }
  }
  ...
  // 非常重要的方法,Renderer 暴露的唯三方法之一(其中还有个是 SSR 的),在内部调用 patch 和 unmount 两个方法
  const render: RootRenderFunction = (vnode, container) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }
  
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }  
}

这个就是创建 Renderer 最核心的函数了,这里只能展示部分代码。baseCreateRenderer 的主要功能是创建 VNode 节点的处理方法,在这些处理方法中会调用 renderOptions 中封装的 DOM API ,实现参考上边代码中的 processText,另外还会导出两个重要的入口方法(忽略 SSR):

  • render 负责把 VNode “渲染”为真实的 DOM,或则移除 VNode 关联的 DOM ,它总会在 VNode 发生改变后被调用。
  • createApp 创建 App 实例的方法。

我们关注的 createApp ,是由 createAppAPI(render, hydrate) 返回的一个方法。

createAppAPI

function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    ...
  }
}

函数 createAppAPI 形成了一个闭包环境,让内部的 createApp 方法可以调用渲染器的 render 方法。

创建应用

function createApp(rootComponent, rootProps = null) {
    ...
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      ...
      mount(rootContainer: HostElement, isHydrate?: boolean): any {
	    ...
      },
    })
    ...
    
    return app
  }

可以看到 ensureRenderer().createApp(...args) 调用的方法其实就是 createAppAPI 返回的 createApp 方法,所以 Demo 代码中传入的 HelloVueApp 对象,被赋值给 App 的 component 属性了。

App 其实就是经常被提到的 Vue 实例,它存在如下属性和方法(在 runtime-core/src/apiCreateApp.tscreateApp 方法中实现):

export interface App<HostElement = any> {
  version: string
  config: AppConfig
  use(plugin: Plugin, ...options: any[]): this
  mixin(mixin: ComponentOptions): this
  component(name: string): Component | undefined
  component(name: string, component: Component): this
  directive(name: string): Directive | undefined
  directive(name: string, directive: Directive): this
  mount(
    rootContainer: HostElement | string,
    isHydrate?: boolean
  ): ComponentPublicInstance
  unmount(rootContainer: HostElement | string): void
  provide<T>(key: InjectionKey<T> | string, value: T): this

  // internal, but we need to expose these for the server-renderer and devtools
  _uid: number
  _component: ConcreteComponent
  _props: Data | null
  _container: HostElement | null
  _context: AppContext
}

可以注意到 App 有一个 mount 方法,他很重要,下文会讲到。

挂载应用

  const { mount } = app
  app.mount = (containerOrSelector: Element | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container)
    container.removeAttribute('v-cloak')
    container.setAttribute('data-v-app', '')
    return proxy
  }

回到最初的 createApp 方法,它创建了 App 后还会代理 mount 函数,在 proxy mount 中会优先处理调用 normalizeContainer 处理入参 containerOrSelector ,也就是 Demo 代码中的 #app ,然后才执行原始的 mount 方法。

标准化应用容器

function normalizeContainer(container: Element | string): Element | null {
  if (isString(container)) {
    const res = document.querySelector(container)
    if (__DEV__ && !res) {
      warn(`Failed to mount app: mount target selector returned null.`)
    }
    return res
  }
  return container
}

函数 normalizeContainer 只是把字符串的入参替换为相应的 DOM 对象,看到这个函数我们也该明白 proxy mount 方法是支持两种参数类型的:

  1. 字符串(Demo 中的 #app 字符串被用于寻找 idapp 的 DOM)
  2. DOM 对象

接下来我们继续来看 mount 真正的实现,上文提过它在 runtime-core/src/apiCreateApp.ts 这个文件的 createApp 方法中。

挂载应用

mount(rootContainer: HostElement, isHydrate?: boolean): any {
  if (!isMounted) {
    const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
    ...
    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      render(vnode, rootContainer)
    }
    isMounted = true
    ...
  }
},

可以看到 mount 方法其实只有两个职责:

  1. 创建 VNode createVNode
  2. 调用渲染器的渲染函数 render

我们先看创建 VNode ...

createVNode

_createVNode

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode {
  if (!type || type === NULL_DYNAMIC_COMPONENT) {
    if (__DEV__ && !type) {
      warn(`Invalid vnode type when creating vnode: ${type}.`)
    }
    type = Comment
  }

  if (isVNode(type)) {
    // 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 */)
    if (children) {
      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.
    if (isProxy(props) || InternalObjectKey in props) {
      props = extend({}, 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
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE
      : isTeleport(type)
        ? ShapeFlags.TELEPORT
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT
            : 0

  if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
    type = toRaw(type)
    warn(
      `Vue received a Component which was made a reactive object. This can ` +
        `lead to unnecessary performance overhead, and should be avoided by ` +
        `marking the component with \`markRaw\` or using \`shallowRef\` ` +
        `instead of \`ref\`.`,
      `\nComponent that was made reactive: `,
      type
    )
  }

  const vnode: VNode = {
    __v_isVNode: true,
    [ReactiveFlags.SKIP]: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    children: null,
    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
  }

  // validate key
  if (__DEV__ && vnode.key !== vnode.key) {
    warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
  }

  normalizeChildren(vnode, children)

  // normalize suspense children
  if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
    const { content, fallback } = normalizeSuspenseChildren(vnode)
    vnode.ssContent = content
    vnode.ssFallback = fallback
  }

  if (
    shouldTrack > 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.
    (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.
    patchFlag !== PatchFlags.HYDRATE_EVENTS
  ) {
    currentBlock.push(vnode)
  }

  return vnode
}

看图和代码应该很清楚,createVNode 创建了一个特定对象用来描述节点,值得注意的是 VNode 中的 shapeFlag 属性,因为它的值和子节点的类型相关,这通常代表着属性将在递归过程中扮演重要角色。在本例中,传入的 createApp 的参数是一个对象且没有子节点,所以 shapeFlag 的值是 ShapeFlags.STATEFUL_COMPONENT ,表示有状态组件。

如果不熟悉 VNode ,可以翻翻这两篇文章:

normalizeChildren

function normalizeChildren(vnode: VNode, children: unknown) {
  let type = 0
  const { shapeFlag } = vnode
  if (children == null) {
    children = null
  } else if (isArray(children)) {
    type = ShapeFlags.ARRAY_CHILDREN
  } else if (typeof children === 'object') {
    if (shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.TELEPORT) {
      // Normalize slot to plain children for plain element and Teleport
      const slot = (children as any).default
      if (slot) {
        // _c marker is added by withCtx() indicating this is a compiled slot
        slot._c && setCompiledSlotRendering(1)
        normalizeChildren(vnode, slot())
        slot._c && setCompiledSlotRendering(-1)
      }
      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.vnode.patchFlag & PatchFlags.DYNAMIC_SLOTS
        ) {
          ;(children as RawSlots)._ = SlotFlags.DYNAMIC
          vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS
        } else {
          ;(children as RawSlots)._ = SlotFlags.STABLE
        }
      }
    }
  } else if (isFunction(children)) {
    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 值就可以知道子节点的类型,方便进行下一步处理。另外,在上方的 createVNode 的函数图解中,我之所以说 Suspense 组件判断存在问题,就是因为它在 shapeFlag 被改变后才进行判断,这个我后面确认了再补充剩余信息。

render

const render: RootRenderFunction = (vnode, container) => {
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    patch(container._vnode || null, vnode, container)
  }
  flushPostFlushCbs()
  container._vnode = vnode
}

上文提到过渲染函数的作用是把 VNode 和 DOM 关联起来,它通过 patch 方法把 VNode 翻译为 DOM ,patch 方法的参数是:

  1. 容器的 VNode
  2. 当前要渲染节点的 VNode (HelloVueApp 对象生成的 VNode)
  3. 容器本身(id 为 app 的 DOM)

patch

const patch: PatchFn = (
  n1,
  n2,
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  optimized = false
) => {
  // patching & not same type, unmount old tree
  if (n1 && !isSameVNodeType(n1, n2)) {
    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,
        optimized
      )
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      } else ...
  }

我们已经知道 patch 入参中的 n2HelloVueApp 对象生成的 VNode ,并且上文说过它的 shapeFlags 的值是 4 (有状态组件),而 ShapeFlags.COMPONENT 的值是 6 包含有状态组件 4 和函数组件 2 ,所以它会进入 processComponent 方法的处理流程。

注:4 & 6 === true

processComponent

const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_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)
    }
  }

函数 processComponent 只有一个分发职责,即判断是否当前节点的父级 VNode 是否存在,存在就执行更新 updateComponent,不存在就执行挂载 mountComponent ,本文不讨论更新,直接看挂载吧。

mountComponent

  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))
    ...
    setupComponent(instance)
    ...
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )
    ...
  }

函数 mountComponent 主要处理三件事:

  1. 创建组件实例 createComponentInstance
  2. 准备组件内容 setupComponent
  3. 渲染组件 setupRenderEffect

createComponentInstance

function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
) {
  const type = vnode.type as ConcreteComponent
  // inherit parent app context - or - if root, adopt from root vnode
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext

  const instance: ComponentInternalInstance = {
    uid: uid++,
    vnode,
    type,
    parent,
    ...
  }
  
  return instance

函数 createComponentInstance 创建一个对象并返回,这个对象便是我们在 Options API 中用 this 访问到的组件实例。

setupComponent

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

  const { props, children, shapeFlag } = instance.vnode
  const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
  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 { setup } = Component
  if (setup) {
    ...
  } else {
    finishComponentSetup(instance, isSSR)
  }
}

函数 setupComponent 的职责是处理组件的一些配置信息,它分为两步:

  1. 初始化 propsslots
  2. 调用 setupStatefulComponent 处理组件的其他配置信息

函数 setupStatefulComponent 主要处理组件的其他配置信息(比如 methods / data / watch 等等),通过调用 finishComponentSetup 方法实现。在此之前,还会通过是否存在 setup 区分 Composition API 和 Options API ,如果是 Composition API 则需要先执行 setup 函数。

function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
 ...
 if (compile && Component.template && !Component.render) {
    if (__DEV__) {
      startMeasure(instance, `compile`)
    }
    Component.render = compile(Component.template, {
      isCustomElement: instance.appContext.config.isCustomElement,
      delimiters: Component.delimiters
    })
    if (__DEV__) {
      endMeasure(instance, `compile`)
    }
  }
  ...
  // support for 2.x options
  if (__FEATURE_OPTIONS_API__) {
    currentInstance = instance
    applyOptions(instance, Component)
    currentInstance = null
  }
  ...
}

函数 finishComponentSetup 的职责也很清晰,两件:

  1. 如果存在 compile 函数且模板未被编译,则执行编译
  2. 调用 applyOptions 处理组件配置信息

Vue3 的 compile 的流程虽然比 Vue2 更清晰,但也需要较长篇幅来解释,故留到其他文章,这里只放一张编译的流程图解,方便了解 compile 函数的输入输出以及核心流程:

函数 applyOptions 会处理很多选项,每个选项都涉及有些浪费时间,既然我们这里只配置了 data 选项,那就只分析 data 的处理流程。

export function applyOptions(
  instance: ComponentInternalInstance,
  options: ComponentOptions,
  deferredData: DataFn[] = [],
  deferredWatch: ComponentWatchOptions[] = [],
  deferredProvide: (Data | Function)[] = [],
  asMixin: boolean = false
) {
  const { data: dataOptions, ... } = options
  ...
  isInBeforeCreate = true
  callSyncHook(
    'beforeCreate',
    LifecycleHooks.BEFORE_CREATE,
    options,
    instance,
    globalMixins
  )
  isInBeforeCreate = false
  ...
  if (dataOptions) {
    resolveData(instance, dataOptions, publicThis)
  }
  ...
  callSyncHook(
    'created',
    LifecycleHooks.CREATED,
    options,
    instance,
    globalMixins
  )
  ...
}

可以看到 applyOptions 的职责如下:

  1. 调用 beforeCreate 钩子,此时配置项还未被处理,所以只能通过实例取到之前就处理了的 props 和 slots
  2. 分发处理函数,比如这里的 resolveData
  3. 调用 created 钩子,此时已经能取到所有配置项内容了
  4. 把其他生命周期钩子注入到实例相应的属性中,比如 instance.bm.push(beforeMount) (未展示代码)

用数组来存放 created 之后的生命周期钩子,是因为 mixin 和 extends 的父类中可能也有配置这些钩子函数

function resolveData(
  instance: ComponentInternalInstance,
  dataFn: DataFn,
  publicThis: ComponentPublicInstance
) {
  const data = dataFn.call(publicThis, publicThis)
  if (!isObject(data)) {
    __DEV__ && warn(`data() should return an object.`)
  } else if (instance.data === EMPTY_OBJ) {
    instance.data = reactive(data)
  } else {
    // existing data: this is a mixin or extends.
    extend(instance.data, data)
  }
}

这里的 dataFn 就是用户配置的 data 选项,resolveData 会先调用 dataFn 函数获得返回对象(如果 dataFn 不是函数会输告警,未贴代码),再判断是否 data 来源是 mixin/extends ,如果是则表示已经是响应式对象直接合并到组件实例的 data 属性中,如果不是则先调用 reactive 使其变成响应式对象再合并。

选项处理流程都是一致的,其他选项如有疑问,相信也能很快找到原因,代码位置 componentOptions.ts

setupRenderEffect

  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // create reactive effect for rendering
    instance.update = effect(function componentEffect() {...}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
  }

函数 setupRenderEffect 内部给组件实例提供了一个 update 方法,该方法由 effect 函数返回。

function isEffect(fn: any): fn is ReactiveEffect {
  return fn && fn._isEffect === true
}

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

函数 effect 职责:

  1. 判断入参 fn(也就是调用时传入的 componentEffect)是否已经被 effect 处理过了,如果是则返回处理前的函数 fn.raw
  2. 调用 createReactiveEffect 函数获得代理函数,其内部在调用 componentEffect 前进行一些记录
    • trackStack 记录一个布尔值,暂时不知道这里追踪的是什么东西
    • effectStack 记录在执行中的 effect 函数栈,渲染完成便出栈
    • activeEffect 记录最新在执行的 effect 函数
  3. 执行代理函数 effect

所以组件实例的更新函数,其实就是 createReactiveEffect 返回的代理函数 effect

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

可以看到 createReactiveEffect 返回的代理函数会记录一些信息外,还会给其添加一些属性:

  • id 自增标识
  • allowRecurse 是否允许递归
  • _isEffect 是否被副作用函数处理(代理)过
  • active 是否停止函数的副作用
  • raw 原始函数
  • deps 收集当前组件跟踪的其他副作用函数
  • options 副作用选项

接下来我们看 fn() 执行的原始函数 componentEffect ,也是实际生效的挂载和更新函数。

function componentEffect() {
  if (!instance.isMounted) {
    ...
    const { bm, m, parent } = instance
    // beforeMount hook
    if (bm) {
      invokeArrayFns(bm)
    }
    ...
    // 获取组件 VNode
    const subTree = (instance.subTree = renderComponentRoot(instance))
    ...
    // patch
    patch(
      null,
      subTree,
      container,
      anchor,
      instance,
      parentSuspense,
      isSVG
    )
    ...
    // mounted hook
    if (m) {
      queuePostRenderEffect(m, parentSuspense)
    }
    ...
    instance.isMounted = true
  } else {
    // 更新逻辑
    ...
  }

可以看到,函数 componentEffect 挂载部分的逻辑如下:

  1. 调用 beforeMount 的钩子
  2. 调用 renderComponentRoot 获取组件 VNode (里边会调用组件的 render 函数)
  3. 调用 patch 函数
  4. 调用 mount 钩子

当前组件通过 renderComponentRoot 取得的 VNode 如下:

{
  children: "Hello Vue!!",
  patchFlag: 0,
  shapeFlag: 8,
  type: Symbol(Text),
  __v_isVNode: true,
  ...
}

然后我们继续看 patch 函数:

...
switch (type) {
  case Text:
    processText(n1, n2, container, anchor)
...

由于 VNode 的 type 属性为 Symbol(Text) ,所以会调用一开始说的 processText 函数处理文本节点。

const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
  if (n1 == null) {
    hostInsert(
      (n2.el = hostCreateText(n2.children as string)),
      container,
      anchor
    )
  } else {
    const el = (n2.el = n1.el!)
    if (n2.children !== n1.children) {
      hostSetText(el, n2.children as string)
    }
  }
}

函数 processText 中的 hostCreateText 其实就是 document.createTextNode 方法,hostInsert 内部调用的也是 document.insertBefore ,代码如下:

const doc = (typeof document !== 'undefined' ? document : null) as Document
...
export {
  insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  },
  ...
  createText: text => doc.createTextNode(text),
}

至此 Hello Vue!! 就被渲染为 HTML ,在网页上展示了,紧接着就会调用 mounted 生命周期钩子(当然调用之前会判断是否为 Suspense 组件)。

总结

对 Vue3 来说,渲染 Hello World 组件经过了几个主要阶段:

  1. 创建渲染器 createRenderer
  2. 创建应用 createApp
  3. 创建虚拟节点 _createVNode
  4. 执行渲染 render

我们在 Vue2 中常提及的 patch 过程,就处于 Vue3 的渲染阶段。

渲染分发函数 patch 不处理实际逻辑,只是针对不同的节点类型分配对应的处理函数,由于节点树的原因,它会在内部形成递归。