第一章:核心概念 - 什么是 v-model

v-model 是 Vue 提供的一个语法糖,用于在表单输入元素自定义组件上实现双向数据绑定

  • 它的本质v-model 会根据所使用的元素类型,自动扩展为不同的属性绑定和事件监听。

  • 它的目的:简化手动连接值和事件监听器的过程,让开发者更专注于数据逻辑而非 DOM 操作。

基本工作原理:

  1. 数据 -> 视图:将数据绑定到元素的 value (或 checkedselected) 属性上。

  2. 视图 -> 数据:监听元素的 input (或 change) 事件,当事件触发时,将最新的值更新到数据中。


第二章:Vue 2 中的 v-model

2.1 在原生表单元素上的使用

在原生输入元素上,Vue 2 的 v-model 行为非常直观,会根据标签类型选择正确的属性和事件。

<input v-model="message" type="text">
<p>Message is: {{ message }}</p>

<!-- 等价于 -->
<input :value="message" @input="message = $event.target.value">
<p>Message is: {{ message }}</p>
<!-- 复选框 -->
<input v-model="isChecked" type="checkbox">
<!-- 等价于 -->
<input :checked="isChecked" @change="isChecked = $event.target.checked">

<!-- 单选框 -->
<input v-model="picked" type="radio" value="one">
<!-- 等价于 -->
<input :checked="picked === 'one'" @change="picked = $event.target.value">

<!-- 下拉选择框 -->
<select v-model="selected">
  <option value="a">A</option>
</select>
<!-- 等价于 -->
<select :value="selected" @change="selected = $event.target.value">
  <option value="a">A</option>
</select>
2.2 在自定义组件上的使用

在 Vue 2 中,一个组件上的 v-model 默认会利用一个名为 value 的 prop 和一个名为 input 的事件。

父组件 (Parent.vue)

<template>
  <ChildComponent v-model="pageTitle" />
</template>

<script>
export default {
  data() {
    return {
      pageTitle: 'Hello World'
    }
  }
}
</script>

子组件 (ChildComponent.vue)

<template>
  <input
    type="text"
    :value="value" <!-- 接收名为 value 的 prop -->
    @input="$emit('input', $event.target.value)" <!-- 发射名为 input 的事件 -->
  >
</template>

<script>
export default {
  props: ['value'] // 必须声明 value prop
}
</script>

上面的代码等价于:

<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
2.3 Vue 2 的 .sync 修饰符

有时,需要对一个 prop 进行“双向绑定”,但又不希望用 v-model 的 value 和 input 约定。为此,Vue 2 引入了 .sync 修饰符。

父组件

<ChildComponent :title.sync="pageTitle" />
<!-- 等价于 -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />

子组件
需要通过发射一个 update:myPropName 的事件来更新父组件的数据。

this.$emit('update:title', newTitle)

Vue 2 的局限:

  1. 一个组件只能有一个 v-model:因为默认只能绑定到 value 这个 prop 上。

  2. 命名冲突:如果组件需要一个真正的 value prop 用于其他目的,就会与 v-model 的约定产生冲突。

  3. 不够灵活v-model 和 .sync 是两套不同的机制,容易混淆。


第三章:Vue 3 中的 v-model

Vue 3 对 v-model 进行了重大改进,旨在解决 Vue 2 中的局限性,提供更大的灵活性和一致性。

3.1 在原生表单元素上的使用

在原生元素上,Vue 3 的 v-model 用法与 Vue 2 完全一致,没有破坏性变化。

<input v-model="message" type="text">
<textarea v-model="message"></textarea>
<input v-model="checked" type="checkbox">
<select v-model="selected">
  <option>A</option>
</select>
3.2 在自定义组件上的使用 (重大变化)

Vue 3 中,组件上的 v-model 默认不再使用 value prop 和 input 事件。

新的默认约定:

  • Prop: modelValue

  • 事件: update:modelValue

父组件 (Parent.vue)

<template>
  <ChildComponent v-model="pageTitle" />
</template>

<script>
export default {
  data() {
    return {
      pageTitle: 'Hello World'
    }
  }
}
</script>

子组件 (ChildComponent.vue)

