J-DuYa / DY-Book

迁移知识点
2 stars 1 forks source link

v-model 指令解析 #22

Open J-DuYa opened 9 months ago

J-DuYa commented 9 months ago

Vue 的响应式原理并不是双向绑定。而真实响应式原理是一种单向的行为,它是数据到 DOM 的映射。而真正的双向绑定,除了数据变化会引起 DOM 的变化之外,还应该在操作 DOM 改变之后,反过来也会影响数据的变化。

v-model 指令就是一种双向绑定的实现。v-model 指令常用于表单元素上,当表单元素输入发生改变之后,会通知数据模型更新。反之,数据模型发生改变之后,也会同步的显示到界面上。

v-model 里面的实现:created、mounted 和 beforeUpdate 三个钩子函数

源码实现目录:runtime-dom/src/directives/vModel.ts

修饰符

v-model 的修饰符有 modifiers:lazy、trim、number

// 48 行
created(el, { modifiers: { lazy, trim, number } }, vnode)

lazy

lazy 修饰符,侦听的是 input 的 change 事件,它不会再 input 输入框实施输入的时候触发,而会在 input 元素值改变且失去焦点的时候触发。如果不设置 lazy,侦听的就是 input 的 input 事件,它会在用户实时输入的时候触发。此外,还会多侦听 compositionstart 和 compositioned 事件(非 lazy 态)。

compositionstart 设置 e.target.composing = true compositionend 设置 e.target.composing = false 同时触发 input 事件

// 52 行
addEventListener(el, lazy ? 'change' : 'input', e => {
  if ((e.target as any).composing) return
  let domValue: string | number = el.value
  if (trim) {
    domValue = domValue.trim()
  }
  if (castToNumber) {
    domValue = looseToNumber(domValue)
  }
  el[assignKey](domValue)
})
// 68 行
if (!lazy) {
  addEventListener(el, 'compositionstart', onCompositionStart)
  addEventListener(el, 'compositionend', onCompositionEnd)
  // Safari < 10.2 & UIWebView doesn't fire compositionend when
  // switching focus before confirming composition choice
  // this also fixes the issue where some browsers e.g. iOS Chrome
  // fires "change" instead of "input" on autocomplete.
  addEventListener(el, 'change', onCompositionEnd)
}

trim

去除输入的首尾。change 事件和 input 事件都侦听。

addEventListener(el, lazy ? 'change' : 'input', e => {
  if ((e.target as any).composing) return
  let domValue: string | number = el.value
  if (trim) {
    domValue = domValue.trim()
  }
  if (castToNumber) {
    domValue = looseToNumber(domValue)
  }
  el[assignKey](domValue)
})
if (trim) {
  addEventListener(el, 'change', () => {
    el.value = el.value.trim()
  })
}

number

会将 DOM 的值转为 number 类型后再赋值给数据。

  const castToNumber =
    number || (vnode.props && vnode.props.type === 'number')

// ...

if (castToNumber) {
  domValue = looseToNumber(domValue)
}

// shared/src/general.ts
export const looseToNumber = (val: any): any => {
  const n = parseFloat(val)
  return isNaN(n) ? val : n
}

mounted 钩子

// 79 行
mounted(el, { value }) {
  el.value = value == null ? '' : value
},

总结

v-model 指令才是阵子意义上的双向绑定,因为数据的流动是双向的,它可以作用于原生的表单元素,也可以作用于自定义指令。

当 v-model 作用于表单元素时,它会借助指令的钩子函数在元素挂载之后把数据赋值给表单元素,在数据变化完成之后,在更新之前,把数据再次赋值给表单元素,这就是数据变化引起 DOM 变化的过程。

此外,表单元素会侦听相关事件,比如 input 标签会侦听 change 或者 input 事件,在事件回调函数中修改数据的值,这就是 DOM 变化引起数据变化的过程。