【Vue.js 3.0源码】Composition API之组件渲染初始化过程(下)

148 阅读7分钟

自我介绍:大家好,我是吉帅振的网络日志;微信公众号:吉帅振的网络日志;前端开发工程师,工作4年,去过上海、北京,经历创业公司,进过大厂,现在郑州敲代码。

一、执行 setup 函数并获取结果

const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])

我们具体来看一下 callWithErrorHandling 函数的实现:

function callWithErrorHandling (fn, instance, type, args) {

  let res

  try {

    res = args ? fn(...args) : fn()

  }

  catch (err) {

    handleError(err, instance, type)

  }

  return res

}

可以看到,它其实就是对 fn 做的一层包装,内部还是执行了 fn,并在有参数的时候传入参数,所以 setup 的第一个参数是 instance.props,第二个参数是 setupContext。函数执行过程中如果有 JavaScript 执行错误就会捕获错误,并执行 handleError 函数来处理。

执行 setup 函数并拿到了返回的结果,那么接下来就要用 handleSetupResult 函数来处理结果

handleSetupResult(instance, setupResult)

我们详细看一下 handleSetupResult 函数的实现:

function handleSetupResult(instance, setupResult) {

  if (isFunction(setupResult)) {

    // setup 返回渲染函数

    instance.render = setupResult

  }

  else if (isObject(setupResult)) {

    // 把 setup 返回结果变成响应式

    instance.setupState = reactive(setupResult)

  }

  finishComponentSetup(instance)

}

可以看到,当 setupResult 是一个对象的时候,我们把它变成了响应式并赋值给 instance.setupState,这样在模板渲染的时候,依据前面的代理规则,instance.ctx 就可以从 instance.setupState 上获取到对应的数据,这就在 setup 函数与模板渲染间建立了联系。

另外 setup 不仅仅支持返回一个对象,也可以返回一个函数作为组件的渲染函数。我们可以改写前面的示例,来看一下这时的情况:

<script>

  import { h } from 'vue'

  export default {

    props: {

      msg: String

    },

    setup (props, { emit }) {

      function onClick () {

        emit('toggle')

      }

      return (ctx) => {

        return [

          h('p', null, ctx.msg),

          h('button', { onClick: onClick }, 'Toggle')

        ]

      }

    }

  }

</script>

这里,我们删除了 HelloWorld 子组件的 template 部分,并把 setup 函数的返回结果改成了函数,也就是说它会作为组件的渲染函数,一切运行正常。

在 handleSetupResult 的最后,会执行 finishComponentSetup 函数完成组件实例的设置,其实这个函数和 setup 函数的执行结果已经没什么关系了,提取到外面放在 handleSetupResult 函数后面执行更合理一些。

另外当组件没有定义的 setup 的时候,也会执行 finishComponentSetup 函数去完成组件实例的设置。

二、完成组件实例设置

接下来我们来看一下 finishComponentSetup 函数的实现:

function finishComponentSetup (instance) {

  const Component = instance.type

  // 对模板或者渲染函数的标准化

  if (!instance.render) {

    if (compile && Component.template && !Component.render) {

      // 运行时编译

      Component.render = compile(Component.template, {

        isCustomElement: instance.appContext.config.isCustomElement || NO

      })

      Component.render._rc = true

    }

    if ((process.env.NODE_ENV !== 'production') && !Component.render) {

      if (!compile && Component.template) {

        // 只编写了 template 但使用了 runtime-only 的版本

        warn(`Component provided template option but ` +

          `runtime compilation is not supported in this build of Vue.` +

          (` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`

          ) /* should not happen */)

      }

      else {

        // 既没有写 render 函数,也没有写 template 模板

        warn(`Component is missing template or render function.`)

      }

    }

    // 组件对象的 render 函数赋值给 instance

    instance.render = (Component.render || NOOP)

    if (instance.render._rc) {

      // 对于使用 with 块的运行时编译的渲染函数,使用新的渲染上下文的代理

      instance.withProxy = new Proxy(instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers)

    }

  }

  // 兼容 Vue.js 2.x Options API

  {

    currentInstance = instance

    applyOptions(instance, Component)

    currentInstance = null

  }

}

函数主要做了两件事情:标准化模板或者渲染函数和兼容 Options API。接下来我们详细分析这两个流程。

标准化模板或者渲染函数

在分析这个过程之前,我们需要了解一些背景知识。组件最终通过运行 render 函数生成子树 vnode,但是我们很少直接去编写 render 函数,通常会使用两种方式开发组件。

第一种是使用 SFC(Single File Components)单文件的开发方式来开发组件,即通过编写组件的 template 模板去描述一个组件的 DOM 结构。我们知道 .vue 类型的文件无法在 Web 端直接加载,因此在 webpack 的编译阶段,它会通过 vue-loader 编译生成组件相关的 JavaScript 和 CSS,并把 template 部分转换成 render 函数添加到组件对象的属性中。

