目录:
-
一、介绍
-
二、使用 $emit更新父组件传递过来的数据(vue3.3以前)
-
三、使用 defineModel 更新父组件传递过来的数据(vue3.4)
- 1.defineModel传递多个v-model
-
四、defineModel源码
- 1.从编译后代码开始探索
- 2.defineModel源码
- 3.useModel 源码
- 4.genRuntimeEmits源码
-
五、总结
一、介绍
defineModel() 返回的值是一个 ref。
它可以像其他 ref 一样被访问以及修改。
它能起到在父组件和当前变量之间的双向绑定的作用。
它的 .value 和父组件的 v-model 的值同步。
当它被子组件改变时,会触发父组件绑定的值一起更新。
都知道,props 的设计原则是单项数据流。子组件默认情况下是无法更改父组件传递过来的数据。如果要更改vue3.3以前是通过 $emit 来实现的。
下面我们来对比一下使用 $emit 和 defineModel 来更新数据。
二、使用 $emit更新父组件传递过来的数据(vue3.3以前)
// 父组件
<template>
<div class="father">
<h1>我是父组件,子组件isShow的内容是:{{ isShow }}</h1>
<button @click="showSonHandle">显示子组件</button>
<Son v-model:isShow="isShow" />
</div>
</template>
<script setup>
import Son from '@/components/Son.vue'
import { ref } from 'vue';
const isShow = ref(true)
const showSonHandle = () => {
isShow.value = true
}
</script>
// 子组件
<script setup>
defineProps({
isShow: {
type: Boolean,
default: true
},
})
const emits = defineEmits(['update:isShow'])
const closeHandle = () => {
emits('update:isShow', false)
}
</script>
<template>
<div class="child" v-if="isShow">
<h1>我是子组件</h1>
<button @click="closeHandle">关闭</button>
</div>
</template>
三、使用 defineModel 更新父组件传递过来的数据(vue3.4)
// 父页面
<template>
<div class="father">
<h1>我是父组件,子组件isShow的内容是:{{ isShow }}</h1>
<button @click="showSonHandle">显示子组件</button>
<Son v-model:isShow="isShow" />
</div>
</template>
<script setup>
import Son from '@/components/Son.vue'
import { ref } from 'vue';
const isShow = ref(true)
const showSonHandle = () => {
isShow.value = true
}
</script>
// 子页面
<script setup>
const isShowBool = defineModel('isShow')
const closeHandle = () => {
isShowBool.value = false
}
</script>
<template>
<div class="child" v-if="isShowBool">
<h1>我是子组件</h1>
<button @click="closeHandle">关闭</button>
</div>
</template>
❝
可以发现使用起来简直太棒了!比之前使用emit好用太多,它可以直接与父组件传的变量进行双向绑定。直接写上 const 变量名 = defineModel('双向绑定的值')
1.defineModel传递多个v-model
<template>
<div class="father">
<h1>我是父组件,son组件userName的值:{{ userName }}。email的值:{{ email }}</h1>
<Son v-model:userName="userName" v-model:email="email" />
</div>
</template>
<script setup>
import Son from '@/components/Son.vue'
import { ref } from 'vue';
const userName = ref('如花')
const email = ref('110@163.com')
</script>
// 子组件
<script setup>
const userName = defineModel('userName')
const email = defineModel('email')
</script>
<template>
<div class="child">
<h1>我是子组件</h1>
<form action="#">
<div>
<label for="">用户名:</label>
<input type="text" v-model="userName">
</div>
<div>
<label for="">邮箱:</label>
<input type="text" v-model="email">
</div>
</form>
</div>
</template>
❝
虽然可以传递多个,但是最好还是少用,而且通常来说,使用props方式父子组件通信,传的参数都是特别的,例如封装弹框组件,取消和确定的时候关闭当前组件。参数不会太多,总之遵守和函数方式一样最好,尽量别超过3个。
到这里,对于基本的用法我们是知道了,但是具体源码层面,我们还不是很清楚,接下来,我们往深一层看看。
四、defineModel源码
在之前,v-model我们是知道它的语法糖的
<input v-model="message">
等价于
<input
:value="message"
@input="message = $event.target.value"
>
- 1.绑定数据:将表单元素的值与 Vue 实例中的数据进行绑定。
- 2.监听事件:监听表单元素的输入事件(如 input 或 change),并更新 Vue 实例中的数据。
1.从编译后代码开始探索
要验证上面的猜想,我们可以通过查看编译之后的Vue代码来完成。
这里我们通过Vue 官方 Playground来作为查看编译后代码的工具,同样是实现上面的例子,来看看编译后的Vue源码是怎么样的 👇
/* Analyzed bindings: {
"userName": "setup-ref",
"email": "setup-ref"
} */
import { useModel as _useModel } from 'vue'
const __sfc__ = {
__name: 'App',
// 核心代码
props: {
"userName": {},
"userNameModifiers": {},
"email": {},
"emailModifiers": {},
},
// 核心代码
emits: ["update:userName", "update:email"],
setup(__props, { expose: __expose }) {
__expose();
// 核心代码
const userName = _useModel(__props, 'userName')
const email = _useModel(__props, 'email')
function render(_ctx, _cache, $props, $setup, $data, $options) {
// 核心代码
_createElementVNode("input", {
type: "text",
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => (($setup.userName) = $event))
}, null, 512 /* NEED_PATCH */), [
[_vModelText, $setup.userName]
])
}
__sfc__.render = render
__sfc__.__scopeId = "data-v-7ba5bd90"
__sfc__.__file = "src/App.vue"
export default __sfc__
❝
通过上面的源码可以很清晰地看到,defineModel的核心其实是_useModel函数,通过_useModel为注册了v-model的props执行双向绑定操作。
那就让我们继续看看_useModel。
首先我们找到defineModel(packages/compiler-sfc/src/script/defineModel.ts)的源码,在92行中可以找到defineModel是通过调用useModel函数来实现的。
2.defineModel源码
export function processDefineModel(
ctx: ScriptCompileContext,
node: Node,
declId?: LVal,
): boolean {
if (!isCallOf(node, DEFINE_MODEL)) {
return false
}
// 将该组件标记为使用了defineModel
ctx.hasDefineModelCall = true
...
// 这里调用了useModel
ctx.s.overwrite(
ctx.startOffset! + node.callee.start!,
ctx.startOffset! + node.callee.end!,
ctx.helper('useModel'),
)
// 并将对应的prop作为参数传递
ctx.s.appendLeft(
ctx.startOffset! +
(node.arguments.length ? node.arguments[0].start! : node.end! - 1),
`__props, ` +
(hasName
? ``
: `${JSON.stringify(modelName)}${optionsRemoved ? `` : `, `}`),
)
return true
}
接下来就是defineModel的核心,useModel(packages/runtime-core/src/helpers/useModel.ts)的实现了👇
3.useModel 源码
xport function useModel<
M extends PropertyKey,
T extends Record<string, any>,
K extends keyof T,
G = T[K],
S = T[K],
>(
props: T,
name: K,
options?: DefineModelOptions<T[K], G, S>,
): ModelRef<T[K], M, G, S>
export function useModel(
props: Record<string, any>,
name: string,
options: DefineModelOptions = EMPTY_OBJ,
): Ref {
const i = getCurrentInstance()!
if (__DEV__ && !i) {
warn(`useModel() called without active instance.`)
return ref() as any
}
const camelizedName = camelize(name)
if (__DEV__ && !(i.propsOptions[0] as NormalizedProps)[camelizedName]) {
warn(`useModel() called with prop "${name}" which is not declared.`)
return ref() as any
}
const hyphenatedName = hyphenate(name)
const modifiers = getModelModifiers(props, camelizedName)
const res = customRef((track, trigger) => {
let localValue: any
let prevSetValue: any = EMPTY_OBJ
let prevEmittedValue: any
// 通过监听props传来的值是否更新,需要同步更新一下
watchSyncEffect(() => {
const propValue = props[camelizedName]
if (hasChanged(localValue, propValue)) {
localValue = propValue
trigger()
}
})
return {
get() {
track()
return options.get ? options.get(localValue) : localValue
},
set(value) {
const emittedValue = options.set ? options.set(value) : value
// 隐式注册`update:modelValue`事件
i.emit(`update:${name}`, emittedValue)
// 如果本地值通过 setter 进行了转换,
// 但传递给父组件的值没有变化,父组件不会触发任何更新,
// 也就不会进行属性同步。
// 然而,本地的输入状态可能会因此失去同步,
// 因此我们需要在这里强制进行一次更新。
if (
hasChanged(value, emittedValue) &&
hasChanged(value, prevSetValue) &&
!hasChanged(emittedValue, prevEmittedValue)
) {
trigger()
}
// 将本次更新的内容缓存起来
prevSetValue = value
prevEmittedValue = emittedValue
},
}
})
// 返回一个标记为ref的对象,当对这个对象进行赋值时即执行事件的发布
return res
}
到这里,有了大概的流程。但是确实最后一个注册事件。 在源码packages/compiler-sfc/src/compileScript.ts 目录下,948行发现了emits 是调用 genRuntimeEmits(packages/compiler-sfc/src/script/defineEmits.ts[48行])
4.genRuntimeEmits源码
function genRuntimeEmits(ctx: ScriptCompileContext): string | undefined {
let emitsDecl = ''
...
// 这里在上面processDefineModel 设置过值
if (ctx.hasDefineModelCall) {
let modelEmitsDecl = `[${Object.keys(ctx.modelDecls)
.map(n => JSON.stringify(`update:${n}`))
.join(', ')}]`
emitsDecl = emitsDecl
? `/*@__PURE__*/${ctx.helper(
'mergeModels',
)}(${emitsDecl}, ${modelEmitsDecl})`
: modelEmitsDecl
}
return emitsDecl
}
这两个地方它通过hasDefineModelCall 判断,然后将事件合并了进去。
五、总结
最后得到这个图。
Vue官方编译代码网站:play.vuejs.org/ Vue代码:github.com/vuejs/core