laitimes

Interviewer: What is the difference between using a v-model on a native input and using a component?

author:Not bald programmer
Interviewer: What is the difference between using a v-model on a native input and using a component?

Preface

  • 面试官:vue3的v-model都用过吧,来讲讲。
  • Fans: v-model is actually a syntactic sugar, and at compile time v-model will be compiled into the :modelValue property and the @update:modelValue event. Generally, a props named modelValue is defined in the child component to receive the value passed by the parent v-model, and then when the value of the child component form changes, the @update:modelValue is used to throw an event to the parent component, and the parent component will update the variables bound to the v-model.
  • Interviewer: What you said is that you use v-model on components, and v-model is also supported on native inputs, what is the difference between using v-model on native inputs and using v-model on components?
  • Fan: Ah, aren't the two the same? both: modelValue property and the syntactic sugar of the @update:modelValue event.
  • Interviewer: The native input tag receives the value attribute and listens for the input or change event. In the same way, you say that v-model will compile to the @update:modelValue event, but the input tag only listens to the input or change event, then you pass the @update of listening: How is the modelValue event triggered?

Let's start with the answer

Let's take a look at my flowchart, as follows:

Interviewer: What is the difference between using a v-model on a native input and using a component?

According to the above flowchart, we know that there are three main differences between using v-model on components and using v-model on native inputs:

  • The v-model on the component will generate the modelValue property and the @update:modelValue event after compilation. However, after compiling with v-model on the native input, the modelValue property will not be generated, but only the onUpdate:modelValue callback function and the vModelText custom directive will be generated.
  • To use a v-model on a component, a props named modelValue are defined in the child component to receive the variables that the parent component binds using the v-model, and then use this modelValue to bind to the form of the child component. When using v-model on the native input, the vModelText custom directive generated after compilation updates the variable value bound by the v-model to the value attribute of the native input input box in the mounted and beforeUpdate hook functions, so as to ensure that the value of the variable bound by the v-model is consistent with the value in the input input box.
  • When using v-model on a component, the child component uses emit to throw a @update:modelValue event and update the v-model bound variables in the event handler @update:modelValue. The v-model is used on top of the native input, which is a custom directive of vModelText generated after compilation, which listens for the input or change event of the native input tag in the created hook function. Manually call the onUpdate:modelValue callback function in the event callback function, and then update the v-model bound variables in the callback function.

Let's look at an example

Here's a demo I wrote, and the code is as follows:

<template>
  <input v-model="msg" />
  <p>input value is: {{ msg }}</p>
</template>

<script setup lang="ts">
import { ref } from "vue";

const msg = ref();
</script>           

The above example is simple, using v-model to bind the msg variable on top of the native input tag. Let's take a look at what the compiled js code looks like, so the question is, how to find the compiled js code?

In fact, it's very simple to find your vue file directly on the network, for example, the file I have here is index.vue, then I only need to find the file called index.vue on the network. However, it should be noted that there are two index.vue js requests on the network, which are the js file compiled by the template module + script module, and the js file compiled by the style module.

So how do you distinguish between these two index.vue files? The URL of the js file compiled by the style module contains the query type=style, as shown in the following figure:

Interviewer: What is the difference between using a v-model on a native input and using a component?

Next, let's take a look at the compiled index.vue, and the simplified code is as follows:

import {
  Fragment as _Fragment,
  createElementBlock as _createElementBlock,
  createElementVNode as _createElementVNode,
  defineComponent as _defineComponent,
  openBlock as _openBlock,
  toDisplayString as _toDisplayString,
  vModelText as _vModelText,
  withDirectives as _withDirectives,
  ref,
} from "/node_modules/.vite/deps/vue.js?v=23bfe016";

const _sfc_main = _defineComponent({
  __name: "index",
  setup(__props, { expose: __expose }) {
    __expose();
    const msg = ref();
    const __returned__ = { msg };
    return __returned__;
  },
});

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock(
      _Fragment,
      null,
      [
        _withDirectives(
          _createElementVNode(
            "input",
            {
              "onUpdate:modelValue":
                _cache[0] || (_cache[0] = ($event) => ($setup.msg = $event)),
            },
            null,
            512
          ),
          [[_vModelText, $setup.msg]]
        ),
        _createElementVNode(
          "p",
          null,
          "input value is: " + _toDisplayString($setup.msg),
          1
        ),
      ],
      64
    )
  );
}
_sfc_main.render = _sfc_render;
export default _sfc_main;           

