vue源码解读二十三: v-model揭秘

172 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第10天,点击查看活动详情

本文主要内容摘抄自黄轶老师的慕课网课程Vue.js 源码全方位深入解析 全面深入理解Vue实现原理,主要用于个人学习和复习,不用作其他用途。

很多同学在理解 Vue 的时候都把 Vue 的数据响应原理理解为双向绑定,但实际上这是不准确的,我们之前提到的数据响应,都是通过数据的改变去驱动 DOM 视图的变化,而双向绑定除了数据驱动 DOM 外,DOM 的变化反过来影响数据,是一个双向关系,在 Vue 中,我们可以通过 v-model 来实现双向绑定。

v-model基本使用

原生DOM使用v-model

<input v-model="val">
<input :value="val" @input="val === e.target.value"  >

当改变input框值的时候触发@input事件,然后更新val的值,而val绑定在inputvalue属性上,所以就会改变input框里面的内容,这样形成了双向绑定。

组件使用v-model

<my-component v-model='val' />
<my-component :value='val' @input="val === argument[0]" />

实现原理是把val传递到子组件中,子组件用value来接收。然后子组件this.$emit来触发父组件的input事件来更新val的值,下面来看看组件是实现过程:

<template>
  <input :value="value" @input="updateInput">
</template>

<script>
export default {
    props: {
       value: string
    }
    methods: {
        updateInput() {
            this.$emit('input', e.target.value)
        }
    }
}
</script>

可以看到子组件触发了在父组件绑定的input事件,然后把值传给父组件,这样就改变了父组件的值,同时这个值又通过props传递给子组件,所以子组件的内容也会发生改变,这样就形成了双向绑定。

v-model 即可以作用在普通表单元素上,又可以作用在组件上,它其实是一个语法糖,接下来我们就来分析 v-model 的实现原理。

表单元素

为了更加直观,我们还是结合示例来分析:

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

这是一个非常简单 demo,我们在 input 元素上设置了 v-model 属性,绑定了 message,当我们在 input 上输入了内容,message 也会同步变化。接下来我们就来分析 Vue 是如何实现这一效果的,其实非常简单。

也是先从编译阶段分析,首先是 parse 阶段, v-model 被当做普通的指令解析到 el.directives 中,这个过程在processAttrs函数中来做的:

function processAttrs (el) {
  const list = el.attrsList
  ...
  for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name
    value = list[i].value
    if (dirRE.test(name)) {
      el.hasBindings = true
      ...
      if (bindRE.test(name)) { // v-bind
        ...
      // 针对指令进行解析
      } else {
        name = name.replace(dirRE, '')
        ...
        // 把指令添加到el上
        addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
       ...
      }
    } 
  }
}

function addDirective (...) {
  (el.directives || (el.directives = [])).push(rangeSetItem({
    name,
    rawName,
    value,
    arg,
    isDynamicArg,
    modifiers
  }, range))
  el.plain = false
}

然后在 codegen 阶段,执行generate

