Code前端首页关于Code前端联系我们

一、Vue2中v-model的本质,value input的语法糖

terry 7小时前 阅读数 8 #Vue
文章标签 model

不少同学在学Vue的时候,会疑惑“Vue2里的modelValue是啥?和v-model有啥关系?” 其实这里容易把Vue2和Vue3的概念弄混,得先把版本差异理清楚,再一步步看双向绑定的逻辑,今天就从Vue2的v-model原理说起,聊聊modelValue的来龙去脉,还有实际开发里怎么用好双向绑定~

Vue2里,v-model是个典型的**语法糖**——它把“传值+监听事件”这两个步骤合并成一个指令,让代码更简洁,但原生组件和自定义组件的v-model,底层逻辑又有点区别,得拆开看:

(1)原生表单元素的v-model

对于<input><textarea><select>这些原生表单组件,v-model的作用是“双向绑定数据”,比如写一个用户名输入框:

<template>
  <div>
    <input v-model="username" placeholder="请输入用户名">
    <p>你输入的是:{{ username }}</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      username: ''
    }
  }
}
</script>

这段代码等价于手动绑定value属性和监听input事件:

<input 
  :value="username" 
  @input="username = $event.target.value" 
  placeholder="请输入用户名"
>

能看到,原生组件的v-model,核心是通过value属性把数据传给DOM,再通过input事件把用户输入的新值传回给Vue实例,这里的value是DOM元素的固有属性,input是DOM元素的固有事件,Vue只是做了语法层面的简化。

(2)自定义组件的v-model

如果是自己写的自定义组件(比如封装的搜索框、下拉选择器),v-model的逻辑和原生组件类似,但依赖的是Vue组件间的props和事件通信,举个例子:封装一个带搜索建议的输入组件<SearchInput>,让它支持v-model绑定父组件的searchText

父组件用法

<template>
  <SearchInput v-model="searchText" />
  <p>当前搜索内容:{{ searchText }}</p>
</template>
<script>
import SearchInput from './SearchInput.vue'
export default {
  components: { SearchInput },
  data() {
    return {
      searchText: ''
    }
  }
}
</script>

子组件SearchInput.vue实现

<template>
  <div class="search-input">
    <input 
      :value="value" 
      @input="handleInput" 
      placeholder="搜索关键词"
    >
    <!-- 这里省略搜索建议列表的逻辑 -->
  </div>
</template>
<script>
export default {
  props: ['value'], // 接收父组件通过v-model传的值
  methods: {
    handleInput(e) {
      // 把用户输入的新值,通过input事件抛给父组件
      this.$emit('input', e.target.value)
    }
  }
}
</script>

这里的关键是:父组件用v-model时,会自动给子组件传一个叫value的props;子组件要更新值时,必须通过this.$emit('input', 新值)通知父组件,父组件收到input事件后,会自动更新自己的searchText变量——这就是自定义组件v-model的核心逻辑:props传value + 事件抛input

“modelValue”从哪来?Vue3的新玩法

如果是Vue3项目,你会发现自定义组件的v-model逻辑变了:默认传的props叫modelValue,触发更新的事件叫update:modelValue,比如Vue3中写自定义组件:

<!-- 子组件MyInput.vue -->
<template>
  <input 
    :value="modelValue" 
    @input="$emit('update:modelValue', $event.target.value)"
  >
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- 父组件用法 -->
<MyInput v-model="searchText" />

这时候父组件的v-model,等价于:modelValue="searchText" @update:modelValue="searchText = $event",能看到,Vue3把原来Vue2中“value+input”的默认组合,改成了“modelValue+update:modelValue”,让v-model的逻辑更统一(不管原生还是自定义组件,底层思路一致),也更灵活(支持多个v-model绑定)。

那为啥Vue2里会提到modelValue?大概率是学习时把Vue3的概念代入Vue2了,Vue2本身没有“modelValue”这个默认的props名,它默认用的是value(针对表单类自定义组件),但Vue2也有办法实现“自定义v-model的props和事件名”,这就要用到model选项

Vue2的model选项:自定义v-model的“绑定规则”

Vue2中,如果你不想让自定义组件的v-model依赖valueinput,可以用model选项来指定v-model对应的props名称触发更新的事件名称