From the above code, we can see that the compiled js code is mainly divided into two parts.

The first block is the _sfc_main component object, which contains the name attribute and the setup method. A Vue component is actually an object, and the _sfc_main object here is a Vue component object.

Let's move on to the second _sfc_render, which I think you should have guessed from the name that it's a render function. Executing this _sfc_render function will generate a virtual DOM, which will then be used by the virtual DOM to generate the real DOM on top of the browser. Let's take a look at the render function.

render function

The render function is preceded by the openBlock function and the createElementBlock function. Its role is to extract as much critical information as possible at compile time, which can reduce the performance overhead of comparing the old and new virtual DOMs at runtime. We won't focus on this in this article, so I won't go into details.

Let's take a look at the array inside, there are two items in the array. The withDirectives function and the createElementVNode function are the input tag and the p tag in the template, respectively. We're going to focus on the input tag, which is the withDirectives function.

withDirectives函数

Does this withDirectives sound familiar? It is an advanced API provided by vue, and we usually don't use it when writing business. The purpose is to add custom directives to the vnode (virtual DOM).

Receives two parameters, the first parameter is the vnode to which the instruction needs to be added, and the second parameter is a two-dimensional array composed of custom instructions. The first layer of a two-dimensional array represents what custom instructions are, and the second layer represents the instruction name, binding value, parameters, and modifiers. The structure of the second layer is: [Directive, value, argument, modifiers]. If you don't need it, you can omit the tail element of the array.

For example:

import { h, withDirectives } from 'vue'

// 一个自定义指令
const pin = {
  mounted() {
    /* ... */
  },
  updated() {
    /* ... */
  }
}

// <div v-pin:top.animate="200"></div>
const vnode = withDirectives(h('div'), [
  [pin, 200, 'top', { animate: true }]
])           

In the example above, we define a custom pin directive that calls the h function to generate the first argument passed from vnode to withDirectives. The second parameter is a custom instruction array, and we only pass a pin custom instruction here. Let's take a look at [Directive, value, argument, modifiers].

  • The first Directive field: "Command Name" corresponds to the PIN custom directive.
  • The second value field: "Command value" corresponds to 200.
  • The third field, argument field: "Parameter" corresponds to the top parameter.
  • The fourth field, modifiers: "modifiers" corresponds to the animate modifier.

So the withDirectives function above is actually the corresponding <div v-pin:top.animate="200"></div>

createElementVNode函数

I think you might have guessed the name of this function, which is to create a vnode (virtual DOM). This function is similar to the h function provided by vue, and the underlying call is a function called createBaseVNode. The first argument received can be either a string (for native elements) or a Vue component definition. The second argument received is the prop to be passed, and the third argument is the child node.

For example:

createElementVNode("input", {
  value: 12,
})           

In the example above, an input vnode is created with a value of 12 in the input box

Now that we've figured out what the withDirectives function and createElementVNode do, you should be able to understand it when we look back at the code corresponding to the input tag. Here's the code:

_withDirectives(
  _createElementVNode(
    "input",
    {
      "onUpdate:modelValue":
        _cache[0] || (_cache[0] = ($event) => ($setup.msg = $event)),
    },
    null,
    512
  ),
  [[_vModelText, $setup.msg]]
)           

Call the withDirectives function with two arguments. The first parameter is the vnode that calls the createElementVNode function to generate an input. The second argument is an array of incoming custom instructions, and it is clear that there is only one item in the first layer of the two-dimensional array, indicating that only one custom instruction has been passed.

Recall the structure of the second layer of the two-dimensional array: [Directive, value, argument, modifiers], the first field Directive means that a custom directive called vModelText is passed here, and the second field value indicates that the value bound to the vModelText directive is $setup.msg. The $setup.msg here actually points to the ref variable named msg defined in setup.

Let's take a look at the createElementVNode function and create an input vnode. A props property called onUpdate:modelValue is passed in, and the value of the property is a cached callback function.

Why do we need caching? Because every time the page is updated, the render function is executed, and every time the render function is executed, the createElementVNode function is called. If you don't cache, it will generate a callback function of onUpdate:modelValue every time you update the page. The callback function here is also very simple, taking a $event variable. This $event variable is the value entered in the input box, and then the value in the latest input box is synchronized to the msg variable in the setup.

