我想告诉你的 Vue3 最新特性原理之defineModel篇

相信大伙都已经收到Vue3最新版的风了吧,新版本的更新中优化了不少此前在Vue3中比较“麻烦”的使用方法,下面是更新的简介图 ?

Vue3.3.4

相信看完上面的简介图,大伙对新特性已经有一个大概的了解了,下面就进入正文: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事件,并且在propssetter中又调用了update:modelValue事件,从而实现的v-model语法糖

上面的猜测又包含了两个问题:

  1. defineModel是如何注册update:modelValue事件的
  2. 如何在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))
}, 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.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 */))
    }
  }
})
// 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-modelprops执行双向绑定操作。

那就让我们继续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.`)
// 当组件实例不存在时则返回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
  }
}
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 // 将该组件标记为使用了defineModel
ctx.hasDefineModelCall = true // 将该组件标记为使用了defineModel
ctx.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老师,得到了下面的答复?
vue3-defineModel-local

好吧,我承认我没看懂,于是乎我找到了关于defineModel的Discussion并且在尤大给的Demo中找到了我想要的答案?
why-defineModel-local

结语

其实本来想和defineProps是如何解构仍保持响应式一起写的,但是感觉如果放在一篇文章中篇幅就太长了,阅读体验不好,所以就放到下一篇中解析吧

如果文中有任何错误或者需要修改的地方,烦请指出,不胜感激

PS: 大伙都看蜘蛛侠:纵横宇宙了吗,真好看啊!特别是迈尔斯和格温看纽约的那个镜头,让我有一种在看边缘行者的快感?,打算这周末去二刷了

我的个人博客:johnsonhuang_blog

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY5AENPG' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片