Vue3源码解析之 render component(二)

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

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

前言

我们知道 组件 分为 有状态组件无状态组件,上篇我们已经分析了 无状态组件,本篇我们就来讲解下 有状态组件 的挂载更新。

案例

首先引入 hrender 函数,声明一个 component 包含 datarender 函数的组件对象,通过 h 函数生成 组件 vnode 对象,之后通过 render 函数渲染该对象。

html 复制代码
<!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 component = {
        data() {
          return {
            msg: 'hello component'
          }
        },
        render() {
          return h('div', this.msg)
        }
      }

      const vnode = h(component)

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

render component

通过上篇我们得知 render 函数的组件挂载,会执行 patch 方法中 processComponentmountComponent 方法:

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

    // 省略

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

    // 省略
  }

通过 createComponentInstance 方法获取到组件的实例,从而 组件组件实例 形成一个双向绑定的关系,即 instance.vnode = vnode,vnode.component = instance,之后执行 setupComponent 方法:

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
}

接着执行 isStatefulComponent 方法对 isStateful 赋值:

ts 复制代码
export function isStatefulComponent(instance: ComponentInternalInstance) {
  return instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
}

根据判断当前返回结果为 4,之后执行 setupStatefulComponent 方法,由于 组件对象 中不存在 setup 属性,执行 finishComponentSetup 方法:

ts 复制代码
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

    // 省略
  }

  // support for 2.x options
  if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
    setCurrentInstance(instance)
    pauseTracking()
    applyOptions(instance)
    resetTracking()
    unsetCurrentInstance()
  }

  // 省略
}

该方法会将组件对象的 render 赋值给组件实例的 renderinstance.render = Component.render,之后执行 applyOptions 方法,该方法定义在 packages/runtime-core/src/componentOptions.ts 文件中:

ts 复制代码
export function applyOptions(instance: ComponentInternalInstance) {
  const options = resolveMergedOptions(instance)
  const publicThis = instance.proxy! as any
  
  // 省略

  const {
    // state
    data: dataOptions,
    computed: computedOptions,
    methods,
    watch: watchOptions,
    provide: provideOptions,
    inject: injectOptions,
    // lifecycle
    created,
    beforeMount,
    mounted,
    beforeUpdate,
    updated,
    activated,
    deactivated,
    beforeDestroy,
    beforeUnmount,
    destroyed,
    unmounted,
    render,
    renderTracked,
    renderTriggered,
    errorCaptured,
    serverPrefetch,
    // public API
    expose,
    inheritAttrs,
    // assets
    components,
    directives,
    filters
  } = options

  // 省略

  if (dataOptions) {
    // 省略
    
    const data = dataOptions.call(publicThis, publicThis)
    
    // 省略
    
    if (!isObject(data)) {
      __DEV__ && warn(`data() should return an object.`)
    } else {
      instance.data = reactive(data)
      if (__DEV__) {
        for (const key in data) {
          checkDuplicateProperties!(OptionTypes.DATA, key)
          // expose data on ctx during dev
          if (!isReservedPrefix(key[0])) {
            Object.defineProperty(ctx, key, {
              configurable: true,
              enumerable: true,
              get: () => data[key],
              set: NOOP
            })
          }
        }
      }
    }
  }

  // 省略

  registerLifecycleHook(onBeforeMount, beforeMount)
  registerLifecycleHook(onMounted, mounted)
  registerLifecycleHook(onBeforeUpdate, beforeUpdate)
  registerLifecycleHook(onUpdated, updated)
  registerLifecycleHook(onActivated, activated)
  registerLifecycleHook(onDeactivated, deactivated)
  registerLifecycleHook(onErrorCaptured, errorCaptured)
  registerLifecycleHook(onRenderTracked, renderTracked)
  registerLifecycleHook(onRenderTriggered, renderTriggered)
  registerLifecycleHook(onBeforeUnmount, beforeUnmount)
  registerLifecycleHook(onUnmounted, unmounted)
  registerLifecycleHook(onServerPrefetch, serverPrefetch)

  // 省略
}

该方法会先解构出组件实例对象的所有配置属性,即我们定义的 datarender生命周期 等函数,这里我们只需关注两个 dataOptionsdata, 以及 render 函数。

之后通过判断 if (dataOptions) 获取到 data 值,当前 dataOptions 为我们传入的 data 函数,返回值为 { msg: 'hello component'},即 data 值为 { msg: 'hello component' }。需要注意的是这里通过 call 来改变了 this 指向 dataOptions.call(publicThis, publicThis),这里的 publicThis 同上篇文章中讲到 proxyToUse 一样,也是一个 proxy 对象。

根据判断当前 data 为对象执行 instance.data = reactive(data),将组件实例的 data 赋值为 reactive 响应式数据。

接着遍历 data 对象将属性挂载到上下文中:

ts 复制代码
if (__DEV__) {
    for (const key in data) {
      checkDuplicateProperties!(OptionTypes.DATA, key)
      // expose data on ctx during dev
      if (!isReservedPrefix(key[0])) {
        Object.defineProperty(ctx, key, {
          configurable: true,
          enumerable: true,
          get: () => data[key],
          set: NOOP
        })
      }
    }
}

当前执行上下文及 publicThis 对象中就包含 msg 属性:

由此我们可以得知 setupComponent 方法主要做了两件事:一是赋值组件实例的 render,二是获取到 data 并通过 reactive 包装成响应式数据赋值给组件实例的 data

之后执行 setupRenderEffect 方法,通过上篇文章得知会执行 renderComponentRoot 来生成 subTree

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
}

通过 normalizeVNode 方法返回 vnode 对象对 result 赋值,我们再看下当前传入的参数,通过 call 改变 renderthis 指向:

ts 复制代码
render!.call(
  proxyToUse,
  proxyToUse!,
  renderCache,
  props,
  setupState,
  data,
  ctx
)

由于 proxyToUse 对象同 publicThis 包含了 msg 属性:

render 函数中 return h('div', this.msg)this.msg 就指向了 proxyToUsemsg,最终 result 赋值为含有 hello componentvnode 对象:

subTree 赋值完成,之后通过 patch 方法进行挂载,最终页面呈现:

总结

  1. 有状态组件 渲染核心其实就是将 render 函数中的 this.xx 变成一个真数据,要达到该目的,就需要改变 this 指向,改变方式就是在 subTree 赋值的方法 renderComponentRoot 中来改变 this

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(二)
相关推荐
小猪努力学前端4 分钟前
基于PixiJS的小游戏广告开发
前端·webgl·游戏开发
哆啦A梦158810 分钟前
62 对接支付宝沙箱
前端·javascript·vue.js·node.js
Tzarevich21 分钟前
用 OOP 思维打造可复用的就地编辑组件:EditInPlace 实战解析
javascript·前端框架
用户81686947472522 分钟前
Lane 优先级模型与时间切片调度
前端·react.js
虎头金猫22 分钟前
MateChat赋能电商行业智能导购:基于DevUI的技术实践
前端·前端框架·aigc·ai编程·ai写作·华为snap·devui
LiuMingXin22 分钟前
CESIUM JS 学习笔记 (持续更新)
前端·cesium
豆苗学前端32 分钟前
面试复盘:谈谈你对 原型、原型链、构造函数、实例、继承的理解
前端·javascript·面试
Crystal32842 分钟前
Git 基础:生成版本、撤消操作、版本重置、忽略文件
前端·git·github
lichenyang45343 分钟前
React 组件通讯全案例解析:从 Context 到 Ref 的实战应用
前端
姓王者1 小时前
chen-er 专为Chen式ER图打造的npm包
前端·javascript