To sum up, add a vModelText custom directive to the vnode of the input tag, and bind the value of the directive to the msg variable. In addition, an onUpdate:modelValue property has been added to the vnode of the input tag, and the value of the property is a callback function, which will update the value of the msg variable to the latest value in the input box. We know that the value in the input box corresponds to the value property, and listens to the input and change events. So there are two questions here:

  • How do I pass the value of the msg variable bound to the vModelText custom directive to the value attribute in the input input box?
  • input标签监听input和change事件,编译后input上面却是一个名为onUpdate:modelValue的props回调函数?

To answer the two questions above, we need to look at what the vModelText custom directive looks like.

vModelText custom directive

vModelText is a runtime v-model instruction, why is it runtime? At compile time, the v-model instruction on the component will be compiled into the modelValue attribute and the @update:modelValue event. Therefore, there are no v-model instructions on the component at runtime, only the native input still has v-model instructions at runtime, that is, vModelText custom instructions.

Let's take a look at the code for the vModelText custom directive:

const vModelText = {
  created(el, { modifiers: { lazy, trim, number } }, vnode) {
    // ...
  },
  mounted(el, { value }) {
    // ...
  },
  beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
    // ...
  },
}           

As you can see from the above, there are three hook functions used in the vModelText custom directive: created, mounted, and beforeUpdate, let's take a look at the parameters used in the above three hook functions:

  • el: The element to which the directive is bound. This can be used to manipulate the DOM directly.
  • binding: An object that contains the following properties. In the example above, the binding object is directly deconstructed.
    • lazy: By default, the v-model updates the data after each input event. You can add the lazy modifier to update the data after each change event, and then update the data when the focus is out of focus in the input box.
    • trim: Removes spaces at both ends of user input.
    • number: Let user input be automatically converted to a number.
    • value:传递给指令的值。 例如在 v-model="msg" 中,其中msg变量的值为“hello word”,value的值就是“hello word”。
    • modifiers:一个包含修饰符的对象,v-model支持lazy, trim, number这三个修饰符。
  • vnode:绑定元素的 VNode(虚拟DOM)。

mounted hook function

Let's start with the mounted hook function, which looks like this:

const vModelText = {
  mounted(el, { value }) {
    el.value = value == null ? "" : value;
  },
}           

The code in Mounted is very simple, if the value of the msg variable bound to the v-model is not empty when mounted, then synchronize the value of the msg variable to the input input box.

created hook function

Let's take a look at the code in the created hook function, as follows:

const assignKey = Symbol("_assign");
const vModelText = {
  created(el, { modifiers: { lazy, trim, number } }, vnode) {
    el[assignKey] = getModelAssigner(vnode);
    const castToNumber =
      number || (vnode.props && vnode.props.type === "number");
    addEventListener(el, lazy ? "change" : "input", (e) => {
      if (e.target.composing) return;
      let domValue = 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();
      });
    }
    if (!lazy) {
      addEventListener(el, "compositionstart", onCompositionStart);
      addEventListener(el, "compositionend", onCompositionEnd);
    }
  },
}           

The code in the created hook function is divided into five main parts.

Part I

First, let's look at the first part of the code:

el[assignKey] = getModelAssigner(vnode);           

Let's start with the getModelAssigner function. Here's the code:

const getModelAssigner = (vnode) => {
const fn = vnode.props["onUpdate:modelValue"];
return isArray(fn) ? (value) => invokeArrayFns(fn, value) : fn;
};           

The code of the getModelAssigner function is very simple, it returns a props callback function named onUpdate:modelValue on the vnode. As mentioned earlier, executing this callback function will update the msg variable bound to the v-model synchronously.

Therefore, the function of the first part of the code is to take out the props callback function named onUpdate:modelValue on the input tag, and then assign the value to the assignKey method of the input tag object, and then manually call the input or chang event in the input box when it is triggered. This assignKey is a Symbol, a unique identifier.

Part II

Let's look at the second part of the code:

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

castToNumber indicates whether the .number modifier is used, or whether the input box has a type=number attribute. If the castToNumber value is true, it will be converted to a number when the value of the input field is processed.

Part III

Let's move on to the code for the third part:

addEventListener(el, lazy ? "change" : "input", (e) => {
if (e.target.composing) return;
let domValue = el.value;
if (trim) {
  domValue = domValue.trim();
}
if (castToNumber) {
  domValue = looseToNumber(domValue);
}
el[assignKey](domValue);
});           

Listen to the input input field, if there is a .lazy modifier, it will listen to the change event, otherwise it will listen to the input event. See, isn't this the same as the .lazy modifier. The .lazy modifier is used to update the data every time the change event is triggered. Let's take a look at the event handler and take a look at the first line of code:

if (e.target.composing) return;           

When the user uses the Pinyin input method to input Chinese characters, the input event will also be triggered during the Pinyin input stage. However, in general, we only want to trigger the input to update the data when the Chinese characters are actually synthesized, so the input event triggered during the input pinyin stage needs to be returned. We'll talk about when e.target.composing is set to true and when it's false.

The rest of the code is very simple, assign the value in the input box, that is, el.value, to the domValue variable. If the .trim modifier is used, run the trim method to remove the spaces at both ends of the domValue variable.

如果castToNumber的值为true,表示使用了.number𱽎饰符或者在input上面使用了type=number。 调用looseToNumber方法将domValue字符串转换为数字。

Finally, the processed domValue, that is, the input value in the processed input box, is called as a parameter to the el[assignKey] method. As we mentioned earlier, el[assignKey] is stored in the props callback function named onUpdate:modelValue on the input tag, and executing the el[assignKey] method is to execute the callback function, in which the value of the msg variable bound to the v-model will be updated to the input value in the processed input box.

Now you know why the input tag listens to the input and change events, but after compilation, the input has a props callback function called onUpdate:modelValue?

Because in the callback of the input or change event, the value of the input box will be processed according to the passed modifier, and then the value of the processed input box will be used as a parameter to manually call the onUpdate:modelValue callback function to update the bound msg variable in the callback function.

Part IV

Let's move on to the code for the fourth part, which is as follows:

if (trim) {
  addEventListener(el, "change", () => {
    el.value = el.value.trim();
  });
}           

This part of the code is very simple, if the .trim modifier is used, the change event is triggered, and the focus is lost in the input field. The value in the input box will also be trimmed, removing the spaces before and after.

Why do we need this code, isn't the value in the input box already trimmed in the input or change event? And the beforeUpdate hook function also executes el.value = newValue to update the value in the input box to the value of the msg variable bound by the v-model.

The answer is: I did trim the value obtained in the input box, and then update the trim value to the msg variable bound by the v-model. However, we don't update the value in the input box to the trim process, although the value in the input box will be updated to the msg variable bound by the v-model in the beforeUpdate hook function. However, if you only enter a space before and after the input box, then the value in the input box and the value of the msg variable will be considered equal in the beforeUpdate hook function after trim processing. el.value = newValue will not be executed, and the value in the input box will still have spaces, so you need to execute the code in the fourth part to replace the value in the input box with the trim value.

Part V

Let's move on to the code for the fifth part, which is as follows:

if (!lazy) {
  addEventListener(el, "compositionstart", onCompositionStart);
  addEventListener(el, "compositionend", onCompositionEnd);
}           

If the .lazy modifier is not used, the bound variable is updated every time it is input.

The compositionstart event listened to here is that the text compositing system will fire the compositionstart event when it starts a new input compositing. For example, this event is triggered when the user starts typing Chinese characters using the Pinyin input method.

The compositionend event listened to here is that the compositionend event will be triggered when the composition of a text paragraph is completed or canceled. For example, this event will be triggered when the user uses the Pinyin input method to synthesize the input Pinyin into Chinese characters.

Let's take a look at the code in onCompositionStart, as follows:

function onCompositionStart(e) {
  e.target.composing = true;
}           

The code is simple, set e.target.composing to true. Remember that we will first judge this e.target.composing in the input or change event in the input input box, and if it is true, then return it, so that the msg variable bound to the v-model will not be updated when entering pinyin.

Let's take a look at the code in onCompositionEnd, as follows:

function onCompositionEnd(e) {
  const target = e.target;
  if (target.composing) {
    target.composing = false;
    target.dispatchEvent(new Event("input"));
  }
}           

When synthesizing pinyin into Chinese characters, e.target.composing will be set to false, why should target.dispatchEvent be called to manually trigger an input event?

