3

What is two-way binding?

Without further ado, let's take a look at a v-model basic example:

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

First of all, we need to understand that: The essence of v-model is an instruction . Therefore, it is the same as our general custom instruction and needs to implement the hook function of the Vue.js life cycle.

Secondly, v-model realizes two-way binding, that is: one-way flow of data to DOM, and one-way flow of DOM to data .

Understand the above two points, and then look at the code will be much clearer.

 // packages/runtime-dom/src/directives/vModel.ts

export const vModelText: ModelDirective<
  HTMLInputElement | HTMLTextAreaElement
> = {
  created() {},
  mounted() {},
  beforeUpdate() {}
}

Open the source code of v-model and we can see that it implements the corresponding Vue.js life cycle hook function, which is actually a built-in custom instruction.

Then, v-model how to realize two-way binding? Specifically, how the one-way flow of data to the DOM and the one-way flow of data from the DOM to the data is achieved.

One-way flow of data to the DOM

 // packages/runtime-dom/src/directives/vModel.ts

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

The one-way flow of data to the DOM is very simple, just one line of code is done, that is, assign the value bound by ---005c10bde5db5d41b0557b058cfabfd8 v-model to el.value .

One-way flow of DOM to data

 // packages/runtime-dom/src/directives/vModel.ts

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

The code is a bit more, but the principle is simple:

  • Listen for the input element's input or change event by custom listener event addEventListener
  • When the user manually enters data, the corresponding function is executed, and the new value of input 77b4ba89d1d41ddfa7b4305d579d0b16--- is obtained through el.value
  • Call el._assign ( onUpdate:modelValue property corresponding function) method v-model binding value

The key to realizing the one-way flow from DOM to data is onUpdate:modelValue . With the help of Vue 3 Template Explorer , we can view the render function generated after compilation, and we can find that there is nothing magical about what it does, it is to help us automatically update v-model on The value of the bound variable.

 <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]
  ])
}

In addition, there are the processing of --- lazy , the processing of trim , the processing of numbers, and the problem that the text is cleared when inputting.

About the functions of the two methods onCompositionStart and onCompositionEnd , see text added with IME to input that has v-model is gone when the view is updated #2302 .

One sentence summary: One-way flow from DOM to data is achieved by using addEventListener .

Finally, the implementation of beforeUpdate , if the value of the data is inconsistent with the value of the DOM, the data is updated to the DOM:

 // packages/runtime-dom/src/directives/vModel.ts

beforeUpdate(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
    }
  }

The above is the text type input element two-way binding principle, of course input element types are not only this, but also such as radio checkbox and other types, if you are interested, you can see it yourself, but the principle is the same, that is, to achieve two functions: one-way flow of data to DOM, and one-way flow of DOM to data .


Seng
121 声望552 粉丝

主业:Web 前端开发,兴趣爱好:读书、爬山