Vue3 从defineProps使用到原理分析
引言
组件间的通信一直是我们开发过程中非常常见的场景,今天主要讲的是Vue3中的父子组件通信。
本篇统一采用vue3的
setup语法糖 +ts写法进行说明,并结合Vue2的写法进行对照。
使用方法
假设我们有两个组件<Father />、<Child />。
父传子Props
-
在子组件中定义要接受的数据(及其类型)。
<script setup lang="ts"> defineProps<{ msg?: string, count: number }>() </script>在Vue3的setup中定义
props时,我们需要使用defineProps这个宏定义api。可能有些刚接触Vue3的同学不太清楚什么叫宏定义api,简单来说,就类似于window.setTimeout一样,是全局注入的api,可以直接setTimeout()进行使用。当然实际上,对于
defineProps的处理是在模板编译的时候处理的。在使用的时候,一般是3种写法, 第1种比较规范的写法,也就是如上所示,用ts的泛型来定义props需要传入的字段及其类型。
第2种写法:(和第一种写法一样)
<script setup> defineProps({ msg: { type: String, required: false }, count: { type: Number, required: true } }) </script>第3种是不用ts的写法,如下所示:
<script setup> defineProps(['msg', 'count']) </script>但是这种写法会没有类型提示,并且所有的参数都会变成可传的。
对应的Vue2/3中
Option API写法如下:<script> export default { props: ['msg', 'count'] } </script> -
在父组件中引入子组件。
import Child from './Child.vue' -
父组件给子组件传递所需数据。
<template> <Child msg="123" :count="123" /> </template>
注意:prop前不写冒号:的时候,默认传递给子组件是字符串,添加冒号如:propA='123'的时候,就是两个引号中间的值(或者变量)对应的类型。
子传父emit
- 子组件中定义要向父组件传递的函数名,以及参数。(除了第一个参数是事件名,后面的全是会传递给父组件的参数)此处需要使用
defineEmits这个宏定义api。const emit = defineEmits<{ (e: 'change', id: number): void }>() // or 不用ts写法 const emit = defineEmits(['change']) - 父组件定义接受来自子组件发送数据的函数。
此处的参数要和function handleChange(id: number) { // 自己的逻辑 }defineEmits中对应上。 - 父组件绑定自定义事件到子组件上。
<template> <Child msg="123" :count="123" @change="handleChange" /> </template> - 子组件中触发
emit,父组件触发回调函数。 Child子组件:
或者下面这种写法:<template> <div> <button @click="handleClick">Change</button> </div> </template> <script setup lang="ts"> const emit = defineEmits<{ (e: 'change', id: number): void }>() function handleClick() { emit('change', 123) } </script>
注意:在template中直接使用<template> <div> <button @click="$emit('change', 123)">Change</button> </div> </template>emit的时候,需要加上$也就是$emit()。
原理分析
那我们在使用这些宏定义的API,如defineProps、defineEmits的时候有没有想一下他们是如何实现的呢,通过源码我们来举例分析一下:
模板编译
既然是宏定义API,我们书写的时候不需要引入或者声明这个方法,那说明要么是在使用之前注入的,要么就是在模板编译的时候识别到代码进行转换的,我们带着问题去看看Vue的源码中是如何实现的。
首先我们全局搜索一下defineProps,发现在packages/compiler-sfc/src/script/defineProps.ts有这么两行声明:
export const DEFINE_PROPS = 'defineProps'
export const WITH_DEFAULTS = 'withDefaults'
同样在这个文件中,有两个函数叫做processDefineProps、processWithDefaults引用了这个宏变量。
export function processDefineProps(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal
) {
if (!isCallOf(node, DEFINE_PROPS)) {
return processWithDefaults(ctx, node, declId)
}
if (ctx.hasDefinePropsCall) {
ctx.error(`duplicate ${DEFINE_PROPS}() call`, node)
}
ctx.hasDefinePropsCall = true
ctx.propsRuntimeDecl = node.arguments[0]
// register bindings
if (ctx.propsRuntimeDecl) {
for (const key of getObjectOrArrayExpressionKeys(ctx.propsRuntimeDecl)) {
if (!(key in ctx.bindingMetadata)) {
ctx.bindingMetadata[key] = BindingTypes.PROPS
}
}
}
// call has type parameters - infer runtime types from it
if (node.typeParameters) {
if (ctx.propsRuntimeDecl) {
ctx.error(
`${DEFINE_PROPS}() cannot accept both type and non-type arguments ` +
`at the same time. Use one or the other.`,
node
)
}
ctx.propsTypeDecl = node.typeParameters.params[0]
}
if (declId) {
// handle props destructure
if (declId.type === 'ObjectPattern') {
processPropsDestructure(ctx, declId)
} else {
ctx.propsIdentifier = ctx.getString(declId)
}
}
return true
}
大家可以看到这段代码其实就是在解析我们在代码中声明的defineProps,另外个函数就是处理withDefault。这个函数又是在packages/compiler-sfc/src/compileScript.ts 中的 compileScript函数中调用的。
这个函数上面有一段注释: * Compile
<script setup>* It requires the whole SFC descriptor because we need to handle and merge * normal<script>+<script setup>if both are present.
其实大家不用看代码也能通过注释和函数名知道,这个函数就是在使用setup语法糖的时候解析我们的代码。从而去判断我们代码中使用的宏定义,并生成相关的数据。(这里有诸多模板编译的一些知识,就不再继续深入讨论)。
转化成虚拟DOM
在模板编译的过程中,会经历一系列的变化,最终转换成虚拟DOM也就是VNode。
模板 -> 模板AST -> Javascript AST -> 代码生成 -> 渲染函数 -> VNode
组件实例化和渲染
在上一步被编译成VNode之后,我们就会执行组件的实例化。在这个过程中,Vue会将父组件传递的属性值赋值给子组件的props。
我们都知道在Vue3中会使用统一的创建函数createApp对我们的App进行创建和挂载。其实createApp中主要是调用ensureRenderer函数,而ensureRenderer实际上真正调用的是createRenderer,最终执行的是baseCreateRenderer 函数。
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container)
container.removeAttribute('v-cloak')
return proxy
}
return app
}) as CreateAppFunction<Element>
const rendererOptions = {
patchProp, // 处理 props 属性
...nodeOps // 处理 DOM 节点操作
}
// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer: Renderer | HydrationRenderer
let enabledHydration = false
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
这里可能有很多函数,但是不重要,大家只需要记住createApp -> baseCreateRenderer这个结果
在packages/runtime-core/src/renderer.ts中的baseCreateRenderer函数中,如果识别到执行的是一个组件,那么就会调用名为mountComponent的内部函数,最终执行 setupComponent 方法对组件进行初始化。
//packages/runtime-core/src/component.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
}
我们可以看到,最终在setupComponent 中完成了对Props的初始化。
转载自:https://juejin.cn/post/7280007125999517748