这是你不知道的defineModel!
相信大伙都已经收到Vue3
最新版的风了吧,新版本的更新中优化了不少此前在Vue3
中比较“麻烦”的使用方法,下面是更新的简介图 👇
相信看完上面的简介图,大伙对新特性已经有一个大概的了解了,下面就进入正文:defineModel
是如何实现的
那接下来我就开始操作了🤺 (e 点点 q 点点 w 点点嘟嘟嘟嘟...)
defineModel核心
新旧对比
在开发的过程中,如果有需要通过子组件进行状态更新
的话,v-model
是一个绕不开的点。以前的v-model
是这样用的 👇
<!-- Father.vue -->
<template>
<span>count</span>
<Child v-model="count" />
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const count = ref<number>(0)
</script>
<!-- Child.vue -->
<template>
count: {{ count }}
<button @click="onClick">count</button>
</template>
<script lang="ts" setup>
const $props = defineProps<{ modelValue: number }>()
const $emits = defineEmit<{
(e: 'update:modelValue', modelValue: number)
// 注册update:modelValue事件,作为状态更新的回调
}>()
function onClick() {
$emits('update:modelValue', $props.modelValue++)
// 状态更新时发布事件
}
</script>
在有了defineModel
之后,我们就可以在Child.vue
中这样实现 👇
<!-- Child.vue -->
<template>
count: {{ count }}
<button @click="onClick">count</button>
</template>
<script lang="ts" setup>
const count = defineModel<number>()
// 一步到位,完成事件注册和监听状态变化并发布事件
function onClick() {
count += 1
}
</script>
相信看完上面的案例之后大伙就已经有一个大概的猜想了:
defineModel
其实为组件实例注册了update:modelValue
事件,并且在props
的setter
中又调用了update:modelValue
事件,从而实现的v-model
语法糖
上面的猜测又包含了两个问题:
defineModel
是如何注册update:modelValue
事件的- 如何在
defineModel变量
修改时发布update:modelValue
事件的
从编译后代码开始探索
要验证上面的猜想,我们可以通过查看编译之后的Vue
代码来完成。
这里我们通过Vue 官方 Playground来作为查看编译后代码的工具,同样是实现上面的例子,来看看编译后的Vue源码
是怎么样的 👇
// Father.vue
const __sfc__ = _defineComponent({
__name: 'App',
setup(__props) {
const count = ref(0)
return (_ctx,_cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("h1", null, _toDisplayString(count.value), 1 /* TEXT */),
_createVNode(Child, {
modelValue: count.value,
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => ((count).value = $event))
// 将v-model转换为modelValue属性以及update:modelValue事件
}, null, 8 /* PROPS */, ["modelValue"])
], 64 /* STABLE_FRAGMENT */))
}
}
})
// Child.vue
const __sfc__ = _defineComponent({
__name: 'Child',
props: {
"modelValue": {},
},
emits: ["update:modelValue"],
setup(__props) {
const compCount = _useModel(__props, "modelValue")
// 核心代码
// 调用_useModel对传入的modelValue属性进行处理
return (_ctx,_cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createTextVNode(" Comp count: " + _toDisplayString(compCount.value) + " ", 1 /* TEXT */),
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = ($event) => (compCount.value++))
}, " press ")
], 64 /* STABLE_FRAGMENT */))
}
}
})
通过上面的源码可以很清晰地看到,defineModel
的核心其实是_useModel
函数,通过_useModel
为注册了v-model
的props
执行双向绑定
操作。
那就让我们继续Deep Down
🤿,从Vue3源码中一探这_useModel
究竟是何方神圣~
如何发布事件
首先我们找到defineModel
的源码,在92行
中可以找到defineModel
是通过调用useModel
函数来实现的👇
export function processDefineModel(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal
): boolean {
if (!ctx.options.defineModel || !isCallOf(node, DEFINE_MODEL)) {
return false
}
ctx.hasDefineModelCall = true // 将该组件标记为使用了defineModel
...
// 在这里对被绑定到子组件的props进行标记,被标记为props类型的值将会在defineProps中被合并为组件的props
// 由于这里不属于本文讨论的内容,如需查看请前往源码仓库
ctx.s.overwrite(
ctx.startOffset! + node.start!,
ctx.startOffset! + node.end!,
`${ctx.helper('useModel')}(__props, ${JSON.stringify(modelName)}${
runtimeOptions ? `, ${runtimeOptions}` : ``
})`
// 从这里可以找到调用了useModel,并将对应的prop作为参数传递👆
)
return true
}
那么接下来就是defineModel
的核心,useModel
的实现了👇
export function useModel(
props: Record<string, any>,
name: string,
options?: { local?: boolean }
): Ref {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
warn(`useModel() called without active instance.`)
// 当组件实例不存在时则返回ref
return ref() as any
}
if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[name]) {
warn(`useModel() called with prop "${name}" which is not declared.`)
// 当useModel被一个不存在的prop调用时,返回ref
return ref() as any
}
// 通过watch监听或setter时发布事件的形式实现在修改时同步更新prop,而不需要显性注册`update:modelValue`事件
if (options && options.local) {
// 确认是否在defineModel中配置local属性为true
const proxy = ref<any>(props[name])
watch(
() => props[name],
v => (proxy.value = v)
)
watch(proxy, value => {
if (value !== props[name]) {
i.emit(`update:${name}`, value)
}
})
return proxy
} else {
return {
__v_isRef: true,
get value() {
return props[name]
},
set value(value) {
i.emit(`update:${name}`, value)
}
} as any
// 直接返回一个标记为ref的对象,当对这个对象进行赋值时即执行事件的发布
}
}
如何注册update:modelValue
事件
到此为止,defineModel
的主体基本上已经较为清晰地展现出来了,但我们的第一个问题仍没有解决,defineModel
是如何注册update:modelValue
事件的?
其实这个问题已经很明显了,在上面的processDefineModel
源码中,我将这段代码单独留下并进行标注👇
ctx.hasDefineModelCall = true // 将该组件标记为使用了defineModel
其实在这里defineModel
就已经将这个组件标记为hasDefineModelCall
,后续在defineEmits
源码中我们可以找到defineEmit
会自动为被标记为hasDefineModelCall
的组件注册对应名称的update
事件👇
export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined {
...
if (ctx.hasDefineModelCall) {
// 对标记为使用了defineModel的实例进行处理
let modelEmitsDecl = `[${Object.keys(ctx.modelDecls)
.map(n => JSON.stringify(`update:${n}`))
// 为每一个使用defineModel注册的prop属性进行事件注册
.join(', ')}]`
emitsDecl = emitsDecl
? `${ctx.helper('mergeModels')}(${emitsDecl}, ${modelEmitsDecl})`
: modelEmitsDecl
// 将使用defineEmits注册的事件和使用defineModel注册的事件合并
}
return emitsDecl
}
新的问题
其实到这为止,defineModel
的整个执行过程已经基本讲解完毕了,但是在看useModel
的源码时我发现了一个问题,为什么要将option
区分为local
和非local
呢?
带着这个问题,我请教了chatGPT老师
,得到了下面的答复👇
好吧,我承认我没看懂,于是乎我找到了关于defineModel
的Discussion并且在尤大给的Demo中找到了我想要的答案👇
结语
其实本来想和defineProps是如何解构仍保持响应式
一起写的,但是感觉如果放在一篇文章中篇幅就太长了,阅读体验不好,所以就放到下一篇中解析吧
如果文中有任何错误或者需要修改的地方,烦请指出,不胜感激
PS: 大伙都看
蜘蛛侠:纵横宇宙
了吗,真好看啊!特别是迈尔斯和格温看纽约的那个镜头,让我有一种在看边缘行者的快感😎,打算这周末去二刷了
我的个人博客:johnsonhuang_blog