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(二)
相关推荐
云白冰10 分钟前
hiprint结合vue2项目实现静默打印详细使用步骤
前端·javascript·vue.js
葡萄架子18 分钟前
Python中的logger作用(from loguru import logger)
java·前端·python
Hi_MrXiao26 分钟前
前端实现图片压缩插件(image-compressorionjs)
前端
阿智@1134 分钟前
Node.js 助力前端开发:自动化操作实战
运维·前端·node.js·自动化
m0_748251721 小时前
前端入门之VUE--ajax、vuex、router,最后的前端总结
前端·vue.js·ajax
上等猿1 小时前
Ajax笔记
前端·笔记·ajax
Amo 67291 小时前
css 编写注意-1-命名约定
前端·css
customer082 小时前
【开源免费】基于SpringBoot+Vue.JS安康旅游网站(JAVA毕业设计)
java·vue.js·spring boot·后端·kafka·开源·旅游
程序猿online2 小时前
nvm安装使用,控制node版本
开发语言·前端·学习
web Rookie2 小时前
React 中 createContext 和 useContext 的深度应用与优化实战
前端·javascript·react.js