function generate (ast,options) {
  const state = new CodegenState(options)
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

export function genElement (el: ASTElement, state: CodegenState): string {
  ...
  if (el.staticRoot && !el.staticProcessed) {
   ...
  } else {
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }

      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'

  // 生成指令的字符串
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','
  ...
  
  return data

执行 genData 的时候,会执行 const dirs = genDirectives(el, state),它的定义在 src/compiler/codegen/index.js 中:

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) {
      needRuntime = !!gen(el, dir, state.warn)
    }
    if (needRuntime) {
      hasRuntime = true
      res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
        dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
      }${
        dir.arg ? `,arg:"${dir.arg}"` : ''
      }${
        dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
      }},`
    }
  }
  if (hasRuntime) {
    return res.slice(0, -1) + ']'
  }
}

genDrirectives 方法就是遍历 el.directives,然后获取每一个指令对应的方法 const gen: DirectiveFunction = state.directives[dir.name],这个指令方法实际上是在实例化 CodegenState 的时候通过 option 传入的,这个 option 就是编译相关的配置,它在不同的平台下配置不同,在 web 环境下的定义在 src/platforms/web/compiler/options.js 下:

export const baseOptions: CompilerOptions = {
  expectHTML: true,
  modules,
  directives,
  isPreTag,
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag,
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}

directives 定义在 src/platforms/web/compiler/directives/index.js 中:

export default {
  model,
  text,
  html
}

那么对于 v-model 而言,对应的 directive 函数是在 src/platforms/web/compiler/directives/model.js 中定义的 model 函数:

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

  if (el.component) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    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)
    // component v-model doesn't need extra runtime
    return false
  } else if (process.env.NODE_ENV !== 'production') {
    
  }
  return true
}

也就是说我们执行 needRuntime = !!gen(el, dir, state.warn) 就是在执行 model 函数,它会根据 AST 元素节点的不同情况去执行不同的逻辑,对于我们这个 case 而言,它会命中 genDefaultModel(el, value, modifiers) 的逻辑,稍后我们也会介绍组件的处理,其它分支同学们可以自行去看。我们来看一下 genDefaultModel 的实现:

function genDefaultModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  const type = el.attrsMap.type
  ...
  // 指令修饰符
  const { lazy, number, trim } = modifiers || {}
  const needCompositionGuard = !lazy && type !== 'range'
  // 事件是input事件
  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})`
  }
  // 用来生成事件回调函数 'message = $event.target.value'
  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()')
  }
}

genDefaultModel 函数先处理了 modifiers,它的不同主要影响的是 event 和 valueExpression 的值,对于我们的例子,event 为 inputvalueExpression 为 $event.target.value。然后去执行 genAssignmentCode 去生成代码,它的定义在 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})`
  }
}

该方法首先对 v-model 对应的 value 做了解析,它处理了非常多的情况,对我们的例子,value 就是 messgae,所以返回的 res.key 为 null,然后我们就得到 ${value}=${assignment},也就是 message=$event.target.value。然后我们又命中了 needCompositionGuard 为 true 的逻辑,所以最终的 code 为 if($event.target.composing)return;message=$event.target.value

code 生成完后,又执行了 2 句非常关键的代码:

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

image.png

这实际上就是 input 实现 v-model 的精髓,通过修改 AST 元素,给 el 添加一个 prop,相当于我们在 input 上动态绑定了 value,又给 el 添加了事件处理,相当于在 input 上绑定了 input 事件,其实转换成模板如下:

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

其实就是动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message 设置为目标值,这样实际上就完成了数据双向绑定了,所以说 v-model 实际上就是语法糖。

再回到 genDirectives,它接下来的逻辑就是根据指令生成一些 data 的代码:

if (needRuntime) {
  hasRuntime = true
  res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
    dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
  }${
    dir.arg ? `,arg:"${dir.arg}"` : ''
  }${
    dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
  }},`
}

对我们的例子而言,最终生成的 render 代码如下:

with(this) {
  return _c('div',[_c('input',{
    directives:[{
      name:"model",
      rawName:"v-model",
      value:(message),
      expression:"message"
    }],
    attrs:{"placeholder":"edit me"},
    domProps:{"value":(message)},
    on:{"input":function($event){
      if($event.target.composing)
        return;
      message=$event.target.value
    }}}),_c('p',[_v("Message is: "+_s(message))])
    ])
}

现在我们知道了v-model通过编译转化为domPropson,然后在执行invokeCreateHooks时调用相应的操作dom的方法绑定到真是DOM上,比如domProps,就会执行如下操作:

image.png

function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  ...
  let key, cur
  // 真实的DOM节点
  const elm = vnode.elm
  for (key in props) {
    cur = props[key]
    ...
    if (key === 'value' && elm.tagName !== 'PROGRESS') {
      elm._value = cur
      const strCur = isUndef(cur) ? '' : String(cur)
      if (shouldUpdateValue(elm, strCur)) {
        // 把值赋值到dom节点的value属性上
        elm.value = strCur
      }
    }
    ...
  }
}

