Vue 3.0组件的渲染流程

Vue简单易上手,只需要简单的文档说明就能上手开发。虽然本人也有一直使用Vue 2.0的项目开发经验,以前也只了解一点核心代码逻辑,没有全面阅读过Vue 2.0的源码。Vue 3.0发布后我也有了一些Vue 3.0项目使用经验,顺藤摸瓜来学习下Vue 3.0源码,向高手学习下编码技巧,以便在项目中更加游刃有余。

由于Vue 3.0使用了TypeScript进行了重构,所以阅读本系列前需要对TypeScript基础语法有所了解,此外需要了解递归调用和函数柯里化等。

Vue 3.0系列文章预估会有20个左右的文章篇幅,每篇文章只会围绕所涉及知识点学习,这样分析起来会比较清晰,否则绕在了一起会很混乱。

如果不想看复杂的分析过程,可以直接看最后的图片总结。

我们经常会使用如下的代码:

import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");

App就是一个Vue组件,浏览器无法识别这个组件,也不知道如何渲染这组件,那Vue是如何将App组件渲染成真实的DOM呢?本文我们来学习下Vue组件转换成真实DOM的渲染过程。

应用程序初始化

createApp入口函数
export const createApp = ((...args) => {
  // 1. 
  const app = ensureRenderer().createApp(...args)

  // 2. 
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 省略...
  }

  return app
}) as CreateAppFunction<Element>

createApp入口函数主要做了两件事情:

  • 使用ensureRenderer().createApp()创建app对象
  • 重写appmount方法。
创建app对象
创建渲染器对象

渲染器是具有平台渲染核心逻辑的JS对象。Vue可以做为跨平台渲染,所以不一定是DOM的渲染。

ensureRenderer()创建一个渲染器对象:

// 渲染器对象
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer

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

这里先判断有没有渲染器,没有再去创建一个渲染器,属于一个延时创建的方式,只有需要的时候才会去创建渲染器。

渲染器初始化的时候会传入一个渲染配置参数—rendererOptions,它定义了attribute处理方法, DOM操作方法等。

export interface RendererOptions<
  HostNode = RendererNode,
  HostElement = RendererElement
> {
  // 处理Prop,attributes等
  patchProp(
    el: HostElement,
    key: string,
    prevValue: any,
    nextValue: any,
    isSVG?: boolean,
    prevChildren?: VNode<HostNode, HostElement>[],
    parentComponent?: ComponentInternalInstance | null,
    parentSuspense?: SuspenseBoundary | null,
    unmountChildren?: UnmountChildrenFn
  ): void
  // 插入节点
  insert(el: HostNode, parent: HostElement, anchor?: HostNode | null): void
  // 省略...
}

虽然开发者不需要直接操作DOM,但是可以猜测到所有的组件会被转换成DOM。渲染器的这个配置参数包含直接操作DOM的方法,因此是非常关键的一个配置。

createRenderer方法内部直接调用baseCreateRenderer方法:

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

baseCreateRenderer方法的代码如下:

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
    
  // 1.    
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId = NOOP,
    cloneNode: hostCloneNode,
    insertStaticContent: hostInsertStaticContent
  } = options

  // 2.
  const patch: PatchFn = (
    n1,
    n2,
    ...
  ) => {}

  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    ...
  ) => {}

  // 省略众多方法...      
  
  const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }
  
  // 3. 
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}
  1. 首先解构传入的RendererOptions对象options, 然后修改了操作DOM的方法的参数名;
  2. 定义了众多的渲染相关的方法,其中最重要的是render方法。render调用了一个重要的patch方法,patch方法又会调用其他的方法,譬如组件处理相关的processComponent方法。如果某个方法需要操作DOM,那就会调用RendererOptions对象options中的方法。
  3. 最后返回一个包含rendercreateApp方法的对象。hydrateundefined

