在前几篇文章中我们一起学习了 Vue3 中新颖的 Composition API,而今天我要带大家一起看一下 Vue3 中的另一个新鲜的写法 —— setup。
在绝大多数情况,我们书写的组件都是有状态的组件,而这类组件在初始化的过程中会被标记为 stateful comonents,当 Vue3 检测到我们在处理这类有状态组件时,就会调用函数 setupStatefulComponent ,来初始化一个状态化组件。处理组件部分的源码位置在: @vue/runtime-core/src/component.ts
。
setupStatefulComponent
接下来我就带着大家一起来剖析一下 setupStatefulComponent 的过程:
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions
if (__DEV__) { /* 检测组件名称、指令、编译选项等等,有错误则报警 */ }
// 0. 创建一个渲染代理的属性的访问缓存
instance.accessCache = Object.create(null)
// 1. 创建一个公共的示例或渲染器代理
// 它将被标记为 raw,所以它不会被追踪
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// 2. 调用 setup()
const { setup } = Component
if (setup) {
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
currentInstance = instance
pauseTracking()
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
resetTracking()
currentInstance = null
if (isPromise(setupResult)) {
if (isSSR) {
// 返回一个 promise,因此服务端渲染可以等待它执行。
return setupResult
.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
.catch(e => {
handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
})
}
} else {
// 捕获 Setup 执行结果
handleSetupResult(instance, setupResult, isSSR)
}
} else {
// 完成组件初始化
finishComponentSetup(instance, isSSR)
}
}
组件一开始会初始化一个 Component 变量,其中保存着组件的选项。接下来如果是 DEV 环境,则会开始检测组件中的各种选项的命名,比如 name、components、directives 等,如果检测有问题,就会在开发环境报出警告。
在检测完毕后,会开始正经的初始化过程,首先会在实例上创建一个 accessCache 的属性,该属性用以缓存渲染器代理属性,以减少读取次数。之后会在组件实例上初始化一个代理属性,这个代理属性代理了组件的上下文,并且将它设置为观察原始值,这样这个代理对象将不会被追踪。
之后就开始处理我们本文关心的 setup 逻辑了。首先从组件中取出 setup 函数,这里判断是否存在 setup 函数,如果不存在,则直接跳转到底部逻辑,执行 finishComponentSetup,完成组件初始化。否则就会进入 if (setup)
之后的分支条件中。
是否执行 createSetupContext 生成 setup 的上下文对象,取决于 setup 函数中形参的数量是否大于 1。
这里需要注意的一个知识点是:在 function 函数对象上调用 length 时,返回值是这个函数的形参数量。
举个例子:
setup() // setup.length === 0
setup(props) // setup.length === 1
setup(props, { emit, attrs }) // setup.length === 2
默认情况下,props 是调用 setup 时必传的参数,所以是否需要去生成 setup 的上下文的条件就是 setup.length > 1 。
那么顺着代码逻辑,我们一起来看一下 setup 上下文中究竟有些什么东西。
export function createSetupContext(
instance: ComponentInternalInstance
): SetupContext {
const expose: SetupContext['expose'] = exposed => {
instance.exposed = proxyRefs(exposed)
}
if (__DEV__) {
/* DEV 逻辑忽略,对上下文选项设置 getter */
} else {
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit,
expose
}
}
}
expose 的妙用
看到这段 createSetupContext 函数的逻辑,我们发现 setup 上下文中就如文档中描述的一样,有 attrs、slots、emit 这三种熟悉的属性,而在这里惊奇的发现竟然还有一个文档中未说明的 expose 属性返回。
expose 是早先 Vue RFC 中的一个提案,expose 的设想是提供一个像 expose({ ...publicMembers })
这样的组合式 API,这样组件的作者就可以在 setup() 中使用该 API 来清除地控制哪些内容会明确地公开暴露给组件使用者。
当你在封装组件时,如果嫌 ref 中暴露的内容过多,不妨用 expose 来约束一下输出。当然这还仅仅是一个 RFC 提案,感兴趣的小伙伴可以偷偷尝鲜哦。
import { ref } from 'vue'
export default {
setup(_, { expose }) {
const count = ref(0)
function increment() {
count.value++
}
// 仅仅暴露 increment 给父组件
expose({
increment
})
return { increment, count }
}
}
例如当你像上方代码一样使用 expose 时,父组件获取的 ref 对象里只会有 increment 属性,而 count 属性将不会暴露出去。
执行 setup 函数
在处理完 setupContext 的上下文后,组件会停止依赖收集,并且开始执行 setup 函数。
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
Vue 会通过 callWithErrorHandling 调用 setup 函数,这里我们可以看最后一行,是作为 args 参数传入的,与上文描述一样,props 会始终传入,若是 setup.length <= 1 , setupContext 则为 null。
调用完 setup 之后,会重置依赖收集状态。接下来判断 setupResult 的返回值类型。
如果 setup 函数的返回值是 promise 类型,并且是服务端渲染的,则会等待继续执行。否则就会报错,说当前版本的 Vue 并不支持 setup 返回 promise 对象。
如果不是 promise 类型返回值,则会通过 handleSetupResult 函数来处理返回结果。
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
// setup 返回了一个行内渲染函数
if (__NODE_JS__ && (instance.type as ComponentOptions).__ssrInlineRender) {
// 当这个函数的名字是 ssrRender (通过 SFC 的行内模式编译)
// 将函数作为服务端渲染函数
instance.ssrRender = setupResult
} else {
// 否则将函数作为渲染函数
instance.render = setupResult as InternalRenderFunction
}
} else if (isObject(setupResult)) {
// 将返回对象转换为响应式对象,并设置为实例的 setupState 属性
instance.setupState = proxyRefs(setupResult)
}
finishComponentSetup(instance, isSSR)
}
在 handleSetupResult 这个结果捕获函数中,首先判断 setup 返回结果的类型,如果是一个函数,并且又是服务端的行内模式渲染函数,则将该结果作为 ssrRender 属性;而在非服务端渲染的情况下,会直接当做 render 函数来处理。
接着会判断 setup 返回结果如果是对象,就会将这个对象转换成一个代理对象,并设置为组件实例的 setupState 属性。
最终还是会跟其他没有 setup 函数的组件一样,调用 finishComponentSetup 完成组件的创建。
finishComponentSetup
这个函数的主要作用是获取并为组件设置渲染函数,对于模板(template)以及渲染函数的获取方式有以下三种规范行为:
1、渲染函数可能已经存在,通过 setup 返回了结果。例如我们在上一节讲的 setup 的返回值为函数的情况。
2、如果 setup 没有返回,则尝试获取组件模板并编译,从 Component.render
中获取渲染函数,
3、如果这个函数还是没有渲染函数,则将 instance.render
设置为空,以便它能从 mixins/extend 等方式中获取渲染函数。
这个在这种规范行为的指导下,首先判断了服务端渲染的情况,接着判断没有 instance.render 存在的情况,当进行这种判断时已经说明组件并没有从 setup 中获得渲染函数,在进行第二种行为的尝试。从组件中获取模板,设置好编译选项后调用 Component.render = compile(template, finalCompilerOptions)
进行编译,这部分编译的知识在我的第一篇文章编译流程中有过详细介绍。
最后将编译后的渲染函数赋值给组件实例的 render 属性,如果没有则赋值为 NOOP 空函数。
接着判断渲染函数是否是使用了 with 块包裹的运行时编译的渲染函数,如果是这种情况则会将渲染代理设置为一个不同的 has
handler 代理陷阱,它的性能更强并且能够去避免检测一些全局变量。
至此组件的初始化完毕,渲染函数也设置结束了。
export function finishComponentSetup(
instance: ComponentInternalInstance,
isSSR: boolean,
skipOptions?: boolean
) {
const Component = instance.type as ComponentOptions
// 模板 / 渲染函数的规范行为
// 1、渲染函数可能已经存在,通过 setup 返回
// 2、除此之外尝试使用 `Component.render` 当做渲染函数
// 3、如果这个函数没有渲染函数,设置 `instance.render` 为空函数,以便它能从 mixins/extend 中获得渲染函数
if (__NODE_JS__ && isSSR) {
instance.render = (instance.render ||
Component.render ||
NOOP) as InternalRenderFunction
} else if (!instance.render) {
// 可以在 setup() 中设置
if (compile && !Component.render) {
const template = Component.template
if (template) {
const { isCustomElement, compilerOptions } = instance.appContext.config
const {
delimiters,
compilerOptions: componentCompilerOptions
} = Component
const finalCompilerOptions: CompilerOptions = extend(
extend(
{
isCustomElement,
delimiters
},
compilerOptions
),
componentCompilerOptions
)
Component.render = compile(template, finalCompilerOptions)
}
}
instance.render = (Component.render || NOOP) as InternalRenderFunction
// 对于使用 `with` 块的运行时编译的渲染函数,这个渲染代理需要不一样的 `has` handler 陷阱,它有更好的
// 性能表现并且只允许白名单内的 globals 属性通过。
if (instance.render._rc) {
instance.withProxy = new Proxy(
instance.ctx,
RuntimeCompiledPublicInstanceProxyHandlers
)
}
}
}
总结
今天我介绍了一个有状态的组件的初始化的过程,在 setup 函数初始化部分进行了仔细的讲解,我们不仅学习了 setup 上下文初始化的条件,也明确的知晓了 setup 上下文究竟给我们暴露了哪些属性,并且从中学到了一个新的 RFC 提案: expose 属性。
我们学习了 setup 函数执行的过程以及 Vue 是如何处理捕获 setup 的返回结果的。
最后我们讲解了组件初始化时,不论是否使用 setup 都会执行的 finishComponentSetup 函数,通过这个函数内部的逻辑我们了解了一个组件在初始化完毕时,渲染函数设置的规则。
如果这篇文章能够帮助到你了解 Vue3 中 setup 的小细节,希望能给本文点一个喜欢❤️。如果想继续追踪后续文章,也可以关注我的账号,再次谢谢能阅读至此的你。