<template>
  <input
    type="text"
    :value="modelValue" <!-- 接收名为 modelValue 的 prop -->
    @input="$emit('update:modelValue', $event.target.value)" <!-- 发射 update:modelValue 事件 -->
  >
</template>

<script>
export default {
  props: ['modelValue'], // 必须声明 modelValue prop
  emits: ['update:modelValue'] // 显式声明发射的事件更好
}
</script>

上面的代码等价于:

<ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event" />
3.3 Vue 3 的强大功能:多个 v-model

这是 Vue 3 解决 Vue 2 核心局限性的杀手锏。你现在可以在一个组件上绑定多个 v-model

父组件

<UserName
  v-model:first-name="firstName"
  v-model:last-name="lastName"
/>
<!-- 等价于 -->
<UserName
  :first-name="firstName"
  @update:first-name="firstName = $event"
  :last-name="lastName"
  @update:last-name="lastName = $event"
/>

子组件 (UserName.vue)

<template>
  <input
    type="text"
    :value="firstName"
    @input="$emit('update:firstName', $event.target.value)"
  >
  <input
    type="text"
    :value="lastName"
    @input="$emit('update:lastName', $event.target.value)"
  >
</template>

<script>
export default {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName']
}
</script>
3.4 自定义 v-model 修饰符

Vue 3 允许你为自定义组件的 v-model 创建修饰符(如 v-model.trim 用于原生输入框)。

假设想创建一个 capitalize 修饰符,它自动将输入的首字母大写。

父组件

<MyComponent v-model.capitalize="myText" />

子组件 (MyComponent.vue)

<template>
  <input
    type="text"
    :value="modelValue"
    @input="emitValue($event.target.value)"
  >
</template>

<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: { // 默认的修饰符对象,名为 modelModifiers
      default: () => ({}) // 默认是一个空对象
    }
  },
  emits: ['update:modelValue'],
  methods: {
    emitValue(value) {
      let formattedValue = value;
      // 检查 modifiers 对象中是否有 capitalize 属性且为 true
      if (this.modelModifiers.capitalize) {
        formattedValue = value.charAt(0).toUpperCase() + value.slice(1);
      }
      // 将格式化后的值发射出去
      this.$emit('update:modelValue', formattedValue);
    }
  }
}
</script>

对于带参数的 v-model (如 v-model:firstName),修饰符对象的名称为 arg + "Modifiers"

<!-- 父组件 -->
<UserName v-model:first-name.capitalize="firstName" />

<!-- 子组件 -->
<script>
export default {
  props: {
    firstName: String,
    firstNameModifiers: { // firstName + Modifiers
      default: () => ({})
    }
  },
  // ...
}
</script>

第四章:对比总结与迁移指南

特性Vue 2Vue 3说明
默认 PropvaluemodelValueVue 3 使用了新的默认 prop 名。
默认事件inputupdate:modelValueVue 3 使用了新的事件名。
多个 v-model不支持。需使用 .sync 修饰符。原生支持v-model:argVue 3 彻底解决了单组件多绑定的问题。
.sync 修饰符存在且常用。已废弃。其功能被 v-model:arg 替代。在 Vue 3 中,v-model:title 等同于 Vue 2 的 :title.sync
自定义修饰符可通过 model 选项配置,但较复杂。通过 modelModifiers 或 argModifiers prop 轻松访问Vue 3 的 API 设计更加直观和强大。
Vue 2 到 Vue 3 的迁移策略
  1. 将代码中的 .sync 部分替换为 v-model:

    • :title.sync="pageTitle" -> v-model:title="pageTitle"

  2. 将所有自定义组件的 v-model 进行调整

    • Prop: 将 value 改为 modelValue

    • 事件: 将 this.$emit('input', payload) 改为 this.$emit('update:modelValue', payload)

  3. 对于需要兼容 Vue 2 的组件,可以使用 Vue 3 的兼容配置(但通常建议为 Vue 3 创建新组件):

    // 在 Vue 3 组件中,如果想同时支持 Vue 2 的语法
    export default {
      props: ['modelValue', 'value'], // 同时接收两种 prop
      computed: {
        // 内部使用一个计算属性来统一数据源
        internalValue: {
          get() {
            return this.modelValue ?? this.value;
          },
          set(value) {
            // 优先发射 Vue 3 的事件
            this.$emit('update:modelValue', value);
            // 同时兼容发射 Vue 2 的事件
            this.$emit('input', value);
          }
        }
      }
    }