The answer is: when the Pinyin is synthesized into Chinese characters, the input event will be triggered before the compositionend event, and since the value of e.target.composing at this time is still true, the subsequent code in the input event will be returned. Therefore, you need to reset e.target.composing to false, and manually trigger an input event to update the msg variable bound to the v-model.

beforeUpdate钩子函数

Let's take a look at the beforeUpdate hook function, which is called every time the page is updated due to a reactive state change, and the code is as follows:

const vModelText = {
  beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
    el[assignKey] = getModelAssigner(vnode);
    // avoid clearing unresolved text. #2302
    if (el.composing) return;

    const elValue =
      number || el.type === "number" ? looseToNumber(el.value) : el.value;
    const newValue = value == null ? "" : value;

    if (elValue === newValue) {
      return;
    }

    if (document.activeElement === el && el.type !== "range") {
      if (lazy) {
        return;
      }
      if (trim && el.value.trim() === newValue) {
        return;
      }
    }

    el.value = newValue;
  },
};           

After reading the previous created function, it is very simple to look at this beforeUpdate function. The final thing to do with the beforeUpdate hook function is this last line of code:

el.value = newValue;           

The meaning of this line of code is to update the value in the input box to the msg variable bound by the v-model, why do you need to execute it in the beforeUpdate hook function?

The answer is that msg is a reactive variable, if the value of the msg variable is changed on the parent component for other reasons, the value in the input input box needs to be updated to the latest msg variable. This explains our previous question: how to pass the value of the msg variable bound to the vModelText custom directive to the value attribute in the input input box?

The first line of code is:

el[assignKey] = getModelAssigner(vnode);           

Here again, the props callback function named onUpdate:modelValue on the vnode is assigned to el[assignKey], hasn't it been assigned once before when created, why is it assigned again?

The answer is that in some scenarios, the onUpdate:modelValue callback function will not be cached, and if there is no cache, then a new onUpdate:modelValue callback function will be generated every time the render function is executed. Therefore, it is necessary to assign the latest onUpdate:modelValue callback function to el[assignKey] every time in the beforeUpdate hook function, and when el[assignKey] is executed when the input or change event is triggered, the latest onUpdate:modelValue callback function is executed.

Let's take a look at the second line of code, which looks like this:

// avoid clearing unresolved text. #2302
if (el.composing) return;           

This line of code is to fix a bug if in the process of entering pinyin, before the Chinese characters are synthesized. If the page is refreshed due to a change in the value of another reactive variable, it should be returned. Otherwise, if el.value = newValue is executed, the input value in the input box will be cleared because the value of the msg variable is still null. For more information, please refer to issue: https://github.com/vuejs/core/issues/2302

The rest of the code is very simple, with the document.activeElement property returning the DOM element that gets the current focus, and the type = "range" that we don't usually use. According to the modifier used, the value in the processed input input box is obtained, and then compared with the msg variable bound to the v-model. If the two are equal, there is no need to execute el.value = newValue to update the value in the input box to the latest value.

summary

Now look at this flowchart and you should be able to understand it easily:

Interviewer: What is the difference between using a v-model on a native input and using a component?

There are three main differences between using v-model on components and using v-model on native inputs:

  • 组件上面的v-model编译后会生成modelValue属性和@update:modelValue事件。 而在原生input上面使用v-model编译后不会生成modelValue属性,只会生成onUpdate:modelValue回调函数和vModelText自定义指令。 (@update:modelValue事件其实等价于onUpdate:modelValue回调函数)
  • To use a v-model on a component, a props named modelValue are defined in the child component to receive the variables that the parent component binds using the v-model, and then use this modelValue to bind to the form of the child component. When using v-model on the native input, the vModelText custom directive generated after compilation updates the variable value bound by the v-model to the value attribute of the native input input box in the mounted and beforeUpdate hook functions, so as to ensure that the value of the variable bound by the v-model is consistent with the value in the input input box.
  • When using v-model on a component, the child component uses emit to throw a @update:modelValue event and update the v-model bound variables in the event handler @update:modelValue. The v-model is used on top of the native input, which is a custom directive of vModelText generated after compilation, which listens for the input or change event of the native input tag in the created hook function. Manually call the onUpdate:modelValue callback function in the event callback function, and then update the v-model bound variables in the callback function.

Read on