上面的createApp的值是createAppAPI的函数返回值,那它又是做什么的呢?

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  // 1.
  return function createApp(rootComponent, rootProps = null) {
    // 2.
    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }
    
    // 2.
    const context = createAppContext()
    // 3.
    const installedPlugins = new Set()
    // 4. 
    let isMounted = false
    
    // 5.
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,

      version,

      get config() {
        return context.config
      },

      set config(v) {
      },

      use(plugin: Plugin, ...options: any[]) {
        if (installedPlugins.has(plugin)) {
          __DEV__ && warn(`Plugin has already been applied to target app.`)
        } else if (plugin && isFunction(plugin.install)) {
          installedPlugins.add(plugin)
          plugin.install(app, ...options)
        } else if (isFunction(plugin)) {
          installedPlugins.add(plugin)
          plugin(app, ...options)
        }
        return app
      },

      mixin(mixin: ComponentOptions) {
        if (__FEATURE_OPTIONS_API__) {
          if (!context.mixins.includes(mixin)) {
            context.mixins.push(mixin)
          }
        return app
      },

      component(name: string, component?: Component): any {
        if (!component) {
          return context.components[name]
        }
        context.components[name] = component
        return app
      },

      directive(name: string, directive?: Directive) {
        if (!directive) {
          return context.directives[name] as any
        }
        context.directives[name] = directive
        return app
      },

      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        if (!isMounted) {
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          // store app context on the root VNode.
          // this will be set on the root instance on initial mount.
          vnode.appContext = context

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


          return vnode.component!.proxy
        }
      },

      unmount() {
        if (isMounted) {
          render(null, app._container)
          delete app._container.__vue_app__
        }
      },

      provide(key, value) {
        context.provides[key as string] = value

        return app
      }
    })

    if (__COMPAT__) {
      installAppCompatProperties(app, context, render)
    }

    return app
  }
}
  1. createAppAPI的执行结果是createApp方法,方法的执行结果是返回一个App对象;
  • 注意:千万不要混淆了Vue框架中的App和开发者定义的App,开发者定义的App其实是函数的传入参数rootComponent这个根组件, rootProps是开发者传入的根组件相关的props
  1. 检查props的合法性—如果不为null,必须是对象;
  2. 创建一个AppContext对象context, 它包含一个app属性指向App对象,pluginprovidedirectivecomponent等都是挂载在此对象上;
  3. installedPlugins用来存储安装的Plugin;
  4. isMounted置为false,标记为未挂载;
  5. 生成了一个app对象,它包含一些属性:_component为开发者定义的App根组件,_props为开发者传入的根组件相关的props,_context为上面定义的AppContext对象context。它还包含了一些方法,use安装插件方法,mixin混入方法,component全局定义组件方法,directive指令方法,mount挂载方法,unmount卸载方法,provide共享数据方法。
  • 这里我们可以看到Vue 3.0一个重大的变化就是这些方法从以前Vue 2.0全局方法变成了app对象方法。
  • 这里面一个重要的方法是mount挂载方法,具体功能后面会做介绍。这个方法持有了render渲染方法,所以调用mount方法的时候不需要传递渲染器,这是函数柯里化的一个重要技巧。
重写mount方法

我们回到createApp入口函数,上个小节分析了const app = ensureRenderer().createApp(...args)这行代码的实现细节,我们接下来分析接下来的流程:

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

  // 1. 
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 2. 
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    
    // 3.
    const component = app._component

    // 4.
    container.innerHTML = ''
    // 5.
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      // 6.
      container.removeAttribute('v-cloak')
      // 7.
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>
  1. 解构了app中的mount方法,然后重写app中的mount方法;
  2. 标准化容器,如果容器是一个字符串,则会调用document.querySelector(container)找到对应的DOM节点,这就是我们可以传入"#app"作为容器的原因;
  3. app._component赋值给component对象,这个对象其实就是开发者提供的App根组件;
  4. 清除容器的内容,即如果容器有子节点将会被清除掉;
  5. 调用框架中的App的的mount方法,即createAppAPI方法中的app对象的mount,进行挂载,这个流程下面详细介绍;
  • 看一眼mount方法调用:mount(container, true, container instanceof SVGElement),先了解下第一个是容器,第二个参数是true,第三个参数是false
  1. 清除掉容器的v-cloak Attribute,这个可以属性可以和{display:none}结合解决网络慢的情况下的页面闪动问题;
  2. 容器加上data-v-app Attribute,这个属性没啥实际作用,就只是个标记;

我们来到Appmount方法:

mount(
    rootContainer: HostElement,
    isHydrate?: boolean,
        isSVG?: boolean
    ): any {
    if (!isMounted) {
        // 1. 
        const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
        )
        // 2.
        vnode.appContext = context
        // 3.
        render(vnode, rootContainer, isSVG)
        // 4.
        isMounted = true
        // 5.
        app._container = rootContainer

        return vnode.component!.proxy
    }
}
  1. 首先根据rootComponentrootProps创建对应的VNode对象vnode;
  2. vnodeappContext赋值为创建app时初始化的context, 这个context上面介绍过,可以挂在插件等内容,此外也有app属性指向app
  3. 渲染vnode,这个下面会重点介绍,暂不深入;
  4. 标记为已挂载;
  5. app_container赋值为父容器;