第五章 Vue 2 与 Vue 3 中 v-model 的源码级对比分析

实现思路

  1. 创建对比展示界面,左侧Vue 2,右侧Vue 3

  2. 实现基础用法、组件用法和修饰符用法的示例

  3. 添加源码解析部分展示关键实现代码

  4. 使用可视化方式展示v-model的数据流

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue 2 vs Vue 3 v-model 源码分析</title>
  <script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
  <script src="https://unpkg.com/vue@3.2.31/dist/vue.global.js"></script>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      line-height: 1.6;
      color: #333;
      background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
      min-height: 100vh;
      padding: 20px;
    }
    .container {
      max-width: 1400px;
      margin: 0 auto;
    }
    header {
      text-align: center;
      margin-bottom: 30px;
      padding: 20px;
      background: white;
      border-radius: 10px;
      box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
    }
    h1 {
      color: #2c3e50;
      margin-bottom: 10px;
    }
    .subtitle {
      color: #7f8c8d;
      font-size: 1.2em;
    }
    .comparison {
      display: flex;
      gap: 20px;
      margin-bottom: 30px;
    }
    @media (max-width: 900px) {
      .comparison {
        flex-direction: column;
      }
    }
    .vue2-section, .vue3-section {
      flex: 1;
      background: white;
      border-radius: 10px;
      overflow: hidden;
      box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
    }
    .section-header {
      background: #42b983;
      color: white;
      padding: 15px;
      text-align: center;
      font-size: 1.4em;
      font-weight: bold;
    }
    .vue2-section .section-header {
      background: #42b983;
    }
    .vue3-section .section-header {
      background: #3498db;
    }
    .content {
      padding: 20px;
    }
    .example {
      margin-bottom: 25px;
      padding: 15px;
      border-radius: 8px;
      background: #f8f9fa;
    }
    h3 {
      margin-bottom: 15px;
      color: #2c3e50;
      border-bottom: 2px solid #eee;
      padding-bottom: 8px;
    }
    .code-block {
      background: #2d2d2d;
      color: #f8f8f2;
      padding: 15px;
      border-radius: 5px;
      overflow-x: auto;
      margin: 15px 0;
      font-family: 'Fira Code', monospace;
      font-size: 0.9em;
    }
    .demo {
      padding: 15px;
      background: white;
      border-radius: 8px;
      margin: 15px 0;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    }
    input, button {
      padding: 10px;
      margin: 5px;
      border: 1px solid #ddd;
      border-radius: 4px;
      width: 100%;
    }
    button {
      background: #42b983;
      color: white;
      cursor: pointer;
      transition: background 0.3s;
    }
    button:hover {
      background: #359f74;
    }
    .source-analysis {
      background: white;
      border-radius: 10px;
      padding: 20px;
      box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
      margin-bottom: 30px;
    }
    .conclusion {
      background: white;
      border-radius: 10px;
      padding: 20px;
      box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
      text-align: center;
    }
    .highlight {
      color: #e96900;
      font-weight: bold;
    }
    .data-flow {
      display: flex;
      justify-content: space-around;
      align-items: center;
      margin: 20px 0;
      padding: 15px;
      background: #f8f9fa;
      border-radius: 8px;
    }
    .arrow {
      font-size: 24px;
      color: #42b983;
    }
  </style>
