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(二)
相关推荐
涔溪40 分钟前
Ecmascript(ES)标准
前端·elasticsearch·ecmascript
榴莲千丞1 小时前
第8章利用CSS制作导航菜单
前端·css
奔跑草-1 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与1 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
guokanglun1 小时前
CSS样式实现3D效果
前端·css·3d
咔咔库奇1 小时前
ES6进阶知识一
前端·ecmascript·es6
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss
龙猫蓝图2 小时前
vue el-date-picker 日期选择器禁用失效问题
前端·javascript·vue.js
fakaifa2 小时前
CRMEB Pro版v3.1源码全开源+PC端+Uniapp前端+搭建教程
前端·小程序·uni-app·php·源码下载
夜色呦2 小时前
掌握ECMAScript模块化:构建高效JavaScript应用
前端·javascript·ecmascript