Appmount流程中有两个重要的逻辑:创建VNodecreateVNode和渲染VNoderender(vnode, rootContainer, isSVG),接下来我们就来介绍他们。

创建VNode

VNode是在前端开发描述DOM的JS对象,它可以描述不同类型的节点,可以是组件节点,也可以是普通的元素节点,还有其他多种类型的节点。DOM是树状结构,VNode也是树状结构。

VNodeFlutter中的Widget类似,只是节点信息的描述树。Flutter中真正的渲染树是RenderObject树,而Vue的渲染树在前端开发中则是DOM树。

Flutter跨平台的逻辑是渲染逻辑根据不同的平台有差异,Vue基于VNode也是可以跨平台的,譬如Weexuniapp就是利用Vue实现多平台的开发。

createVNode

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 {
  // 1.
  if (isVNode(type)) {
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    return cloned
  }

  if (props) {
    // 2.
    props = guardReactiveProps(props)!
    // 3.
    let { class: klass, style } = props
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass)
    }
    // 4.
    if (isObject(style)) {
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }

  // 5. 
  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

  // 6. 
  return createBaseVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    shapeFlag,
    isBlockNode,
    true
  )
}
  1. 如果传入的type参数本来就是VNode,那么就复制一份并且标准化子节点并返回;
  2. 如果有props参数就进行参数标准化,如果是响应式对象就拷贝一份,否则不做处理;响应式的对象复制是为了避免修改响应式的数据造成其他副作用;
  3. class进行标准化,如果是字符串就直接返回对应的原值,如果是数组就对数组的每个元素进行标准化,如果是对象就获取对象的属性值为true然后用空格分开;参考文档
  4. style进行标准化,如果是字符串或者对象直接返回原值,如果是数据就对数组中的每个元素的keyvalue组合成style对象;参考文档
  5. 根据type的类型编码成ShapeFlags,如果传入的是Object,则编码成ShapeFlags.STATEFUL_COMPONENT
  6. 最后调用createBaseVNode进行真正的创建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
) {
  // 1. 
  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) {
    // 2. 
    normalizeChildren(vnode, children)
  } else if (children) {
    vnode.shapeFlag |= isString(children)
      ? ShapeFlags.TEXT_CHILDREN
      : ShapeFlags.ARRAY_CHILDREN
  }

  return vnode
}
  1. 生成vnode对象,包含了typeprops参数;
  2. 标准化子节点—将子节点赋值给vnode对象的children属性,根据子节点的ShapeFlags修改点前VNodeShapeFlags

渲染VNode

我们接下来看看mount方法中的render(vnode, rootContainer, isSVG)的逻辑:

  const render: RootRenderFunction = (vnode, container, isSVG) => {
    // 1. 
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 2.
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
    // 3. 
    container._vnode = vnode
  }
  1. 如果vnodenull,则卸载父容器的_vnode对象;
  2. 如果vnode不为null,则调用patch方法,第一次container._vnodenullvnode为开发者的App生成的VNode,container#appDOM元素;
  3. 父容器的_vnode设置为vnode
挂载和更新VNode
  // 1.
  const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    // 2.
    if (n1 === n2) {
      return
    }

    // 3.
    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
    }
    
    // 4.
    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
          )
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }

    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    }
  }
  1. patch方法的第一个参数是旧的VNode,第二个参数是新的VNode,第三个参数是父节点DOM元素;
  2. 如果新旧VNode是同一个对象就不需要操作,直接返回;
  3. 如果新旧VNodeVNodeType不一致,就先卸载旧的VNode,将旧的VNode置空,再挂载新的VNode
  4. 根据新VNodetypeshapeFlag,如果是组件则进入processComponent方法,如果普通节点就进入processElement方法;
processComponent 处理组件
  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) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }

如果旧的VNodenull,且不是keep-alive组件,则调用mountComponent方法进行组件的挂载。

