原理

vue2.x的双向数据绑定是通过数据劫持结合发布者订阅者模式的方式来实现。通过object.defineProperty来劫持各个属性的setter,getter,在数据变化时发布消息给订阅者,触发相应的监听回调来渲染视图。保持数据和视图同步。

Vue3.0中的响应式采用了ES6中的Proxy方法。Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

Target。使用Proxy包装的对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)

Handler。函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理p 的行为

解决的痛点:

关于对象: Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对property执行 getter/setter转化,所以property必须在 data对象上存在才能让 Vue 将它转换为响应式的。

关于数组: Vue 不能检测以下数组的变动:

利用索引直接设置一个数组项时。例如:vm.items[indexOfItem] = newValue

修改数组的长度时。例如:vm.items.length = newLength

相比于vue2.x,使用proxy的优势

  • defineProperty只能监听某个属性,不能对全对象监听
  • 可以省去for in、闭包等内容来提升效率(直接绑定整个对象即可)
  • 可以监听数组和对象,不用再去单独的对数组做特异性操作
  • vue3.x可以检测到数组内部数据的变化

V-Model原理

在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:

<input
  :value="text"
  @input="event => text = event.target.value">

v-model 指令帮我们简化了这一步骤:

<input v-model="text">

另外,v-model 还可以用于各种不同类型的输入,<textarea><select> 元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:

  • 文本类型的 <input><textarea> 元素会绑定 value property 并侦听 input 事件;
  • <input type="checkbox"><input type="radio"> 会绑定 checked property 并侦听 change 事件;
  • <select> 会绑定 value property 并侦听 change 事件:

v-model 会忽略任何表单元素上初始的 valuecheckedselected attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用响应式系统的 API 来声明该初始值。

多个 v-model

利用刚才在 v-model小节中学到的技巧,我们可以在一个组件上创建多个 v-model 双向绑定,每一个 v-model 都会同步不同的 prop:

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
<script setup>
defineProps({
  firstName: String,
  lastName: String
})

defineEmits(['update:firstName', 'update:lastName'])
</script>

<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>

处理 v-model

在学习输入绑定时,我们知道了 v-model 有一些内置的修饰符,例如 .trim.number.lazy。在某些场景下,你可能想要一个自定义组件的 v-model 支持自定义的修饰符。

我们来创建一个自定义的修饰符 capitalize,它会自动将 v-model 绑定输入的字符串值第一个字母转为大写:

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

组件的 v-model 上所添加的修饰符,可以通过 modelModifiers prop 在组件内访问到。在下面的组件中,我们声明了 modelModifiers 这个 prop,它的默认值是一个空对象:

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

defineEmits(['update:modelValue'])

console.log(props.modelModifiers) // { capitalize: true }
</script>

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

注意这里组件的 modelModifiers prop 包含了 capitalize 且其值为 true,因为它在模板中的 v-model 绑定上被使用了。

有了 modelModifiers 这个 prop,我们就可以在原生事件侦听函数中检查它的值,然后决定触发的自定义事件中要向父组件传递什么值。在下面的代码里,我们就是在每次 `` 元素触发 input 事件时将值的首字母大写:

<script setup>
const props = defineProps({
  modelValue: String,
  modelModifiers: { default: () => ({}) }
})

const emit = defineEmits(['update:modelValue'])

function emitValue(e) {
  let value = e.target.value
  if (props.modelModifiers.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

<template>
  <input type="text" :value="modelValue" @input="emitValue" />
</template>

对于又有参数又有修饰符的 v-model 绑定,生成的 prop 名将是 arg + "Modifiers"。举例来说:

<MyComponent v-model:title.capitalize="myText">

相应的声明应该是:

const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])

console.log(props.titleModifiers) // { capitalize: true }