Vue3源码解析之 render component(一)

1,444 阅读9分钟

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue3 源码解析系列第 12 篇,关注专栏

前言

前面我们分析了 render 函数对 虚拟 DOM 的渲染、更新、删除等,以及 DOM 的属性、样式、事件的挂载更新,本篇我们就来看下 render 函数是如何对 component 组件 进行挂载更新的。

回顾

通过 h 函数我们了解到,组件 本身是一个 对象,它必须包含一个 render 函数,该函数决定了它的渲染内容;如果我们想定义 数据,则需要通过 data 选项 进行注册,data 选项 应该是一个函数,并且返回 一个对象,该对象中包含了所有的 响应式数据

组件 又分为 有状态组件无状态组件 ,而 Vue 中通常把 状态 比作 数据 的意思。所谓 有状态组件 指拥有自己的状态(data),可以响应数据的变化、执行生命周期钩子函数,以及触发重新渲染。无状态组件 通常是基于传递的属性(props)进行渲染,不拥有自己的状态,也不关心数据变化,生命周期钩子函数也不会执行。

下面我们先来分析 无状态组件 是如何挂载更新的。

案例

首先引入 h 、 render 函数,声明一个 component1 包含 render 函数的组件对象,通过 h 函数生成 组件 vnode 对象,之后通过 render 函数渲染该对象,两秒后重新声明一个 component2 组件对象进行更新渲染。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>

  <body>
    <div id="app"></div>
    <script>
      const { h, render } = Vue

      const component1 = {
        render() {
          return h('div', 'hello component')
        }
      }

      const vnode1 = h(component1)

      render(vnode1, document.querySelector('#app'))

      setTimeout(() => {
        const component2 = {
          render() {
            return h('div', 'update component')
          }
        }

        const vnode2 = h(component2)

        render(vnode2, document.querySelector('#app'))
      }, 2000)
    </script>
  </body>
</html>

render component

我们知道 render 函数的挂载是从 patch 方法开始的:

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
  }
  
 const patch: PatchFn = (
    n1, // 旧节点
    n2, // 新节点
    container, // 容器
    anchor = null, // 锚点
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    // 省略
    
    const { type, ref, shapeFlag } = n2
    // 根据 新节点类型 判断
    switch (type) {
      // 省略
      
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          // 省略
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } 
        // 省略
    }

    // 省略
  }  

先看下传入的 新节点 n2 的值:

render-component.png

当前节点 type 类型为包含 render 函数的对象,shapeFlag4 表示是组件,根据判断执行 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) {
      // keep-alive 组件
      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)
    }
  }

该方法通过判断 旧节点 n1 是否存在来执行挂载或更新,由于首次渲染,n1 不存在且不为 keep-alive 类型组件,之后执行 mountComponent 方法:

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

    // 省略

    // resolve props and slots for setup context
    if (!(__COMPAT__ && compatMountInstance)) {
      if (__DEV__) {
        startMeasure(instance, `init`)
      }
      setupComponent(instance)
      if (__DEV__) {
        endMeasure(instance, `init`)
      }
    }

    // 省略

    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )

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

该方法先通过 createComponentInstance 函数创建了 组件实例 并赋值给 initialVNode.component 上,initialVNode 为我们传入的 新节点 n2,该方法定义在 packages/runtime-core/src/component.ts 文件中:

export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
) {
  const type = vnode.type as ConcreteComponent
  
  // 省略

  const instance: ComponentInternalInstance = {
    uid: uid++,
    vnode,
    type,
    parent,
    // 省略
  }
  
  // 省略

  return instance
}

可见 createComponentInstance 方法最终返回的是一个实例对象,该对象中的 type新节点的 type ,也就是我们传入的包含 render 函数的对象 { render() { return h('div', 'hello component') } }。此时 新节点 中就存在一个 component 的组件实例:

render-comp-instance.png

之后执行 setupComponent 方法,该方法定义在 packages/runtime-core/src/component.ts 文件中:

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

我们暂时先不关注 isStatefulComponentinitPropsinitSlots 这三个方法,因为当前为 无状态组件 渲染,接着执行 setupStatefulComponent 方法:

