相信大伙都已经收到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><!-- 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><!-- 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)}>()function onClick() {$emits('update:modelValue', $props.modelValue++)}</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) }>() function onClick() { $emits('update:modelValue', $props.modelValue++) } </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) }>() 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><!-- Child.vue --> <template> count: {{ count }} <button @click="onClick">count</button> </template> <script lang="ts" setup> const count = defineModel<number>() function onClick() { count += 1 } </script><!-- 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.vueconst __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))}, null, 8 /* PROPS */, ["modelValue"])], 64 /* STABLE_FRAGMENT */))}}})// 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)) }, null, 8 /* PROPS */, ["modelValue"]) ], 64 /* STABLE_FRAGMENT */)) } } })// 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)) }, null, 8 /* PROPS */, ["modelValue"]) ], 64 /* STABLE_FRAGMENT */)) } } })
// Child.vueconst __sfc__ = _defineComponent({__name: 'Child',props: {"modelValue": {},},emits: ["update:modelValue"],setup(__props) {const compCount = _useModel(__props, "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 */))}}})// Child.vue const __sfc__ = _defineComponent({ __name: 'Child', props: { "modelValue": {}, }, emits: ["update:modelValue"], setup(__props) { const compCount = _useModel(__props, "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 */)) } } })// Child.vue const __sfc__ = _defineComponent({ __name: 'Child', props: { "modelValue": {}, }, emits: ["update:modelValue"], setup(__props) { const compCount = _useModel(__props, "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)}${// 从这里可以找到调用了useModel,并将对应的prop作为参数传递?runtimeOptions ? `, ${runtimeOptions}` : ``})`)return true}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)}${ // 从这里可以找到调用了useModel,并将对应的prop作为参数传递? runtimeOptions ? `, ${runtimeOptions}` : `` })` ) return true }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)}${ // 从这里可以找到调用了useModel,并将对应的prop作为参数传递? runtimeOptions ? `, ${runtimeOptions}` : `` })` ) 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.`)// 当组件实例不存在时则返回refreturn ref() as any}if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[name]) {warn(`useModel() called with prop "${name}" which is not declared.`)// 当useModel被一个不存在的prop调用时,返回refreturn ref() as any}// 通过watch监听或setter时发布事件的形式实现在修改时同步更新prop,而不需要显性注册`update:modelValue`事件if (options && options.local) {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}}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) { 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 } }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) { 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 } }
如何注册update:modelValue
事件
到此为止,defineModel
的主体基本上已经较为清晰地展现出来了,但我们的第一个问题仍没有解决,defineModel
是如何注册update:modelValue
事件的?
其实这个问题已经很明显了,在上面的processDefineModel
源码中,我将这段代码单独留下并进行标注?
ctx.hasDefineModelCall = true // 将该组件标记为使用了defineModelctx.hasDefineModelCall = true // 将该组件标记为使用了defineModelctx.hasDefineModelCall = true // 将该组件标记为使用了defineModel
其实在这里defineModel
就已经将这个组件标记为hasDefineModelCall
,后续在defineEmits
源码中我们可以找到defineEmit
会自动为被标记为hasDefineModelCall
的组件注册对应名称的update
事件?
export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined {...if (ctx.hasDefineModelCall) {let modelEmitsDecl = `[${Object.keys(ctx.modelDecls).map(n => JSON.stringify(`update:${n}`)).join(', ')}]`emitsDecl = emitsDecl? `${ctx.helper('mergeModels')}(${emitsDecl}, ${modelEmitsDecl})`: modelEmitsDecl}return emitsDecl}export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined { ... if (ctx.hasDefineModelCall) { let modelEmitsDecl = `[${Object.keys(ctx.modelDecls) .map(n => JSON.stringify(`update:${n}`)) .join(', ')}]` emitsDecl = emitsDecl ? `${ctx.helper('mergeModels')}(${emitsDecl}, ${modelEmitsDecl})` : modelEmitsDecl } return emitsDecl }export function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined { ... if (ctx.hasDefineModelCall) { let modelEmitsDecl = `[${Object.keys(ctx.modelDecls) .map(n => JSON.stringify(`update:${n}`)) .join(', ')}]` emitsDecl = emitsDecl ? `${ctx.helper('mergeModels')}(${emitsDecl}, ${modelEmitsDecl})` : modelEmitsDecl } return emitsDecl }
新的问题
其实到这为止,defineModel
的整个执行过程已经基本讲解完毕了,但是在看useModel
的源码时我发现了一个问题,为什么要将option
区分为local
和非local
呢?
带着这个问题,我请教了chatGPT老师
,得到了下面的答复?
好吧,我承认我没看懂,于是乎我找到了关于defineModel
的Discussion并且在尤大给的Demo中找到了我想要的答案?
结语
其实本来想和defineProps是如何解构仍保持响应式
一起写的,但是感觉如果放在一篇文章中篇幅就太长了,阅读体验不好,所以就放到下一篇中解析吧
如果文中有任何错误或者需要修改的地方,烦请指出,不胜感激
PS: 大伙都看
蜘蛛侠:纵横宇宙
了吗,真好看啊!特别是迈尔斯和格温看纽约的那个镜头,让我有一种在看边缘行者的快感?,打算这周末去二刷了
我的个人博客:johnsonhuang_blog