举个实际场景:做一个开关组件<Switch>,希望v-model绑定的是checked状态,触发更新的事件是change(因为开关更适合用change事件,而非input事件)。

子组件<Switch>实现

<template>
  <button 
    class="switch-btn" 
    :class="{ active: checked }" 
    @click="handleClick"
  >
    {{ checked ? '开' : '关' }}
  </button>
</template>
<script>
export default {
  props: {
    checked: {
      type: Boolean,
      default: false
    }
  },
  model: {
    prop: 'checked', // 指定v-model对应的props名称
    event: 'change'  // 指定触发更新的事件名称
  },
  methods: {
    handleClick() {
      // 触发change事件,把新的状态抛给父组件
      this.$emit('change', !this.checked)
    }
  }
}
</script>
<style scoped>
.switch-btn {
  padding: 4px 12px;
  border: 1px solid #eee;
  border-radius: 4px;
}
.switch-btn.active {
  background: #42b983;
  color: #fff;
}
</style>

父组件用法

<template>
  <Switch v-model="isOpen" />
  <p>开关状态:{{ isOpen ? '开启' : '关闭' }}</p>
</template>
<script>
import Switch from './Switch.vue'
export default {
  components: { Switch },
  data() {
    return {
      isOpen: false
    }
  }
}
</script>

这时,父组件的<Switch v-model="isOpen" />等价于:

<Switch 
  :checked="isOpen" 
  @change="isOpen = $event" 
/>

能看到,Vue2通过model选项,实现了和Vue3中modelValue类似的效果——让v-model的绑定关系可配置,虽然名字不是modelValue,但思路是一致的:不再固定用value和input,而是允许开发者自定义v-model对应的props和事件。

Vue2的.sync修饰符:实现“多双向绑定”

Vue3里可以给一个组件绑多个v-model(比如<MyComp v-model:foo="a" v-model:bar="b" />),Vue2里要实现类似“多个数据双向绑定”的效果,得用.sync修饰符

.sync也是个语法糖,它的逻辑是:父组件传一个props(比如title),子组件通过$emit('update:title', 新值)来通知父组件更新,举个实际例子:做一个弹窗组件<Modal>,需要同时双向绑定“显示状态(visible)”和“标题(title)”。

子组件<Modal>实现

<template>
  <div class="modal-mask" v-show="visible">
    <div class="modal-content">
      <h2>{{ title }}</h2>
      <button @click="handleClose">关闭弹窗</button>
      <button @click="handleChangeTitle">修改标题</button>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    visible: {
      type: Boolean,
      default: false
    }, {
      type: String,
      default: '默认标题'
    }
  },
  methods: {
    handleClose() {
      // 通知父组件更新visible为false
      this.$emit('update:visible', false)
    },
    handleChangeTitle() {
      // 通知父组件更新title为新值
      this.$emit('update:title', '新的弹窗标题')
    }
  }
}
</script>
<style scoped>
.modal-mask {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.3);
  display: flex;
  justify-content: center;
  align-items: center;
}
.modal-content {
  background: #fff;
  padding: 20px;
  border-radius: 8px;
}
</style>

父组件用法

<template>
  <button @click="isShow = true">打开弹窗</button>
  <Modal 
    :visible.sync="isShow" sync="modalTitle" 
  />
  <p>当前标题:{{ modalTitle }}</p>
</template>
<script>
import Modal from './Modal.vue'
export default {
  components: { Modal },
  data() {
    return {
      isShow: false,
      modalTitle: '默认标题'
    }
  }
}
</script>

这里父组件的:visible.sync="isShow"等价于:

:visible="isShow" 
@update:visible="isShow = $event"
```  sync="modalTitle"`等价于`:title="modalTitle" @update:title="modalTitle = $event"`。  
可以发现,.sync的逻辑和Vue3中“v-model:xxx”的逻辑几乎一样,只是Vue2里叫`update:xxx`,Vue3里默认是`update:modelValue`,所以Vue2通过.sync,也能实现“一个组件绑定多个双向数据”的需求~  
### 五、从Vue2到Vue3,v-model的设计思路有啥变化?  
Vue2的v-model设计,其实有点“分裂”:  
- 对原生组件,用`value`(DOM属性) + `input`(DOM事件);  
- 对自定义组件,默认也用`value` + `input`,但可以通过`model`选项自定义;  
- 想实现多双向绑定,得用`.sync`修饰符,而.sync的语法和v-model又不一样(一个是`:xxx.sync`,一个是`v-model`)。  
Vue3则把这些逻辑**统一**了:  
- 不管原生还是自定义组件,v-model默认基于`modelValue`(props) + `update:modelValue`(事件);  
- 想自定义v-model的名称,直接用`v-model:xxx`,<MyComp v-model:foo="a" />`,对应props是`foo`,事件是`update:foo`;  
- 这样一来,v-model和.sync的功能合并了,API更简洁,也避免了Vue2里“同一功能两种语法”的问题。  
所以回到“Vue2里的modelValue”这个问题——它本身不是Vue2的概念,而是Vue3为了统一v-model逻辑提出的新默认props名,但学习时把Vue2的v-model、model选项、.sync和Vue3的modelValue联系起来,能更清晰看到Vue团队对“双向绑定”这个核心功能的设计演进:**从分散的语法糖,到统一、灵活的API**。  
### 六、Vue2开发中,双向绑定的常见坑和解决思路  
实际写Vue2代码时,关于v-model和双向绑定容易踩这些坑,得注意:  
#### (1)自定义组件中,props接收的“value”不能直接修改  
Vue的props是**单向数据流**——父组件传值给子组件,子组件只能读,不能直接改,所以在自定义组件里,拿到父组件传的`value`(或model选项指定的props)后,必须通过`$emit`事件通知父组件更新,不能自己赋值。  
错误示例(子组件直接改props):  
```vue
<template>
  <input :value="value" @input="handleInput">
</template>
<script>
export default {
  props: ['value'],
  methods: {
    handleInput(e) {
      this.value = e.target.value // 错误!props是只读的,不能直接修改
    }
  }
}
</script>

正确示例(用$emit通知父组件):

handleInput(e) {
  this.$emit('input', e.target.value) // 正确:触发事件,让父组件更新
}

(2)用model选项时,别忘在props里声明对应的属性

比如前面的<Switch>组件,model选项里写了prop: 'checked',那在props选项中必须声明checked,否则Vue会报错“Missing required prop: "checked"”。

反例(props没声明checked):

export default {
  // props里没写checked,但是model里指定了prop: 'checked'
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    // 这里漏了checked的声明
  },
  // ...
}

正例(props声明checked):

props: {
  checked: {
    type: Boolean,
    default: false
  }
},
model: {
  prop: 'checked',
  event: 'change'
},

(3).sync修饰符和v-model一起用,别搞混事件名

有些同学会在同一个组件上同时用v-model和.sync,

<Child v-model="a" :b.sync="b" />

这本身没问题,但要注意:

  • v-model对应的事件是input(或model选项指定的事件);
  • .sync对应的事件是update:b
    子组件里触发事件时,千万不能把事件名写混,比如子组件想更新b,必须用this.$emit('update:b', 新值),而不是this.$emit('input', 新值)

(4)父组件给子组件传值,v-model和:value同时用会冲突

<Child v-model="a" :value="b" />

这时子组件的value props会同时接收a(来自v-model)和b(来自:value),导致值混乱,因为v-model已经帮你传了value(除非用model选项改了prop名),所以别重复传value

Vue2的v-model和“modelValue”的联系

一句话概括:Vue2本身没有modelValue这个默认props,但Vue3用modelValue重构了v-model的逻辑

Vue2里实现双向绑定,核心靠这几套组合拳:

  • 原生组件v-model:value(DOM属性) + input(DOM事件);
  • 自定义组件v-model:value(props) + input(事件),或通过model选项自定义props和事件名;
  • 多双向绑定:.sync修饰符,基于xxx(props) + update:xxx(事件)。

而Vue3的modelValue,是对Vue2这些逻辑的统一和简化——把分散的v-model、model选项、.sync整合成“v-model:xxx”语法,默认用modelValue作为props名,让双向绑定的API更一致、更灵活。

所以学Vue2时,重点掌握v-model的语法糖本质、自定义组件的v-model实现(value+input或model选项)、.sync的用法;等学Vue3时,再对比理解modelValue的设计,就能自然衔接,明白Vue团队对双向绑定的迭代思路啦~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门