function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions

  // 省略
  const { setup } = Component
  if (setup) {
    // 省略
  } else {
    finishComponentSetup(instance, isSSR)
  }
}

当前组件实例 type 中不存在 setup 属性,执行 finishComponentSetup 方法:

export function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean,
  skipOptions?: boolean
) {
  const Component = instance.type as ComponentOptions

  // 省略

  // template / render function normalization
  // could be already set when returned from setup()
  if (!instance.render) {
    // 省略

    instance.render = (Component.render || NOOP) as InternalRenderFunction

    // for runtime-compiled render functions using `with` blocks, the render
    // proxy used needs a different `has` handler which is more performant and
    // also only allows a whitelist of globals to fallthrough.
    if (installWithProxy) {
      installWithProxy(instance)
    }
  }

  // 省略
}

先将组件实例的 type 赋值给 Component,由于当前组件实例不存在 render 属性,则将组件的 Component.render 也就是我们传入的组件对象赋值给实例的 instance.render 上,此时 组件实例 instance 就具备了 render 方法:

render-comp-render.png

可见 finishComponentSetup 方法主要是将 组件实例的 render 指向了 组件对象的 render 上,也就是说 setupStatefulComponent 方法主要是将 组件的 render 进行赋值。

继续执行 mountComponent 方法中的 setupRenderEffect 方法:

setupRenderEffect(
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
)

该方法是组件渲染的核心方法:

const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const componentUpdateFn = () => {
      // 省略
    }

    // create reactive effect for rendering
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(update),
      instance.scope // track it in component's effect scope
    ))

    const update: SchedulerJob = (instance.update = () => effect.run())
    update.id = instance.uid
    // 省略

    update()
  }

先创建了一个 componentUpdateFn 匿名函数,然后创建了一个 ReactiveEffect 响应式的实例 effect,并赋值给组件实例的 instance.effect 上。ReactiveEffect 我们在前面文章中也已经讲解过,它是响应式系统的关键,主要用于依赖追踪和副作用的管理:

export class ReactiveEffect<T = any> {
  // 省略

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }

  run() {
    // 省略
    
    try {
      // 省略
      
      return this.fn()
    } finally {
      // 省略

      if (this.deferStop) {
        this.stop()
      }
    }
  }

  stop() {
    // 省略
  }
}

ReactiveEffect 第一个参数 fn 为传入的匿名函数 componentUpdateFn,第二个参数 scheduler 调度器为 () => queueJob(update)queueJob 之前也讲过通过队列形式来执行 update 方法。

之后声明一个 update 函数 () => effect.run() 并将赋值给组件实例的 instance.update 上,最后执行 update 方法等同于执行 effect.run(),我们知道执行 run 方法实际执行的是 this.fn()componentUpdateFn 匿名函数的执行,我们回过来再看下 componentUpdateFn 方法:

const componentUpdateFn = () => {
  if (!instance.isMounted) {
    // 省略

    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

    // 省略
  } else {
    // 省略
  }
}

我们知道 Vue 定义了一些生命周期状态,当前 isMountedfalse,之后根据判断声明 subTree 执行 renderComponentRoot 方法,它被定义在 packages/runtime-core/src/componentRenderUtils.ts 文件中:

export function renderComponentRoot(
  instance: ComponentInternalInstance
): VNode {
  const {
    type: Component,
    vnode,
    proxy,
    withProxy,
    props,
    propsOptions: [propsOptions],
    slots,
    attrs,
    emit,
    render,
    renderCache,
    data,
    setupState,
    ctx,
    inheritAttrs
  } = instance

  let result
  let fallthroughAttrs
  const prev = setCurrentRenderingInstance(instance)
  if (__DEV__) {
    accessedAttrs = false
  }

  try {
    if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
      // withProxy is a proxy with a different `has` trap only for
      // runtime-compiled render functions using `with` block.
      const proxyToUse = withProxy || proxy
      result = normalizeVNode(
        render!.call(
          proxyToUse,
          proxyToUse!,
          renderCache,
          props,
          setupState,
          data,
          ctx
        )
      )
      fallthroughAttrs = attrs
    } else {
       // 省略
    }
  } catch (err) {
    // 省略
  }

 // 省略
  return result
}

