什么是双向绑定?

废话不多说,咱们先来看一个 v-model 根本的示例:

<input type="text" v-model="search">

首先,咱们要明确一点的是:v-model 的实质是指令。因而,它跟咱们个别的自定义指令是一样的,须要实现 Vue.js 生命周期的钩子函数。

其次,v-model 实现了双向绑定,也就是:数据到 DOM 的单向流动DOM 到数据的单向流动

明确了下面这两点,再来看代码就清晰多了。

// packages/runtime-dom/src/directives/vModel.tsexport const vModelText: ModelDirective<  HTMLInputElement | HTMLTextAreaElement> = {  created() {},  mounted() {},  beforeUpdate() {}}

关上 v-model 的源码咱们能够看到,它实现了对应的 Vue.js 生命周期钩子函数,实际上它就是一个内置的自定义指令。

那么,v-model 如何实现双向绑定的呢?具体来说,数据到 DOM 的单向流动以及DOM 到数据的单向流动是如何实现的。

数据到 DOM 的单向流动

// packages/runtime-dom/src/directives/vModel.tsexport const vModelText: ModelDirective<  HTMLInputElement | HTMLTextAreaElement> = {  // set value on mounted so it's after min/max for type="range"  mounted(el, { value }) {    el.value = value == null ? '' : value  }}

数据到 DOM 的单向流动实现非常简单,一行代码就搞定了,就是把 v-model 绑定的值赋值给 el.value

DOM 到数据的单向流动

// packages/runtime-dom/src/directives/vModel.tsexport const vModelText: ModelDirective<  HTMLInputElement | HTMLTextAreaElement> = {  created(el, { modifiers: { lazy, trim, number } }, vnode) {    el._assign = getModelAssigner(vnode)        // see: https://github.com/vuejs/core/issues/3813    const castToNumber = number || (vnode.props && vnode.props.type === 'number')        // 实现 lazy 性能    addEventListener(el, lazy ? 'change' : 'input', e => {      // `composing=true` 时不把 DOM 的值赋值给数据      if ((e.target as any).composing) return            let domValue: string | number = el.value      if (trim) {        domValue = domValue.trim()      } else if (castToNumber) {        domValue = toNumber(domValue)      }            // DOM 的值扭转时,同时扭转对应的数据(即扭转 v-model 上绑定的变量的值)      el._assign(domValue)    })        // 实现 trim 性能    if (trim) {      addEventListener(el, 'change', () => {        el.value = el.value.trim()      })    }        // 不配置 lazy 时,监听的是 input 的 input 事件,它会在用户实时输出的时候触发。    // 此外,还会多监听 compositionstart 和 compositionend 事件。    if (!lazy) {        // 这是因为,用户应用拼音输入法开始输出汉字时,这个事件会被触发,        // 此时,设置 `composing=true`,在 input 事件回调里能够进行判断,防止将 DOM 的值赋值给数据,      // 因为此时并未输出实现。      addEventListener(el, 'compositionstart', onCompositionStart)            // 当用户从输入法中确定选中了一些数据实现输出后(如中文输入法常见的按空格确认输出的文字),      // 设置 `composing=false`,在 onCompositionEnd 中手动触发 input 事件,实现数据的赋值。      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)    }  }}function onCompositionStart(e: Event) {  (e.target as any).composing = true}function onCompositionEnd(e: Event) {  const target = e.target as any  if (target.composing) {    target.composing = false    target.dispatchEvent(new Event('input'))  }}const getModelAssigner = (vnode: VNode): AssignerFn => {  const fn = vnode.props!['onUpdate:modelValue']  return isArray(fn) ? value => invokeArrayFns(fn, value) : fn}

代码有点多,但原理很简略:

  • 通过自定义监听事件 addEventListener 来监听 input 元素的 inputchange 事件
  • 当用户手动输出数据时执行对应的函数,并通过 el.value 获取 input 的新值
  • 调用 el._assignonUpdate:modelValue 属性对应的函数)办法 v-model 绑定的值

一句话总结:通过应用 addEventListener 来实现DOM 到数据的单向流动

除此之外,还有对 lazy 的解决、trim 的解决、数字的解决、以及解决正在输出时文本被清空的问题。

对于 onCompositionStartonCompositionEnd 两个办法的作用,详见 text added with IME to input that has v-model is gone when the view is updated #2302。

而所谓的 onUpdate:modelValue 就是一个语法糖,借助 Vue 3 Template Explorer,咱们能够查看其编译后生成的 render 函数,能够发现它做所的事件并没有什么神奇的中央,就是帮咱们自动更新 v-model 上绑定的变量的值。

<input type="text" v-model="search">import { vModelText as _vModelText, withDirectives as _withDirectives, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"export function render(_ctx, _cache, $props, $setup, $data, $options) {  return _withDirectives((_openBlock(), _createElementBlock("input", {    type: "text",        // `onUpdate:modelValue` 所做的事,    // 就是主动帮咱们更新 `v-model` 上绑定的变量的值。    "onUpdate:modelValue": $event => ((_ctx.search) = $event)      }, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [    [_vModelText, _ctx.search]  ])}

最初是 beforeUpdate 的实现,如果数据的值和 DOM 的值不统一,则将数据更新到 DOM:

// packages/runtime-dom/src/directives/vModel.tsbeforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {    el._assign = getModelAssigner(vnode)    // avoid clearing unresolved text. #2302      // 输出某些语言如中文,在没有输出实现时,在更新时会主动将已存在的文本清空,具体可见 issue#2302    if ((el as any).composing) return      if (document.activeElement === el) {      if (lazy) {        return      }      if (trim && el.value.trim() === value) {        return      }      if ((number || el.type === 'number') && toNumber(el.value) === value) {        return      }    }    const newValue = value == null ? '' : value    if (el.value !== newValue) {      el.value = newValue    }  }

以上就是 text 类型的 input 元素双向绑定原理,当然 input 元素类型不止这个,还有诸如 radiocheckbox 等类型,大家有趣味的话能够本人去看,然而原理都是雷同的,就是实现两个性能:数据到 DOM 的单向流动DOM 到数据的单向流动