另外一种开发方式是不借助 webpack 编译,直接引入 Vue.js,开箱即用,我们直接在组件对象 template 属性中编写组件的模板,然后在运行阶段编译生成 render 函数,这种方式通常用于有一定历史包袱的古老项目。

因此 Vue.js 在 Web 端有两个版本:runtime-only 和 runtime-compiled。我们更推荐用 runtime-only 版本的 Vue.js,因为相对而言它体积更小,而且在运行时不用编译,不仅耗时更少而且性能更优秀。遇到一些不得已的情况比如上述提到的古老项目,我们也可以选择 runtime-compiled 版本。

runtime-only 和 runtime-compiled 的主要区别在于是否注册了这个 compile 方法。

在 Vue.js 3.0 中,compile 方法是通过外部注册的:

let compile;

function registerRuntimeCompiler(_compile) {

    compile = _compile;

}

回到标准化模板或者渲染函数逻辑,我们先看 instance.render 是否存在,如果不存在则开始标准化流程,这里主要需要处理以下三种情况。

  1. compile 和组件 template 属性存在render 方法不存在的情况。此时, runtime-compiled 版本会在 JavaScript 运行时进行模板编译,生成 render 函数。
  2. compile 和 render 方法不存在,组件 template 属性存在的情况。此时由于没有 compile,这里用的是 runtime-only 的版本,因此要报一个警告来告诉用户,想要运行时编译得使用 runtime-compiled 版本的 Vue.js。
  3. 组件既没有写 render 函数,也没有写 template 模板,此时要报一个警告,告诉用户组件缺少了 render 函数或者 template 模板。

处理完以上情况后,就要把组件的 render 函数赋值给 instance.render。到了组件渲染的时候,就可以运行 instance.render 函数生成组件的子树 vnode 了。

另外对于使用 with 块运行时编译的渲染函数,渲染上下文的代理是 RuntimeCompiledPublicInstanceProxyHandlers,它是在之前渲染上下文代理 PublicInstanceProxyHandlers 的基础上进行的扩展,主要对 has 函数的实现做了优化:

const RuntimeCompiledPublicInstanceProxyHandlers = {

  ...PublicInstanceProxyHandlers,

  get(target, key) {

    if (key === Symbol.unscopables) {

      return

    }

    return PublicInstanceProxyHandlers.get(target, key, target)

  },

  has(_, key) {

    // 如果 key 以 _ 开头或者 key 在全局变量白名单内,则 has 为 false

    const has = key[0] !== '_' && !isGloballyWhitelisted(key)

    if ((process.env.NODE_ENV !== 'production') && !has && PublicInstanceProxyHandlers.has(_, key)) {

      warn(`Property ${JSON.stringify(key)} should not start with _ which is a reserved prefix for Vue internals.`)

    }

    return has

  }

}

这里如果 key 以 _ 开头,或者 key 在全局变量的白名单内,则 has 为 false,此时则直接命中警告,不用再进行之前那一系列的判断了。

了解完标准化模板或者渲染函数流程,我们来看完成组件实例设置的最后一个流程——兼容 Vue.js 2.x 的 Options API。

Options API:兼容 Vue.js 2.x

我们知道 Vue.js 2.x 是通过组件对象的方式去描述一个组件,之前我们也说过,Vue.js 3.0 仍然支持 Vue.js 2.x Options API 的写法,这主要就是通过 applyOptions方法实现的。

function applyOptions(instance, options, deferredData = [], deferredWatch = [], asMixin = false) {

  const {

    // 组合

    mixins, extends: extendsOptions,

    // 数组状态

    props: propsOptions, data: dataOptions, computed: computedOptions, methods, watch: watchOptions, provide: provideOptions, inject: injectOptions,

    // 组件和指令

    components, directives,

    // 生命周期

    beforeMount, mounted, beforeUpdate, updated, activated, deactivated, beforeUnmount, unmounted, renderTracked, renderTriggered, errorCaptured } = options;



  // instance.proxy 作为 this

  const publicThis = instance.proxy;

  const ctx = instance.ctx;



  // 处理全局 mixin

  // 处理 extend

  // 处理本地 mixins

  // props 已经在外面处理过了

  // 处理 inject

  // 处理 方法

  // 处理 data

  // 处理计算属性

  // 处理 watch

  // 处理 provide

  // 处理组件

  // 处理指令

  // 处理生命周期 option

}

由于 applyOptions 的代码特别长,所以这里我用注释列出了它主要做的事情,感兴趣的同学可以去翻阅它的源码。

三、总结

组件的初始化流程,主要包括创建组件实例和设置组件实例。通过进一步细节的深入,我们也了解了渲染上下文的代理过程;了解了 Composition API 中的 setup 启动函数执行的时机,以及如何建立 setup 返回结果和模板渲染之间的联系;了解了组件定义的模板或者渲染函数的标准化过程;了解了如何兼容 Vue.js 2.x 的 Options API。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