mountComponent 挂载组件
  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 1. 
    const compatMountInstance =
      __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
    const instance: ComponentInternalInstance =
      compatMountInstance ||
      (initialVNode.component = createComponentInstance(
        initialVNode,
        parentComponent,
        parentSuspense
      ))

    // 2. 
    if (!(__COMPAT__ && compatMountInstance)) {
      setupComponent(instance)
    }

    // 3.
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )

  }
  1. 创建组件实例ComponentInternalInstance对象compatMountInstance,我们看到创建的实例接收了vnode和父容器节点的DOM元素, 其他的属性初始化的时候都是默认的;
  2. 设置组件实例主要是从持有的vnode中获取propsslot等属性然后进行相应的属性设置,此外开发者如果再组件中有使用setup方法,那这个setup方法也会被调用,持有其中的属性和方法;
  3. 根据组件实例compatMountInstance,vnode和父容器节点的DOM元素创建一个带副作用渲染函数;
setupRenderEffect - 创建带副作用的渲染函数
  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    
    // 1. 
    const componentUpdateFn = () => {
        if (!instance.isMounted) {
            const subTree = (instance.subTree = renderComponentRoot(instance))
            patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )
          initialVNode.el = subTree.el
        }
    }

    // 2. 
    const effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(instance.update),
      instance.scope // track it in component's effect scope
    )
    
    // 3.
    const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
    update.id = instance.uid
    // allowRecurse
    // #1801, #2043 component render effects should allow recursive updates
    effect.allowRecurse = update.allowRecurse = true

    // 4.
    update()
  }
  1. 创建了一个初次渲染或者更新渲染的componentUpdateFn方法,其实他们都会调用patch方法,他们的区别是初次渲染的第一个参数是null,而更新渲染时第一个参数是旧的VNode;
  • componentUpdateFn中调用的patch方法有一个特点就是传了instance,即把ComponentInternalInstance对象当做参数传入patch方法。
  1. 创建了一个ReactiveEffect对象effect,这个对象的第一个参数fn是一个函数,当effect调用update方法时会执行fn的函数调用;
  2. effectrun函数赋值给instanceupdate属性,并给update标记一个id
  3. 执行update(), 也就是执行componentUpdateFn方法,componentUpdateFn方法会调用patch方法,递归下去。

patch 到最后肯定是处理普通元素的VNode,所以接下来我们就了解下普通元素的VNode是如何处理的。

processElement 处理普通元素节点
  const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    isSVG = isSVG || (n2.type as string) === 'svg'
    if (n1 == null) {
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
      
    }
  }

processElementn1null的时候是进入mountElement挂载元素方法。

mountElement 挂载元素节点
const mountElement = (
  vnode: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  let el: RendererElement
  let vnodeHook: VNodeHook | undefined | null
  const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode

  // 1. 
  el = vnode.el = hostCreateElement(
    vnode.type as string,
    isSVG,
    props && props.is,
    props
  )

  // 2. 
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    hostSetElementText(el, vnode.children as string)
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(
      vnode.children as VNodeArrayChildren,
      el,
      null,
      parentComponent,
      parentSuspense,
      isSVG && type !== 'foreignObject',
      slotScopeIds,
      optimized
    )
  }

  if (dirs) {
    invokeDirectiveHook(vnode, null, parentComponent, 'created')
  }
  // props
  if (props) {
    for (const key in props) {
      if (key !== 'value' && !isReservedProp(key)) {
        hostPatchProp(
          el,
          key,
          null,
          props[key],
          isSVG,
          vnode.children as VNode[],
          parentComponent,
          parentSuspense,
          unmountChildren
        )
      }
    }
  }
    
  // 4.    
  hostInsert(el, container, anchor)
  
}

  1. 通过hostCreateElement创建DOM元素节点,我们前面介绍过hostCreateElement是创建渲染器时候传入的配置参数,本质是调用doc.createElement(tag, is ? { is } : undefined)
  2. 对子节点进行处理:如果子节点是文本,则最后实际调用el.textContent = text;如果子节点是数组,调用mountChildren方法,数组的每个子节点调用patch挂载子节点;
  3. 判断如果有props,通过hostPatchProp方法给这个DOM节点设置相关的class,style,event等属性。
  4. 将创建的DOM元素挂载到container上。

我们看到了先进行path深度优先遍历,然后再挂载创建的DOM元素,也就是说挂载DOM元素是从树的子节点开始然后逐步挂载直到根节点,最后整体挂载到#app这个元素上。至此整个DOM数渲染完成了。

总结

createApp

createApp

mount

mount

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值