preface

V-model is often referred to as two-way binding, but it must not be confused with the data response principle, because the data response is to drive the view rendering through the change of data, and two-way binding in addition to data can drive DOM rendering, DOM changes can also affect data, which is a two-way relationship.

case

The V-model applies to the DOM element

An 🌰 :

let vm = new Vue({
  el: '#app',
  template: '<div>'
  + '<input v-model="message" placeholder="edit me">' +
  '<p>Message is: {{ message }}</p>' +
  '</div>'.data() {
    return {
      message: ' '}}})Copy the code

When we enter a copy in the form, the message changes as well. OK, let’s find out.

Compilation phase

The template is compiled first, in the parse phase, where the V-Model is resolved to the EL. directives as normal, and then executed in the CodeGen phase, when genData is executed

Source: the SRC/compiler/codegen/index. Js

/** * Constructs a data object string based on the attributes of the AST element node, which will be passed as a parameter when vNodes are createdexport function genData (el: ASTElement, state: CodegenState): string {
  const dirs = genDirectives(el, state)
}
Copy the code

Look at the implementation of genDirectives:

/* Generate instruction */function genDirectives (el: ASTElement, state: CodegenState): string | void {
  const dirs = el.directives
  if (!dirs) return
  let res = 'directives:['
  let hasRuntime = false
  let i, l, dir, needRuntime
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i]
    needRuntime = true/* Const gen: DirectiveFunction = state.directives[dir.name]if(gen) {/* Run the gen() command */ needRuntime =!! gen(el, dir, state.warn) } } }Copy the code

The Gendrirectiv method is to walk through the EL. Directives and obtain the corresponding method for each directive. src/platforms/web/compiler/directives/index.js

import model from './model'
import text from './text'
import html from './html'

export default {
  model,
  text,
  html
}
Copy the code

So it is clear that we find the model of the corresponding source address: SRC/platforms/web/compiler directives/model. Js

export default functionmodel ( el: ASTElement, dir: ASTDirective, _warn: Function ): ? boolean { warn = _warn const value = dir.value const modifiers = dir.modifiers const tag = el.tag consttype = el.attrsMap.type

  if(el.component) {/* Set v-Model */ genComponentModel(EL, value, modifiers)return false
  } else if (tag === 'select') {
    genSelect(el, value, modifiers)
  } else if (tag === 'input' && type= = ='checkbox') {
    genCheckboxModel(el, value, modifiers)
  } else if (tag === 'input' && type= = ='radio') {
    genRadioModel(el, value, modifiers)
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers)
  } else if(! config.isReservedTag(tag)) { genComponentModel(el, value, modifiers)return false
  }
  return true
}
Copy the code

NeedRuntime =!! Gen (EL, DIR, state.warn) is the model function that executes different logic depending on the AST element node. For our case, This will hit the logic of genDefaultModel(EL, Value, modiFIERS), and we’ll cover component handling in a moment. Let’s look at the implementation of genDefaultModel:

functiongenDefaultModel ( el: ASTElement, value: string, modifiers: ? ASTModifiers ): ? boolean { consttype= el.attrsMap.type const { lazy, number, trim } = modifiers || {} const needCompositionGuard = ! lazy &&type! = ='range'
  const event = lazy
    ? 'change'
    : type= = ='range'
      ? RANGE_TOKEN
      : 'input'

  let valueExpression = '$event.target.value'
  if (trim) {
    valueExpression = `$event.target.value.trim()`
  }
  if (number) {
    valueExpression = `_n(${valueExpression})`
  }

  let code = genAssignmentCode(value, valueExpression)
  if (needCompositionGuard) {
    code = `if($event.target.composing)return;${code}`
  }
  
  addProp(el, 'value', ` (${value})`)
  addHandler(el, event, code, null, true)
  if (trim || number) {
    addHandler(el, 'blur'.'$forceUpdate()')}}Copy the code

We went through the genDefaultModel function step by step, first dealing with modifiers, whose differences mainly affect the values of event and valueExpression. For our example, event is input, ValueExpression $event. The target value. Then go to executive genAssignmentCode to generate code, it is defined in SRC/compiler directives/model. Js

export function genAssignmentCode (
  value: string,
  assignment: string
): string {
  const res = parseModel(value)
  if (res.key === null) {
    return `${value}=${assignment}`}else {
    return `$set(${res.exp}.${res.key}.${assignment}) `}}Copy the code

${value}=${assignment}; ${assignment}; ${assignment}; ${assignment}

message=$event.target.value
Copy the code

Then we hit needCompositionGuard true,

if (needCompositionGuard) {
    code = `if($event.target.composing)return;${code}`}Copy the code

So the final code is zero

if($event.target.composing)return; message=$event.target.value
Copy the code

After code is generated, two key lines of code are executed:

addProp(el, 'value', ` (${value})`)
addHandler(el, event, code, null, true)
Copy the code

This is actually the essence of input implementing the V-model. By modifying the AST element and adding a prop to EL, we dynamically bind value to input, and add event handling to EL, which is equivalent to binding input events to input. The actual conversion to a template is as follows

<input
  v-bind:value="message"
  v-on:input="message=$event.target.value">
Copy the code

The value of the input is dynamically bound to the messgae variable, and the message is set to the target value when the input event is triggered. This is two-way binding, so the V-model is essentially a syntax sugar.

V-models act on components

Let’s start with an example

let Child = {
  template: '<div>'
  + '<input :value="value" @input="updateValue" placeholder="edit me">' +
  '</div>',
  props: ['value'],
  methods: {
    updateValue(e) {
      this.$emit('input', e.target.value)
    }
  }
}