关于事件的处理之前的章节已经分析过了,所以对于 input 的 v-model 而言,完全就是语法糖,并且对于其它表单元素套路都是一样,区别在于生成的事件代码会略有不同。

v-model 除了作用在表单元素上,新版的 Vue 还把这一语法糖用在了组件上,接下来我们来分析它的实现。

vue对v-model的优化

下面的代码在输入框中输入英文的时候,两者表现一样,但是如果在输入中文的时候会有些不同,v-model在输入中文的时候只有在中文拼音输入完才会显示在输入框中,但是:value="value" @input="updateValue"在中文没有输入完的时候输入框会显示拼音字母,这显示是不符合我们要求的,所以vue对这种情况做了优化,下面我们从源码的角度看看它做了那些优化。

<input v-model="value">
<input :value="value" @input="updateValue" >

vue的入口文件里会执行如下代码:

import platformDirectives from './directives/index'

extend(Vue.options.directives, platformDirectives)

platformDirectives表示平台的指令,它有v-modelv-show两个web平台指令,其中v-model的指令如下:

import model from './model'
import show from './show'

export default {
  model,
  show
}

// model.js
const directive = {
  inserted (el, binding, vnode, oldVnode) {
    if (vnode.tag === 'select') {
      if (oldVnode.elm && !oldVnode.elm._vOptions) {
        mergeVNodeHook(vnode, 'postpatch', () => {
          directive.componentUpdated(el, binding, vnode)
        })
      } else {
        setSelected(el, binding, vnode.context)
      }
      el._vOptions = [].map.call(el.options, getValue)
    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
      el._vModifiers = binding.modifiers
      if (!binding.modifiers.lazy) {
        el.addEventListener('compositionstart', onCompositionStart)
        el.addEventListener('compositionend', onCompositionEnd)
        el.addEventListener('change', onCompositionEnd)
      }
    }
  },

  componentUpdated (el, binding, vnode) {
     ...
  }
}

那什么时候开始执行这个指令呢?

with(this) {
  return _c('div',[_c('input',{
    directives:[{
      name:"model",
      rawName:"v-model",
      value:(message),
      expression:"message"
    }],
    attrs:{"placeholder":"edit me"},
    domProps:{"value":(message)},
    on:{"input":function($event){
      if($event.target.composing)
        return;
      message=$event.target.value
    }}}),_c('p',[_v("Message is: "+_s(message))])
    ])
}

在真实dom节点创建成功后,执行invokeCreateHooks

function invokeCreateHooks (vnode, insertedVnodeQueue) {
    for (let i = 0; i < cbs.create.length; ++i) {
      cbs.create[i](emptyNode, vnode)
    }
    i = vnode.data.hook // Reuse variable
    if (isDef(i)) {
      if (isDef(i.create)) i.create(emptyNode, vnode)
      if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
    }
 }

然后会执行create钩子函数:

export default {
  create: updateDirectives,
  update: updateDirectives
}

function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode)
  }
}

function _update (oldVnode, vnode) {
  const isCreate = oldVnode === emptyNode
  const isDestroy = vnode === emptyNode
  const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
  // 找到v-model指令所对应的处理对象
  const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)

  const dirsWithInsert = []
  const dirsWithPostpatch = []

  let key, oldDir, dir
  for (key in newDirs) {
    oldDir = oldDirs[key]
    dir = newDirs[key]
    if (!oldDir) {
      callHook(dir, 'bind', vnode, oldVnode)
      if (dir.def && dir.def.inserted) {
        dirsWithInsert.push(dir)
      }
    } else {
        ...
    }
  }
  // 执行指令函数
  if (dirsWithInsert.length) {
    const callInsert = () => {
      for (let i = 0; i < dirsWithInsert.length; i++) {
        callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
      }
    }
    if (isCreate) {
      // 把insert所对应的函数赋值给vnode.data.hook中
      mergeVNodeHook(vnode, 'insert', callInsert)
    } else {
      callInsert()
    }
  }
  ...
}

