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(二)
相关推荐
wearegogog1235 小时前
基于 MATLAB 的卡尔曼滤波器实现,用于消除噪声并估算信号
前端·算法·matlab
Drawing stars5 小时前
JAVA后端 前端 大模型应用 学习路线
java·前端·学习
品克缤5 小时前
Element UI MessageBox 增加第三个按钮(DOM Hack 方案)
前端·javascript·vue.js
小二·5 小时前
Python Web 开发进阶实战:性能压测与调优 —— Locust + Prometheus + Grafana 构建高并发可观测系统
前端·python·prometheus
小沐°6 小时前
vue-设置不同环境的打包和运行
前端·javascript·vue.js
Irene19916 小时前
Vue3 <Suspense> 使用指南与注意事项
vue.js·suspense
qq_419854056 小时前
CSS动效
前端·javascript·css
烛阴6 小时前
3D字体TextGeometry
前端·webgl·three.js
acheding6 小时前
Vue3 + AntV/X6 自定义节点实践:组件化节点与事件联动
前端框架·vue
桜吹雪7 小时前
markstream-vue实战踩坑笔记
前端