let vm = new Vue({
  el: '#app',
  template: '<div>' +
  '<child v-model="message"></child>' +
  '<p>Message is: {{ message }}</p>' +
  '</div>'.data() {
    return {
      message: ' '
    }
  },
  components: {
    Child
  }
})
Copy the code

As you can see, the parent component references the child component using the V-Model to associate the data message; The child component defines a value prop and issues an event in the input event callback via this.$emit(‘input’, e.target.value), both of which are necessary for the V-model to work. Then, we will go to the SRC/platforms/web/compiler directives/model. Js

else if(! config.isReservedTag(tag)) { genComponentModel(el, value, modifiers);return false
}
Copy the code

GenComponentModel functions defined in SRC/compiler directives/model. In js:

/* Generate the component's V-model */export functiongenComponentModel ( el: ASTElement, value: string, modifiers: ? ASTModifiers ): ? boolean { const { number, trim } = modifiers || {} const baseValueExpression ='? v'
  let valueExpression = baseValueExpression
  if (trim) {
    valueExpression =
      `(typeof ${baseValueExpression}= = ='string'` +
      `? ${baseValueExpression}.trim()` +
      `: ${baseValueExpression})`
  }
  if (number) {
    valueExpression = `_n(${valueExpression})`
  }
  const assignment = genAssignmentCode(value, valueExpression)

  el.model = {
    value: `(${value})`,
    expression: JSON.stringify(value),
    callback: `function (${baseValueExpression}) {${assignment}} `}}Copy the code

The logic of genComponentModel is simple. For our example, the value of the generated el.model is:

el.model = {
  callback:'function (? v) {message=? v}',
  expression:'"message"',
  value:'(message)'
}
Copy the code

So after Gencache, there is some logic in the genData function as follows:

if(el.model) { data += `model:{value:${ el.model.value },callback:${ el.model.callback },expression:${ el.model.expression `}}},Copy the code

The final render code generated by the parent component looks like this:

with(this){
  return _c('div',[_c('child',{
    model:{
      value:(message),
      callback:function ($$v) {
        message=$$v
      },
      expression:"message"
    }
  }),
  _c('p',[_v("Message is: "+_s(message))])],1)
}
Copy the code

We can see that a Model object is generated, value is our message, and callback assigns the target value to Message. The createComponent function, defined in SRC /core/vdom/create-component.js, is then executed during the creation of the child vnode: SRC /core/vdom/create-component.js

export functioncreateComponent ( Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ? Array<VNode>, tag? : string ): VNode | Array<VNode> | void {if (isDef(data.model)) {
   transformModel(Ctor.options, data)
 }

 const vnode = new VNode(
   `vue-component-${Ctor.cid}${name ? `-${name}` : ' '}`,
   data, undefined, undefined, undefined, context,
   { Ctor, propsData, listeners, tag, children },
   asyncFactory
 )
 
 return vnode
}
Copy the code

TransformModel (Ctor. Options, data) ¶

function transformModel (options, data: any) {
  const prop = (options.model && options.model.prop) || 'value'
  const event = (options.model && options.model.event) || 'input'; (data.props || (data.props = {}))[prop] = data.model.value const on = data.on || (data.on = {})if (isDef(on[event])) {
    on[event] = [data.model.callback].concat(on[event])
  } else {
    on[event] = data.model.callback
  }
}
Copy the code

/ / add data.model.value (); / / add data.model.value (); / / Add data.model.value (); / / Add data.model.value (); And add data.model.callback (the event that triggers the double bind) to data.on, so our example extends as follows:

data.props = {
  value: (message),
}
data.on = {
  input: function ($$v) {
    message=$$v}}Copy the code

We’re writing the parent component like this:

let vm = new Vue({
  el: '#app',
  template: '<div>' +
  '<child :value="message" @input="message=arguments[0]"></child>' +
  '<p>Message is: {{ message }}</p>' +
  '</div>'.data() {
    return {
      message: ' '
    }
  },
  components: {
    Child
  }
})
Copy the code

The value passed by the child component is bound to the parent component’s message and listens for a custom input event. When the child sends an input event, the parent component modifies the value of Message in the event callback and the value changes. The input value of the child component is updated. This is the typical Vue parent-child communication pattern. The parent component passes data to the child component through prop, and the child component notifies the parent component of the changes through $emit events. Therefore, the V-model on the component is also a syntax sugar. In addition, as I mentioned earlier, we notice that the implementation of the component V-Model, the value prop of the child component, and the name of the distributed input event are configurable. We can see the processing of these parts in the transformModel:

function transformModel (options, data: any) {
  const prop = (options.model && options.model.prop) || 'value'
  const event = (options.model && options.model.event) || 'input'
  // ...
}
Copy the code

This means that you can use the Model option to configure the prop name that the child component receives and the event name that the child sends when defining it. For example:

let Child = {
  template: '<div>'
  + '<input :value="msg" @input="updateValue" placeholder="edit me">' +
  '</div>',
  props: ['msg'],
  model: {
    prop: 'msg',
    event: 'change'
  },
  methods: {
    updateValue(e) {
      this.$emit('change', e.target.value)
    }
  }
}

let vm = new Vue({
  el: '#app',
  template: '<div>' +
  '<child v-model="message"></child>' +
  '<p>Message is: {{ message }}</p>' +
  '</div>'.data() {
    return {
      message: ' '
    }
  },
  components: {
    Child
  }
})
Copy the code

The child component changes the name of the prop it receives and the name of the event it sends. However, the parent component as the caller is not concerned with this. The advantage of this is that we can use value as a prop for other purposes.

Finally thanks miss huang’s article: ustbhuangyi. Making. IO/vue – analysi…