function normalizeDirectives (
  dirs: ?Array<VNodeDirective>,
  vm: Component
): { [key: string]: VNodeDirective } {
  const res = Object.create(null)
  ...
  let i, dir
  for (i = 0; i < dirs.length; i++) {
    dir = dirs[i]
    
    res[getRawDirName(dir)] = dir
    // 从实例vm上的$options的directives对象中,找到v-model指令所对应的处理对象
    dir.def = resolveAsset(vm.$options, 'directives', dir.name, true)
  }
  return res
}

// 执行对应的指令函数
function callHook (dir, hook, vnode, oldVnode, isDestroy) {
  const fn = dir.def && dir.def[hook]
  if (fn) {
    try {
      // 传入vnode对应的真实DOM,指令对象
      fn(vnode.elm, dir, vnode, oldVnode, isDestroy)
    } catch (e) {
      handleError(e, vnode.context, `directive ${dir.name} ${hook} hook`)
    }
  }
}

以上的这些操作都是为了把指令所对应的函数存放在vnode.data.hook中,然后接着执行后面的逻辑,把当前vnode放到队列中。

i = vnode.data.hook // Reuse variable
if (isDef(i)) {
  if (isDef(i.create)) i.create(emptyNode, vnode)
  if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}

到目前为止还没有执行对应的指令函数,当所有的组件创建完成后执行执行invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch),这个时候才会执行指令对应的函数。

function invokeInsertHook (vnode, queue, initial) {
    if (isTrue(initial) && isDef(vnode.parent)) {
      vnode.parent.data.pendingInsert = queue
    } else {
      for (let i = 0; i < queue.length; ++i) {
        queue[i].data.hook.insert(queue[i])
      }
    }
  }

那么insert函数做了什么呢?它会给节点绑定compositionstart, compositionend两个事件。

const directive = {
  inserted (el, binding, vnode, oldVnode) {
    if (vnode.tag === 'select') {
      if (oldVnode.elm && !oldVnode.elm._vOptions) {
        mergeVNodeHook(vnode, 'postpatch', () => {
          directive.componentUpdated(el, binding, vnode)
        })
      } else {
        setSelected(el, binding, vnode.context)
      }
      el._vOptions = [].map.call(el.options, getValue)
    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
      el._vModifiers = binding.modifiers
      if (!binding.modifiers.lazy) {
        el.addEventListener('compositionstart', onCompositionStart)
        el.addEventListener('compositionend', onCompositionEnd)
        el.addEventListener('change', onCompositionEnd)
      }
    }
  },
}

当用户使用拼音输入法开始输入汉字时,compositionstart事件就会被触发。当文字组成完成或取消时, compositionend 事件将被触发。onCompositionStart触发的时机要早于input事件

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

function onCompositionEnd (e) {
  if (!e.target.composing) return
  e.target.composing = false
  trigger(e.target, 'input')
}

function trigger (el, type) {
  const e = document.createEvent('HTMLEvents')
  e.initEvent(type, true, true)
  el.dispatchEvent(e)
}

当用户输入中文的拼音时,触发onCompositionStart,把e.target.composing = true,此时再触发input事件,在input事件中有如下的逻辑if($event.target.composing)return;,所以它直接返回了。

 on:{"input":function($event){
      if($event.target.composing)
        return;
      message=$event.target.value
}}

当文字输入完成后,触发onCompositionEnd,同时自定义了一个input事件并触发。

这就是vuev-medel做的优化。

组件

为了更加直观,我们也是通过一个例子分析:

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

可以看到,父组件引用 child 子组件的地方使用了 v-model 关联了数据 message;而子组件定义了一个 value 的 prop,并且在 input 事件的回调函数中,通过 this.$emit('input', e.target.value) 派发了一个事件,为了让 v-model 生效,这两点是必须的。

