likes
comments
collection
share

Vue3源码解析之 render component(一)

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

前言

前面我们分析了 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 的值:

Vue3源码解析之 render component(一)

当前节点 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 的组件实例:

Vue3源码解析之 render component(一)

之后执行 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 方法:

Vue3源码解析之 render component(一)

可见 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 对象:

Vue3源码解析之 render component(一)

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

Vue3源码解析之 render component(一)

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

Vue3源码解析之 render component(一)

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 卸载方法,之后重新执行新节点的挂载逻辑,最终页面呈现:

Vue3源码解析之 render component(一)

总结

  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 源码解析系列