likes
comments
collection
share

第二天-阅读和理解 Vue3 的初始化流程

作者站长头像
站长
· 阅读数 14

入口函数 createApp

export const createApp = ((...args) => {
  // 渲染核心
  // 1. ensureRenderer 创建一个渲染器
  // 2. 调用渲染器的 createApp ,创建app应用实例
  const app = ensureRenderer().createApp(...args)

  // 用于在开发环境下进行一些额外的检查和警告。
  if (__DEV__) {
    // 用于检查使用了无效的 HTML 标签名。因为 Vue 只会在浏览器支持的标签上工作,使用未知的标签名可能会导致问题。
    injectNativeTagCheck(app)
    // 用于检查编译器选项。编译器选项是在应用程序实例化期间传递给 createApp 函数的对象。
    injectCompilerOptionsCheck(app)
  }

  const { mount } = app
  // 扩展 app.mount 方法
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 判断 el 是否存在
    const container = normalizeContainer(containerOrSelector)
    if (!container) return

    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      // __UNSAFE__
      // Reason: potential execution of JS expressions in in-DOM template.
      // The user must make sure the in-DOM template is trusted. If it's
      // rendered by the server, the template should not contain any user data.
      component.template = container.innerHTML
      // 2.x compat check
      if (__COMPAT__ && __DEV__) {
        for (let i = 0; i < container.attributes.length; i++) {
          const attr = container.attributes[i]
          if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
            compatUtils.warnDeprecation(
              DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
              null
            )
            break
          }
        }
      }
    }

    // 挂载前,清空html
    container.innerHTML = ''
    // 挂载元素进行渲染
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      // Vue2的话,会给#app设置一个v-cloak属性,在render的时候清空掉
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

入口函数createApp这里主要功能:

  • 调用 ensureRenderer 函数创建一个渲染器,并通过这个渲染器创建一个应用程序实例对象。
  • mount 方法进行了一些扩展,增加了清空 HTML 内容的逻辑,并在挂载元素上设置了 data-v-app 属性。
  • 返回 app 实例对象

挂载函数 mount

mount(
        rootContainer: HostElement, // 应用挂载的宿主元素
        isHydrate?: boolean,  // 是否开启服务端渲染的混合模式
        isSVG?: boolean  // 宿主元素是否是SVG元素
      ): any {
        // 首次加载,并未挂载
        if (!isMounted) {
          if (__DEV__ && (rootContainer as any).__vue_app__) {
            warn(
              `There is already an app instance mounted on the host container.\n` +
                ` If you want to mount another app on the same host container,` +
                ` you need to unmount the previous app by calling \`app.unmount()\` first.`
            )
          }
          // 创建虚拟节点
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          vnode.appContext = context

          // HMR root reload
          if (__DEV__) {
            context.reload = () => {
              render(cloneVNode(vnode), rootContainer, isSVG)
            }
          }

          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            // 渲染一个vnode -> dom -> rootContainer
            render(vnode, rootContainer, isSVG)
          }
          // 
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = vnode.component
            devtoolsInitApp(app, version)
          }

          return getExposeProxy(vnode.component!) || vnode.component!.proxy
        } else if (__DEV__) {
          warn(
            `App has already been mounted.\n` +
              `If you want to remount the same app, move your app creation logic ` +
              `into a factory function and create fresh app instances for each ` +
              `mount - e.g. \`const createMyApp = () => createApp(App)\``
          )
        }
      }

mount方法接受三个参数:rootContainer表示应用挂载的宿主元素,isHydrate表示是否开启服务端渲染的混合模式,isSVG表示宿主元素是否是SVG元素。

如果应用没有挂载过,则会通过createVNode方法创建一个根虚拟节点,并将其渲染到rootContainer中。在渲染过程中,如果开启了服务端渲染的混合模式,则调用hydrate方法进行渲染。否则,调用render方法进行渲染。渲染结束后,将isMounted设置为true,表示应用已挂载,同时将根虚拟节点的appContext属性设置为context,将app._container设置为rootContainer,将(rootContainer as any).__vue_app__设置为app

如果应用已经挂载过,且当前是开发模式,则会打印警告信息,提示应用已经被挂载过。

createVNode _createVNode