</head>
<body>
  <div class="container">
    <header>
      <h1>Vue 2 与 Vue 3 中 v-model 的源码分析</h1>
      <p class="subtitle">从实现原理到使用方式的全面对比</p>
    </header>

    <div class="comparison">
      <div class="vue2-section">
        <div class="section-header">Vue 2</div>
        <div class="content">
          <div class="example">
            <h3>基础用法</h3>
            <div class="code-block">
              &lt;input v-model="message" placeholder="编辑我"&gt;
              &lt;p&gt;消息是: {{ message }}&lt;/p&gt;
            </div>
            <div class="demo">
              <div id="vue2-app">
                <input v-model="message" placeholder="编辑我">
                <p>消息是: {{ message }}</p>
              </div>
            </div>
          </div>

          <div class="example">
            <h3>组件上的使用</h3>
            <div class="code-block">
              &lt;custom-input v-model="searchText"&gt;&lt;/custom-input&gt;
              &lt;p&gt;搜索内容: {{ searchText }}&lt;/p&gt;
            </div>
            <div class="demo">
              <div id="vue2-component">
                <custom-input v-model="searchText"></custom-input>
                <p>搜索内容: {{ searchText }}</p>
              </div>
            </div>
          </div>

          <div class="example">
            <h3>修饰符</h3>
            <div class="code-block">
              &lt;input v-model.trim="trimmedText" placeholder="输入前后有空格"&gt;
              &lt;p&gt;修剪后: "{{ trimmedText }}"&lt;/p&gt;
            </div>
            <div class="demo">
              <div id="vue2-modifiers">
                <input v-model.trim="trimmedText" placeholder="输入前后有空格">
                <p>修剪后: "{{ trimmedText }}"</p>
              </div>
            </div>
          </div>
        </div>
      </div>

      <div class="vue3-section">
        <div class="section-header">Vue 3</div>
        <div class="content">
          <div class="example">
            <h3>基础用法</h3>
            <div class="code-block">
              &lt;input v-model="message" placeholder="编辑我"&gt;
              &lt;p&gt;消息是: {{ message }}&lt;/p&gt;
            </div>
            <div class="demo">
              <div id="vue3-app">
                <input v-model="message" placeholder="编辑我">
                <p>消息是: {{ message }}</p>
              </div>
            </div>
          </div>

          <div class="example">
            <h3>组件上的使用</h3>
            <div class="code-block">
              &lt;custom-input v-model="searchText"&gt;&lt;/custom-input&gt;
              &lt;p&gt;搜索内容: {{ searchText }}&lt;/p&gt;
            </div>
            <div class="demo">
              <div id="vue3-component">
                <custom-input v-model="searchText"></custom-input>
                <p>搜索内容: {{ searchText }}</p>
              </div>
            </div>
          </div>

          <div class="example">
            <h3>多个 v-model</h3>
            <div class="code-block">
              &lt;user-name
                v-model:first-name="firstName"
                v-model:last-name="lastName"
              &gt;&lt;/user-name&gt;
              &lt;p&gt;全名: {{ firstName }} {{ lastName }}&lt;/p&gt;
            </div>
            <div class="demo">
              <div id="vue3-multiple">
                <user-name
                  v-model:first-name="firstName"
                  v-model:last-name="lastName"
                ></user-name>
                <p>全名: {{ firstName }} {{ lastName }}</p>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <div class="source-analysis">
      <h3>源码实现解析</h3>
      
      <div class="example">
        <h4>Vue 2 v-model 实现</h4>
        <div class="code-block">
// 简化的Vue 2 v-model实现
function transformModel(options, data) {
  const prop = (options.model && options.model.prop) || 'value';
  const event = (options.model && options.model.event) || 'input';
  
  // 在data.props中添加prop
  (data.props || (data.props = {}))[prop] = data.model.value;
  
  // 在data.on中添加事件监听
  (data.on || (data.on = {}))[event] = function(value) {
    this.$emit(event, value);
  };
}
        </div>
        <p>Vue 2 的 v-model 在编译阶段被转换为 value prop 和 input 事件。</p>
      </div>

      <div class="example">
        <h4>Vue 3 v-model 实现</h4>
        <div class="code-block">
