自我介绍:大家好,我是吉帅振的网络日志;微信公众号:吉帅振的网络日志;前端开发工程师,工作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 是否存在,如果不存在则开始标准化流程,这里主要需要处理以下三种情况。
- compile 和组件 template 属性存在,render 方法不存在的情况。此时, runtime-compiled 版本会在 JavaScript 运行时进行模板编译,生成 render 函数。
- compile 和 render 方法不存在,组件 template 属性存在的情况。此时由于没有 compile,这里用的是 runtime-only 的版本,因此要报一个警告来告诉用户,想要运行时编译得使用 runtime-compiled 版本的 Vue.js。
- 组件既没有写 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。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。