render - Vue3源码解读

290 阅读2分钟

目录:

一. 前言

createApp方法执行中,我们先执行了createVNode生成了VNode,然后执行render方法,并且将VNode作为参数传入,同时将context值绑定到vnode.appContext属性上,接下来我们看下render方法的实现。

//源码路径 core/packages/runtime-core/src/apiCreateApp.ts

vnode.appContext = context
render(vnode, rootContainer, isSVG)

二. render 方法的定义

代码很简短,可以看到,如果传入的VNode为空,则直接卸载旧的dom,反则执行patch方法,从vue2中我们可以了解到,该方法是一个diff算法,将新旧dom进行比对后渲染,让我们去看下patch方法的实现。

//源码路径 core/packages/runtime-core/src/renderer.ts

  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
  }

三. patch 首次执行

1. patch 的调用

我们可以看到,传入了一个container._vnodevnode,而container._vnode初始值为null,该值作为旧的节点,也就是目前真实的dom,而vnode是新的节点,意思是即将要渲染为真实dom的虚拟节点。

//源码路径 core/packages/runtime-core/src/renderer.ts

patch(container._vnode || null, vnode, container, null, null, null, isSVG)

2. patch 的实现

该方法内部实现用来为不同的vnode做不同的处理。

  • 首先,通过判断如果更改前后节点相同的话直接退出,不做任何处理
  • 通过isSameVNodeType对比新旧节点的类型,不相同直接卸载掉旧的节点
  • 接下来通过switch来对不同的type进行不同的处理,第一次挂载则进入processComponent方法,因此我们接着往下看
//源码路径 core/packages/runtime-core/src/renderer.ts

  const patch: PatchFn = (
    n1, // 上一次 vnode 初始值null
    n2, // 新的 vnode
    container, // 真实dom
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    if (n1 === n2) {
      return
    }

    // 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,
          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})`)
        }
    }

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

3. processComponent

我们看到,首先进来之后有一个判断n1是否为null,如果n1null,则说明还没有真实dom被渲染,则进行第一次执行挂载mountComponent,反则执行updataComponent。我们继续往下看,进入mountComponent方法中。

//源码路径 core/packages/runtime-core/src/renderer.ts

  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)
    }
  }

4.mountComponent

首先在该组件中,执行了createComponentInstance方法,创建了组件实例instance,然后执行setupComponent,也就是在这其中执行了我们传入的setup,我们接着往下看。

//源码路径 core/packages/runtime-core/src/renderer.ts

  const mountComponent: MountComponentFn = (
    initialVNode, // 根组件
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // 2.x compat may pre-create the component instance before actually
    // mounting
    const compatMountInstance =
      __COMPAT__ && initialVNode.isCompatRoot && initialVNode.component;

      const instance: ComponentInternalInstance =
      compatMountInstance ||
      (initialVNode.component = createComponentInstance(
        initialVNode,
        parentComponent,
        parentSuspense
      ))

    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
    )

    if (__DEV__) {
      popWarningContext()
      endMeasure(instance, `mount`)
    }
  }

5. setupComponent

该方法首先对propsslot进行了初始化,然后执行了关键的一步setStatefulComponent,我们接着往下看。

//源码路径 core/packages/runtime-core/src/component.ts

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
}

6.setStatefulComponent

该函数中首先通过setup.length来确定回调函数参数,如果有两个参数,则将执行createSetupComtext方法来构建我们的第二个参数ctx。然后通过callWithErrotHandling方法执行setup方法,判断其执行结果是否为Promise异步组件进行处理,反则执行handleSetupResult方法,对结果进行处理。

//源码路径 core/packages/runtime-core/src/component.ts

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions
  // 2. call setup()
  const { setup } = Component
  if (setup) {
    // 根据setup参数
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
// 异步组件处理
    if (isPromise(setupResult)) {
      setupResult.then(unsetCurrentInstance, unsetCurrentInstance)

      if (isSSR) {
        // return the promise so server-renderer can wait on it
        return setupResult
          .then((resolvedResult: unknown) => {
            handleSetupResult(instance, resolvedResult, isSSR)
          })
          .catch(e => {
            handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
          })
      } else if (__FEATURE_SUSPENSE__) {
        // async setup returned Promise.
        // bail here and wait for re-entry.
        instance.asyncDep = setupResult
      } else if (__DEV__) {
        warn(
          `setup() returned a Promise, but the version of Vue you are using ` +
            `does not support it yet.`
        )
      }
    } else {
      handleSetupResult(instance, setupResult, isSSR)
    }
  } else {
    finishComponentSetup(instance, isSSR)
  }
}

7.setupRenderEffect

在执行完 setup 之后,我们继续回到mountComponent中,执行该方法中的最后一个方法setupRenderEffect
下面源码仅展示一部分,主要再其中定义了一个方法componentUpdateFn,当该组件中的响应式数据发生改变时,就会执行该方法更新页面。实现data 驱动 视图。我们可以看到componentUpdateFn中又执行了patch,进行递归渲染页面。我们继续往下看。

//源码路径 core/packages/runtime-core/src/renderer.ts

  const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const componentUpdateFn = () => {
      if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, parent } = instance
        const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)
        if(el && hydrateNode){
        // 服务器渲染
        } else {
          const subTree = (instance.subTree = renderComponentRoot(instance))
          patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )

          initialVNode.el = subTree.el
        }
        instance.isMounted = true
      }
      
    // create reactive effect for rendering
    
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(instance.update),
      instance.scope // track it in component's effect scope
    ))
    const update = (instance.update = effect.run.bind(effect) as SchedulerJob)

    update.id = instance.uid
    // allowRecurse
    // #1801, #2043 component render effects should allow recursive updates
    toggleRecurse(instance, true)


    update()
  }

四. patch 再次执行

上一次我们在patch中进入了processComponent中,这次,我们进入proccessElement,我们继续往下看

1. processElement

可以从下面源码中看到,通过判断n1 == null来判断是否为第一次执行,如果第一次执行则进入mountElement方法中,否则进入patchElement中,我们先来看看mountElement中发生了什么。

//源码路径 core/packages/runtime-core/src/renderer.ts

 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 {
      patchElement(
        n1,
        n2,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }

2. mountElement

下边源码省略部分实现,该方法中主要执行了两个操作,一是 hostCreateElement方法,二是hostInsert,除此之外,判断该element是否还有子节点,有的话执行mountChildren,再重复之前的方法。

  • hostCreateElement: 用来创建一个Element,类似于document.createELement
  • hostInsert:用来将Element插入到页面中,类似于document.insertbefore
//源码路径 core/packages/runtime-core/src/renderer.ts

  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
   
   el = vnode.el = hostCreateElement(
        vnode.type as string,
        isSVG,
        props && props.is,
        props
      )
  }
  
 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
     )
   }
  
  hostInsert(el, container, anchor)

五. 总结

整个render渲染流程大致如下:\

render>patch>processComponent>mountComponent>生成instance>setupComponent>setStatefulComponent(在此阶段执行我们传入的setup)>setupRenderEffect(此阶段生成更新函数componentUpdateFn)>>patch>processELement>mountElement>hostCreateELement>hostInsertrender -> patch -> processComponent -> mountComponent -> 生成instance -> setupComponent -> setStatefulComponent(在此阶段执行我们传入的setup) -> setupRenderEffect(此阶段生成更新函数componentUpdateFn) ->-> patch -> processELement -> mountElement -> hostCreateELement -> hostInsert