export const createVNode = (
  __DEV__
    ? createVNodeWithArgsTransform // 在开发环境下进行的参数转换,主要用于提供更友好的开发者体验。
    : _createVNode
) as typeof _createVNode

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, // VNode 的类型
  props: (Data & VNodeProps) | null = null, // VNode 的属性
  children: unknown = null, // VNode 的子节点
  patchFlag: number = 0, // 表示如何 patch 这个 VNode 的标志
  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
  }

  // 如果它是一个已经存在的 VNode,那么就会对它进行克隆,并合并新的 props 和 children。
  if (isVNode(type)) {
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
      if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
        currentBlock[currentBlock.indexOf(type)] = cloned
      } else {
        currentBlock.push(cloned)
      }
    }
    cloned.patchFlag |= PatchFlags.BAIL
    return cloned
  }

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

  // 2.x async/functional component compat
  if (__COMPAT__) {
    type = convertLegacyComponent(type, currentRenderingInstance)
  }

  // 对class 和 style 进行归一化处理
  if (props) {
    props = guardReactiveProps(props)!
    let { class: klass, style } = props
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass)
    }
    if (isObject(style)) {
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }

  // 根据 type 的类型,计算出这个 VNode 的 shapeFlag
  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
    )
  }

  // 调用 createBaseVNode 传入 处理过的参数
  return createBaseVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    shapeFlag,
    isBlockNode,
    true
  )
}

这个函数接受多个参数,包括 type(VNode 的类型)、props(VNode 的属性)、children(VNode 的子节点)、patchFlag(表示如何 patch 这个 VNode 的标志)、dynamicProps(动态属性的名称列表)等。这些参数会被用来创建一个新的 VNode 对象,并返回它。

这个函数首先会对 type 进行一些处理,如果它是空或者是一个特殊的动态组件标记,就会被设置为一个注释节点。如果它是一个已经存在的 VNode,那么就会对它进行克隆,并合并新的 props 和 children。接着,它会对 class 和 style 进行归一化处理,然后根据 type 的类型,计算出这个 VNode 的 shapeFlag(一个位掩码),表示它的类型。最后,它会调用 createBaseVNode 函数,使用这些参数来创建一个新的 VNode 对象,并返回它。

render

const render: RootRenderFunction = (
    vnode,  // 表示要渲染的根组件虚拟节点
    container,  // 表示要挂载到的 DOM 元素
    isSVG // 表示是否是 SVG 元素
    ) => {
    
    // vnode 为空,则卸载
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 首次执行,传入根组件vnode,参数1是null
      // 所以首次执行挂载过程 或 更新组件
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPreFlushCbs()
    flushPostFlushCbs()
    container._vnode = vnode
  }

render 函数是用来将组件的虚拟节点 (vnode) 渲染到真实的 DOM 中的。

函数接受三个参数:vnode 表示要渲染的根组件虚拟节点,container 表示要挂载到的 DOM 元素,isSVG 表示是否是 SVG 元素。

如果 vnode 为 null,则说明需要卸载之前已经渲染过的组件,这时会先调用 unmount 函数卸载组件,然后将 container._vnode 置为 null。

否则,就会调用 patch 函数进行挂载或更新组件。如果 container._vnode 为 null,说明是首次挂载,此时将第一个参数传入 null。如果 container._vnode 不为 null,则说明之前已经有组件挂载过,此时将 container._vnode 传入。

最后,函数会分别调用 flushPreFlushCbs 和 flushPostFlushCbs 函数来执行预先和后续挂载的钩子函数,并将 container._vnode 置为 vnode。

总结

从 Vue3 的初始化的过程中,涉及到好一些概念,上文所列有如下几个概念,其他的概念,遇到再看:

  1. 应用实例:通过 createApp 函数创建的应用实例是一个包含组件、指令、插件等功能的对象,可以调用其方法实现对应用的控制。
  2. 虚拟 DOM:Vue3 中的虚拟 DOM 是基于 VNode 的,VNode 是对真实 DOM 的抽象表示,可以通过 createVNode 函数创建。
  3. 渲染函数:Vue3 通过渲染函数将 VNode 渲染为真实 DOM,渲染函数包括 patch 函数和 render 函数,其中 patch 函数实现了虚拟 DOM 的比较和更新,render 函数实现了将 VNode 渲染为真实 DOM 的过程。

重点函数

  1. createApp: 创建一个应用实例,返回一个应用实例对象,包括提供了一些方法和选项,如 mountunmountdirectivecomponent 等。
  2. createRenderer: 创建一个渲染器实例,用于渲染组件或者 vnode。其中会创建一个 Patching 算法,用于对比新旧 vnode 的不同,并在必要时更新 DOM 树。
  3. mount: 用于挂载应用实例,其中会调用 patch 函数对组件进行初始化,并在必要时创建 DOM 节点。
  4. _createVNode: 用于创建 vnode 对象,其中会对 vnode 的 type、props、children 进行预处理,用于后续的渲染过程。

Vue3 初始化过程还未结束,后续继续