接着我们从源码角度分析实现原理,还是从编译阶段说起,对于父组件而言,在编译阶段会解析 v-model 指令,依然会执行 genData 函数中的 genDirectives 函数,接着执行 src/platforms/web/compiler/directives/model.js 中定义的 model 函数,此时tag是'child',并命中如下逻辑:

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

image.png

genComponentModel 函数定义在 src/compiler/directives/model.js 中:

function genComponentModel (
  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}}`
  }
}

genComponentModel 的逻辑很简单,对我们的例子而言,生成的 el.model 的值为:

image.png

那么在 genDirectives 之后,genData 函数中有一段逻辑如下:

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

那么父组件最终生成的 render 代码如下:

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

然后在创建子组件 vnode 阶段,会执行 createComponent 函数,它的定义在 src/core/vdom/create-component.js 中:

export function createComponent (
 Ctor: Class<Component> | Function | Object | void,
 data: ?VNodeData,
 context: Component,
 children: ?Array<VNode>,
 tag?: string
): VNode | Array<VNode> | void {
 // ...
 // 把组件v-model中的数据放到组件的attrs和events中(transform component v-model data into props & events)
 if (isDef(data.model)) {
   transformModel(Ctor.options, data)
 }
 ...
 // 这个函数就会从data.props和data.attrs上摘取对应的值赋值给组件options上定义的props,这样组件就能获得到从父组件传递过来的值了。
 const propsData = extractPropsFromVNodeData(data, Ctor, tag)
 // ...
 const listeners = data.on
 // ...
 const vnode = new VNode(
   `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
   data, undefined, undefined, undefined, context,
   { Ctor, propsData, listeners, tag, children },
   asyncFactory
 )
 
 return vnode
}

其中会对 data.model 的情况做处理,执行 transformModel(Ctor.options, data) 方法:

function transformModel (options, data: any) {
  // 如果options没有写model,那么就默认为value和input
  const prop = (options.model && options.model.prop) || 'value'
  const event = (options.model && options.model.event) || 'input'
  // ;(data.props || (data.props = {}))[prop] = data.model.value (这是2.5版本的写法)
  (data.attrs || (data.attrs = {}))[prop] = data.model.value; (这是2.6版本的写法)
  const on = data.on || (data.on = {})
  if (isDef(on[event])) {
    on[event] = [data.model.callback].concat(on[event])
  } else {
    on[event] = data.model.callback
  }
}

transformModel 逻辑很简单,给 data.attrs 添加 data.model.value,并且给data.on 添加 data.model.callback,对我们的例子而言,扩展结果如下:

data.attrs = {
  value: (message),
}
data.on = {
  input: function ($$v) {
    message=$$v
  }
} 

其实就相当于我们在这样编写父组件:

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

子组件传递的 value 绑定到当前父组件的 message,同时监听自定义 input 事件,当子组件派发 input 事件的时候,父组件会在事件回调函数中修改 message 的值,同时 value 也会发生变化,子组件的 input 值被更新。

这就是典型的 Vue 的父子组件通讯模式,父组件通过 prop 把数据传递到子组件,子组件修改了数据后把改变通过 $emit 事件的方式通知父组件,所以说组件上的 v-model 也是一种语法糖。

另外我们注意到组件 v-model 的实现,子组件的 value prop 以及派发的 input 事件名是可配的,可以看到 transformModel 中对这部分的处理:

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

也就是说可以在定义子组件的时候通过 model 选项配置子组件接收的 prop 名以及派发的事件名,举个例子:

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

子组件修改了接收的 prop 名以及派发的事件名,然而这一切父组件作为调用方是不用关心的,这样做的好处是我们可以把 value 这个 prop 作为其它的用途。

那么至此,v-model 的实现就分析完了,我们了解到它是 Vue 双向绑定的真正实现,但本质上就是一种语法糖,它即可以支持原生表单元素,也可以支持自定义组件。在组件的实现中,我们是可以配置子组件接收的 prop 名称,以及派发的事件名称。