// 简化的Vue 3 v-model实现
export const vModelText = {
  created(el, { value, modifiers: { lazy, trim, number } }, vnode) {
    // 获取数据设置函数
    el._assign = getModelAssigner(vnode);
    
    // 添加事件监听
    const event = lazy ? 'change' : 'input';
    el.addEventListener(event, e => {
      let value = e.target.value;
      if (trim) {
        value = value.trim();
      }
      if (number) {
        value = toNumber(value);
      }
      el._assign(value);
    });
    
    // 初始化值
    if (value != null) {
      el.value = value;
    }
  },
  
  beforeUpdate(el, { value, oldValue }) {
    if (value !== oldValue) {
      el.value = value;
    }
  }
};
        </div>
        <p>Vue 3 的 v-model 使用指令实现,支持更多内置功能,并且默认使用 modelValue prop 和 update:modelValue 事件。</p>
      </div>

      <div class="data-flow">
        <div>数据</div>
        <div class="arrow">⇩</div>
        <div>v-model 指令</div>
        <div class="arrow">⇩</div>
        <div>UI 组件</div>
        <div class="arrow">⇩</div>
        <div>用户输入</div>
        <div class="arrow">⇩</div>
        <div>更新数据</div>
      </div>
    </div>

    <div class="conclusion">
      <h3>总结</h3>
      <p>Vue 2 的 v-model 主要是语法糖,将 value prop 和 input 事件绑定在一起。</p>
      <p>Vue 3 的 v-model 进行了重大改进,支持多个 v-model 绑定,默认使用 modelValue 和 update:modelValue,</p>
      <p>并且通过指令实现提供了更好的灵活性和功能扩展性。</p>
    </div>
  </div>

  <!-- Vue 2 实现 -->
  <script>
    // Vue 2 基础示例
    new Vue({
      el: '#vue2-app',
      data: {
        message: 'Hello Vue 2!'
      }
    });

    // Vue 2 自定义组件
    Vue.component('custom-input', {
      props: ['value'],
      template: `
        <div>
          <input 
            :value="value"
            @input="$emit('input', $event.target.value)"
            placeholder="输入搜索内容"
          >
        </div>
      `
    });
    
    new Vue({
      el: '#vue2-component',
      data: {
        searchText: 'Vue 2 搜索'
      }
    });

    // Vue 2 修饰符示例
    new Vue({
      el: '#vue2-modifiers',
      data: {
        trimmedText: ''
      }
    });
  </script>

  <!-- Vue 3 实现 -->
  <script>
    // Vue 3 基础示例
    Vue.createApp({
      data() {
        return {
          message: 'Hello Vue 3!'
        }
      }
    }).mount('#vue3-app');

    // Vue 3 自定义组件
    const app = Vue.createApp({});
    
    app.component('custom-input', {
      props: ['modelValue'],
      emits: ['update:modelValue'],
      template: `
        <div>
          <input 
            :value="modelValue"
            @input="$emit('update:modelValue', $event.target.value)"
            placeholder="输入搜索内容"
          >
        </div>
      `
    });
    
    app.mount('#vue3-component');

    // Vue 3 多个v-model示例
    const multipleApp = Vue.createApp({
      data() {
        return {
          firstName: '张',
          lastName: '三'
        }
      }
    });
    
    multipleApp.component('user-name', {
      props: {
        firstName: String,
        lastName: String
      },
      emits: ['update:firstName', 'update:lastName'],
      template: `
        <div>
          <input 
            :value="firstName"
            @input="$emit('update:firstName', $event.target.value)"
            placeholder="姓"
          >
          <input 
            :value="lastName"
            @input="$emit('update:lastName', $event.target.value)"
            placeholder="名"
          >
        </div>
      `
    });
    
    multipleApp.mount('#vue3-multiple');
  </script>
</body>
</html>

关键特性说明

  1. Vue 2 v-model:

    • 基于value属性和input事件

    • 在组件上使用需要手动实现value prop和input事件

    • 通过编译阶段转换实现

  2. Vue 3 v-model:

    • 基于modelValue属性和update:modelValue事件

    • 支持多个v-model绑定

    • 通过指令实现,提供更好的灵活性和扩展性

    • 内置.trim、.number等修饰符处理

  3. 源码分析:

    • 展示了简化版的Vue 2和Vue 3 v-model实现

    • Vue 2通过编译时转换,Vue 3通过指令实现

    • 可视化数据流展示了v-model的工作原理

结论

Vue 3 的 v-model 是对 Vue 2 版本的一次全面升级。它通过引入 modelValue/update:modelValue 的默认约定、支持多个 v-model 绑定以及提供更强大的自定义修饰符功能,解决了 Vue 2 中的主要痛点,让 API 变得更加一致、灵活和强大。虽然带来了一些 breaking changes,但迁移路径清晰,且最终能带来更简洁、更可维护的代码。

更多推荐