根据判断 if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) 当前组件为无状态组件,所以结果为 true,执行 result = normalizeVNode() 对其赋值,我们知道 normalizeVNode 方法传入的参数如果是一个 对象类型 则直接返回该对象:

result = normalizeVNode(
    render!.call(
      proxyToUse,
      proxyToUse!,
      renderCache,
      props,
      setupState,
      data,
      ctx
    )
)

export function normalizeVNode(child: VNodeChild): VNode {
  if (child == null || typeof child === 'boolean') {
    // empty placeholder
    return createVNode(Comment)
  } else if (isArray(child)) {
    // fragment
    return createVNode(
      Fragment,
      null,
      // #3666, avoid reference pollution when reusing vnode
      child.slice()
    )
  } else if (typeof child === 'object') {
    // already vnode, this should be the most common since compiled templates
    // always produce all-vnode children arrays
    return cloneIfMounted(child)
  } else {
    // strings and numbers
    return createVNode(Text, null, String(child))
  }
}

而当前 render 是通过组件实例解构获取的,即我们传入的组件对象的 render 函数,通过 call 改变了 this 指向,我们再看下指向的 proxyToUse 是一个 proxy 对象:

render-comp-proxyToUse.png

那么执行 render 函数,实际执行的是 h('div', 'hello component'),得到的是一个 type 类型为 div,子节点为 hello componentvnode 对象,并将该对象赋值给了 result

render-comp-result.png

所以我们可以得知 subTree 实际是获取一个 vnode 对象,之后执行 patch 方法对其挂载,最后页面呈现:

render-comp-patch.png

render component update

根据案例,两秒后重新渲染组件,重新执行 render 函数中的 patch 方法:

const patch: PatchFn = (
    n1, // 旧节点
    n2, // 新节点
    container, // 容器
    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 (n1 && !isSameVNodeType(n1, n2)),我们再看下 isSameVNodeType 方法:

export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  if (
    __DEV__ &&
    n2.shapeFlag & ShapeFlags.COMPONENT &&
    hmrDirtyComponents.has(n2.type as ConcreteComponent)
  ) {
    // HMR only: if the component has been hot-updated, force a reload.
    return false
  }
  return n1.type === n2.type && n1.key === n2.key
}

由于当前 type 均为对象,新旧节点的 type 不同返回 false,执行 unmount 卸载方法,之后重新执行新节点的挂载逻辑,最终页面呈现:

render-comp-update.png

总结

  1. 组件的挂载执行的是 processComponent 方法中的 mountComponent 方法。
  2. 通过 createComponentInstance 方法获取到组件的实例,从而 组件组件实例 形成一个双向绑定的关系,即 instance.vnode = vnode,vnode.component = instance
  3. 通过 setupComponent 方法,对组件实例的 render 进行赋值 instance.render = Component.render
  4. 执行 setupRenderEffect 方法,通过创建一个 ReactiveEffect 响应式实例,利用 update 方法的执行来触发 componentUpdateFn 匿名函数的执行。
  5. 根据组件状态来生成 subTree,而 subTree 本质上是 renderComponentRoot 方法的返回值 vnode
  6. 最后通过 patch 方法对组件的挂载。

Vue3 源码实现

vue-next-mini

Vue3 源码解析系列

  1. Vue3源码解析之 源码调试
  2. Vue3源码解析之 reactive
  3. Vue3源码解析之 ref
  4. Vue3源码解析之 computed
  5. Vue3源码解析之 watch
  6. Vue3源码解析之 runtime
  7. Vue3源码解析之 h
  8. Vue3源码解析之 render(一)
  9. Vue3源码解析之 render(二)
  10. Vue3源码解析之 render(三)
  11. Vue3源码解析之 render(四)
  12. Vue3源码解析之 render component(一)
  13. Vue3源码解析之 render component(二)
  14. Vue3源码解析之 render component(三)
  15. Vue3源码解析之 render component(四)
  16. Vue3源码解析之 render component(五)
  17. Vue3源码解析之 diff(一)
  18. Vue3源码解析之 diff(二)
  19. Vue3源码解析之 compiler(一)
  20. Vue3源码解析之 compiler(二)
  21. Vue3源码解析之 compiler(三)
  22. Vue3源码解析之 createApp