Vue 3.0组件的渲染流程

1,068 阅读15分钟
Vue 3.0 系列文章

Vue 3.0组件的渲染流程

Vue 3.0组件的更新流程和diff算法详解

揭开Vue3.0 setup函数的神秘面纱

Vue 3.0 Props的初始化和更新流程的细节分析

Vue3.0 响应式实现原理分析

Vue 3.0 计算属性的实现原理分析

Vue3.0 常用响应式API的使用和原理分析(一)

Vue3.0 常用响应式API的使用和原理分析(二)

Vue 3.0 Provide和Inject实现共享数据

Vue 3.0 Teleport的使用和原理分析

Vue3侦听器和异步任务调度, 其中有个神秘角色

Vue3.0 指令

Vue3.0 内置指令的底层细节分析

Vue3.0 的事件绑定的实现逻辑是什么

Vue3.0 的双向绑定是如何实现的

Vue3.0的插槽是如何实现的?

探究Vue3.0的keep-alive和动态组件的实现逻辑

Vuex 4.x

Vue Router 4 的使用,一篇文章给你讲透彻

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