The bidirectional binding of Vue is that data changes can cause interface changes, and interface data changes can also drive data changes.

This feature is actually different from the one-way data flow specification, so it was very attractive to me when I first started to contact Vue. We found that Element UI forms also make heavy use of v-Model bidirectional binding.

Bidirectional binding is not supported by all elements/components. Vue currently supports input, SELECT, checkbox, radio, and components for bidirectional binding using v-model directives.

I had a big problem with bidirectional binding: why doesn’t bidirectional binding cause an update loop? Namely interface change -> data change -> interface change -> data change ->…

v-modelBidirectional binding of form elements

Since different form elements use different internal directives, we’ll use input as an example. The bidirectional binding principle for other form elements is very similar.

This section deals with instructions and event handling. If it is not clear, I suggest you refer to my previous two articles, otherwise you may have some confusion.

Case analysis

<input v-model="value" />
<div>{{ value }}</div>

setup() {
  let value = ref("");
  return {
    value
  };
}
Copy the code

A few simple lines of code implement bidirectional binding between the input form element and the data value.

The code analysis

Let’s look at the render function

import { vModelText as _vModelText, createElementVNode as _createElementVNode, withDirectives as _withDirectives, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock(_Fragment, null, [ _withDirectives(_createElementVNode("input", { "onUpdate:modelValue": $event => ((_ctx.value) = $event) }, null, 8 /* PROPS */, ["onUpdate:modelValue"]), [ [_vModelText, _ctx.value] ]), _createElementVNode("div", null, _toDisplayString(_ctx.value), 1 /* TEXT */), _createTextVNode(" setup() { let value = ref(""); return { value };  }") ], 64 /* STABLE_FRAGMENT */)) } // Check the console for the ASTCopy the code

We analyze the _withDirectives function and see that the INPUT generated VNode uses the _vModelText internal directive and adds a pro function for event handling called onUpdate:modelValue, The onUpdate:modelValue function is used to modify the value;

vModelTextinstruction

export const vModelText: ModelDirective< HTMLInputElement | HTMLTextAreaElement > = { created(el, { modifiers: {lazy, trim, number}}, vnode) {// Get vnode.props! [' onUpdate: modelValue] el. The corresponding function _assign = getModelAssigner (vnode) const castToNumber = numbr | | (vnode. Props && Vnode.props. Type === 'number') // If there is a lazy modifier on the input change event, otherwise on the input event addEventListener(el, lazy? 'change' : 'input', e => { let domValue: String | number = el. Value the if (trim) {/ / if there is a trim modifier, DomValue = domValue.trim()} else if (castToNumber) {// If there is a number modifier or if the input type is number, DomValue = toNumber(domValue)} el._assign(domValue)})} beforeUpdate(el, { value, modifiers: {lazy, trim, number}}, vnode) {// Update the function 'onUpdate:modelValue' because it may not update the data, El. _assign = getModelAssigner(vnode) 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 // Update value if (el.value! == newValue) { el.value = newValue } } }Copy the code
  • createdHook function, if anylazyThe modifier,inputA form to monitorchangeEvent, otherwise listeninputEvents;
  • beforeUpdateHook function, to retrieveonUpdate:modelValueFunction, because the re-render function may change the function, and re-giveinputThe assignment;
  • inputEnter the new content in, if anytrimModifier is carried out to remove Spaces, if anynumberModifiers orinputType isnumberThe type needs to be converted tonumberAnd then throughonUpdate:modelValueCorresponding function modificationvalueValue.

Conclusion:

  1. Data ->DOM: reactive datavalueChanges trigger component updates,inputContent will find changes;
  2. DOM – > data:vModelTextThe directive implements a pair ofinputthevalueChange of monitor, according tovModelTextThe modifier of the instruction is finishedinputthevalueValue, and then passonUpdate:modelValueCorresponding function$event => (value = $event)To complete reactive data againvalueModifications. Changes to reactive data trigger component updates.

Think about it

Why wouldn’t there be an update loop

Input input data -> data processing -> Call onUpdate:modelValue corresponding $event => (inputValue = $event) method -> Reactive data change component update -> INPUT Settings update input.value = The newValue update stops at this point.

Why updateinputThe new value of thevNodelTextThe directivebeforeUpdateTo perform?

Directives are updated in two ways: beforeUpdate and updated. Executing in beforeUpdate has two advantages:

  1. Update the DOM before updating itinputNew value of, if only modifiedinputThe value of this is eliminatedpatchPropPart of the operation has been improvedpatchPerformance.
  2. The directivebeforeUpdateIs before DOM update, andupdatedThe hook function is updated in the DOMAsynchronous executionIf there are too many synchronization tasks in complex services, update delay or lag may occur.

v-modelBidirectional binding of components

<Son v-model="modalValue"/>
Copy the code

It’s the same thing

<Son :modalValue="modalValue" @update:modalValue="modalUpdate=$event.target.value"/>
Copy the code

The v-model bidirectional binding of components is essentially a syntax sugar, passing data to child components through prop, which can modify data through v-ON time binding

If you want to customize the Model parameters

<Son v-model:visible="visible"/>
setup(props, ctx){
    ctx.emit("update:visible", false)
}
Copy the code