v-model

v-model指令是 Vue.js 实现数据双向绑定的重要方式之一。如果不了解v-model的实现原理,我们真的会以为v-model会对数据进行双向绑定,但是实际上,Vue.js 里无论哪一种形式的双向绑定,其内部实现都是单向的。

使用v-model时,当改变视图,Vue.js 内部会将由视图改变的值,通过触发事件的方式反馈到数据层,通过预先添加的事件处理方法,改变数据的值。这一过程,我们仅仅通过使用v-model是无法知晓的,进而通过表象认为“使用v-model能做到数据的双向绑定”。

接下来,我们将从编译阶段、代码生成阶段、运行时阶段,一步一步分析v-model是如何实现的。

编译阶段

processAttrs

parse阶段会对元素节点上的所有特性进行处理。processAttrs函数里将解析特性的修饰符,当识别出v-model是指令且不是v-bindv-on指令时,会将其当做常规指令来处理:解析出指令的参数,并调用addDirective添加指令。

/**
 * 处理 attributes,包括指令和非指令
 */
function processAttrs (el) {
  const list = el.attrsList
  let i, l, name, rawName, value, modifiers, isProp
  for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name
    value = list[i].value
    // const dirRE = /^v-|^@|^:/
    if (dirRE.test(name)) {
      // 处理指令

      // mark element as dynamic
      // 标记元素是动态的,在优化 AST 阶段,若 el.hasBindings 为 true,则该元素就不是静态节点
      el.hasBindings = true
      // modifiers
      // 处理修饰符
      modifiers = parseModifiers(name)
      // 移除修饰符
      // modifierRE = /\.[^.]+/g
      if (modifiers) {
        name = name.replace(modifierRE, '')
      }
      if (bindRE.test(name)) { // v-bind
        // 处理数据绑定 v-bind 指令
        // ...
      } else if (onRE.test(name)) { // v-on
        // 处理事件监听
        // ...
      } else { // normal directives
        // 处理常规指令
        // dirRE = /^v-|^@|^:/
        name = name.replace(dirRE, '')
        // parse arg
        // argRE = /:(.*)$/
        // 解析指令的参数
        const argMatch = name.match(argRE)
        const arg = argMatch && argMatch[1]
        if (arg) {
          name = name.slice(0, -(arg.length + 1))
        }
        addDirective(el, name, rawName, value, arg, modifiers)
        if (process.env.NODE_ENV !== 'production' && name === 'model') {
          checkForAliasModel(el, value)
        }
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

addDirective

/**
 * 添加指令
 * @param {*} el 元素
 * @param {*} name 指令名称(经过处理,去除了 v-/@/: 前缀、修饰符、参数)
 * @param {*} rawName 指令名称(未经处理,保留了 v-/@/: 前缀、修饰符、参数)
 * @param {*} value 指令的表达式
 * @param {*} arg 指令的参数
 * @param {*} modifiers 指令的修饰符
 */
export function addDirective (
  el: ASTElement,
  name: string,
  rawName: string,
  value: string,
  arg: ?string,
  modifiers: ?ASTModifiers
) {
  (el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers })
  el.plain = false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

代码生成阶段

genData

在生成代码阶段,将调用genData函数生成节点的数据对象,在其中调用genDirectives生成指令相关的代码。

/**
 * 生成 createElement(name, data, children) 中的 data 数据对象(字符串形式)
 */
export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'

  // directives first.
  // directives may mutate the el's other properties before they are generated.

  // 生成 directives 数据
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

genDirectives

genDirectives函数里,会生成指令相关的代码,其主要做了两件事:

  1. 针对某些指令进行特殊的处理,调用它们各自的指令生成函数生成该指令的代码,包括:
    • 核心指令
      • v-on
      • v-bind
      • v-cloak
    • Web 平台指令
      • v-model
      • v-text
      • v-html
  2. 对于需要运行时的指令,将其拼成指令对象字符串

TODO: 该章节主要是讲述v-model指令的代码生成,后续将针对以上所列的其他指令进行详细分析。

/**
 * 生成 data 里 directives 数据
 *
 * el.directive 的数据结构为:[{ name, rawName, value, arg, modifiers }]
 *
 */
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
    /*
     * state.directives 包含的指令有
     *
     * - 核心指令
     *   - v-on
     *   - v-bind
     *   - v-cloak
     * - Web 平台指令
     *   - v-model
     *   - v-text
     *   - v-html
     */
    const gen: DirectiveFunction = state.directives[dir.name]
    if (gen) {
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.
      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) + ']'
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

state.directives 的来源

generate生成render函数字符串的最开始,会先基于传入的options选项对象生成CodegenState的实例statestate里包含了一些在代码生成过程中需要用到的数据,包括state.directives

import baseDirectives from '../directives/index'

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // _c: createElement
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

export class CodegenState {
  options: CompilerOptions;
  warn: Function;
  transforms: Array<TransformFunction>;
  dataGenFns: Array<DataGenFunction>;
  directives: { [key: string]: DirectiveFunction };
  maybeComponent: (el: ASTElement) => boolean;
  onceId: number;
  staticRenderFns: Array<string>;

  constructor (options: CompilerOptions) {
    this.options = options
    this.warn = options.warn || baseWarn
    this.transforms = pluckModuleFunction(options.modules, 'transformCode')
    this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
    this.directives = extend(extend({}, baseDirectives), options.directives)
    const isReservedTag = options.isReservedTag || no
    this.maybeComponent = (el: ASTElement) => !isReservedTag(el.tag)
    this.onceId = 0
    this.staticRenderFns = []
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

state.directives是有两部分组成的,baseDirectivesoptions.directives

baseDirectives

baseDirectives是核心的指令,独立于平台。

// src/compiler/directives/index.js
import on from './on'
import bind from './bind'
import { noop } from 'shared/util'

export default {
  on,
  bind,
  cloak: noop
}
1
2
3
4
5
6
7
8
9
10
options.directives

调用generate传入的options来源于baseOptions和用户调用compileToFunctions传入的options的合并,详情:compile 函数之 options 合并

实际上调用compileToFunctions传入的options并没有directives。只有baseOptions存在directives,如下所示:

// src/platforms/web/compiler/directives/index.js
import model from './model'
import text from './text'
import html from './html'

export default {
  model,
  text,
  html
}
1
2
3
4
5
6
7
8
9
10
合并 directives

最终的state.directives就是baseDirectivesbaseOptions.directives合并的结果。

export class CodegenState {
  constructor (options: CompilerOptions) {
    this.options = options
    // ...
    this.directives = extend(extend({}, baseDirectives), options.directives)
    // ...
  }
}
1
2
3
4
5
6
7
8

因此,我们找到了v-model的代码生成函数是在src/platforms/web/compiler/directives/model.js文件里,我们继续分析。

生成 v-model 指令代码

// src/platforms/web/compiler/directives/model.js

/**
 * 生成 v-model 指令的代码
 * @param {*} el AST 元素
 * @param {*} dir 指令对象,结构为 { name, rawName, value, arg, modifiers }
 * @param {*} _warn 警告函数
 * @return {Boolena} 是否需要额外的运行时
 */
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 (process.env.NODE_ENV !== 'production') {
    // inputs with type="file" are read only and setting the input's
    // value will throw an error.
    if (tag === 'input' && type === 'file') {
      warn(
        `<${el.tag} v-model="${value}" type="file">:\n` +
        `File inputs are read only. Use a v-on:change listener instead.`
      )
    }
  }

  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') {
    warn(
      `<${el.tag} v-model="${value}">: ` +
      `v-model is not supported on this element type. ` +
      'If you are working with contenteditable, it\'s recommended to ' +
      'wrap a library dedicated for that purpose inside a custom component.'
    )
  }

  // ensure runtime directive metadata
  return true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

纵观如上的model函数,我们能够知道,哪些元素可以使用v-model指令:

  • 动态组件
  • select元素
  • checkbox类型的input元素
  • radio类型的input元素
  • 其他类型的input元素
  • textarea元素
  • 自定义组件

我们将一一详细讲解如上的各个元素的v-model指令的代码生成。

动态组件、自定义组件

组件的v-model的代码生成,最终会往 AST 元素上添加model属性,即el.model = { value, express, callback }

其具体的生成过程为:

  1. 构造v-model指令的valueExpression
    • valueExpression初始为$v
    • 若存在.trim修饰符,加上trim()相关的代码
    • 若存在.number修饰符,加上toNumber相关的代码
  2. 基于指令的原始表达式valuevalueExpression构造赋值语句assignment
    • 解析value
      • value是属性的方式,则赋值语句为${value}=${valueExpression}
      • value是属性路径的方式,解析出路径最终的keykey之前的对象exp,赋值语句为$set(${res.exp}, ${res.key}, ${valueExpression})
  3. 往 AST 元素上添加model属性,其值为对象,包含如下属性:
    • valuev-model表达式的字符串形式
    • expressionv-model表达式字符串的 JSON 形式
    • callbackv-model的表达式改变时的回调函数,function (${baseValueExpression}) {${assignment}}
// src/compiler/directives/model.js
/**
 * Cross-platform code generation for component v-model
 * 跨平台生成组件节点 v-model 指令的代码
 *
 * @param {*} el 组件 AST 节点
 * @param {*} value v-model 的表达式
 * @param {*} modifiers v-model 的修饰符对象
 */
export 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) {
    // _n: toNumber,转换为数字
    valueExpression = `_n(${valueExpression})`
  }
  const assignment = genAssignmentCode(value, valueExpression)

  el.model = {
    value: `(${value})`,
    expression: `"${value}"`,
    callback: `function (${baseValueExpression}) {${assignment}}`
  }
}

/**
 * Cross-platform codegen helper for generating v-model value assignment code.
 */
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})`
  }
}

/**
 * Parse a v-model expression into a base path and a final key segment.
 * Handles both dot-path and possible square brackets.
 *
 * Possible cases:
 *
 * - test
 * - test[key]
 * - test[test1[key]]
 * - test["a"][key]
 * - xxx.test[a[a].test1[key]]
 * - test.xxx.a["asa"][test1[key]]
 *
 */

let len, str, chr, index, expressionPos, expressionEndPos

type ModelParseResult = {
  exp: string,
  key: string | null
}

/**
 * 解析出 v-model 表达式里的 exp 部分和 key 部分
 * @param {*} val 表达式
 * @return {Object} 结果对象,{ exp、key }
 *
 * 针对可能的表达式返回的结果:
 *
 * - test :                              {exp: 'test', key: null}
 * - test.test1 :                         {exp: 'test', key: '"test1"'}
 * - test[key]                            {exp: 'test', key: 'key'}
 * - test[test1[key]]                     {exp: 'test', key: 'test1[key]'}
 * - test["a"][key]                       {exp: 'test["a"]', key: 'key'}
 * - xxx.test[a[a].test1[key]]            {exp: 'xxx.test', key: 'a[a].test1[key]'}
 * - test.xxx.a["asa"][test1[key]]        {exp: 'test.xxx.a["asa"]', key: 'test1[key]'}
 */
export function parseModel (val: string): ModelParseResult {
  // Fix https://github.com/vuejs/vue/pull/7730
  // allow v-model="obj.val " (trailing whitespace)
  val = val.trim()
  len = val.length

  // 不存在 [,或 ] 不是最后一位
  if (val.indexOf('[') < 0 || val.lastIndexOf(']') < len - 1) {
    index = val.lastIndexOf('.')
    if (index > -1) {
      return {
        exp: val.slice(0, index),
        key: '"' + val.slice(index + 1) + '"'
      }
    } else {
      return {
        exp: val,
        key: null
      }
    }
  }

  str = val
  index = expressionPos = expressionEndPos = 0

  while (!eof()) {
    chr = next()
    /* istanbul ignore if */
    if (isStringStart(chr)) {
      parseString(chr)
    } else if (chr === 0x5B) {
      // 0x5B: 左中括号 [
      parseBracket(chr)
    }
  }

  return {
    exp: val.slice(0, expressionPos),
    key: val.slice(expressionPos + 1, expressionEndPos)
  }
}

function next (): number {
  return str.charCodeAt(++index)
}

function eof (): boolean {
  return index >= len
}

function isStringStart (chr: number): boolean {
  // 0x22: 双引号 "
  // 0x27: 单引号 '
  return chr === 0x22 || chr === 0x27
}

function parseBracket (chr: number): void {
  let inBracket = 1
  expressionPos = index
  while (!eof()) {
    chr = next()
    if (isStringStart(chr)) {
      parseString(chr)
      continue
    }
    // 0x5B: 左中括号 [
    // 0x5D: 右中括号 ]
    if (chr === 0x5B) inBracket++
    if (chr === 0x5D) inBracket--
    if (inBracket === 0) {
      expressionEndPos = index
      break
    }
  }
}

/**
 * 解析字符串,找到下一个相同的符号,比如 " 或 '
 */
function parseString (chr: number): void {
  const stringQuote = chr
  while (!eof()) {
    chr = next()
    if (chr === stringQuote) {
      break
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

我们给出一简单示例,方便理解上面的生成代码:

// 父组件
const ParentComponent = {
  name: 'ParentComponent',
  template: `
    <div class="parent-root">
      <ChildComponent v-model="first"></ChildComponent>
    </div>
  `,
  components: {
    ChildComponent
  },
  data () {
    return {
      simpleProperty: 'simpleProperty',
      propertyPath: {
        propertyKey: 'third'
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 父组件的 render 函数
(function anonymous() {
    with (this) {
        return _c('div', {
            staticClass: "parent-root"
        }, [_c('ChildComponent', {
            // v-model="simpleProperty"
            model: {
                value: (simpleProperty),
                callback: function($v) {
                    simpleProperty = $v
                },
                expression: "simpleProperty"
            }

            // v-model.trim="simpleProperty"
            model: {
                value: (singleProperty),
                callback: function($v) {
                    singleProperty = (typeof $v === 'string' ? $v.trim() : $v)
                },
                expression: "singleProperty"
            }

            // v-model.number="simpleProperty"
            model: {
                value: (singleProperty),
                callback: function($v) {
                    singleProperty = _n($v)
                },
                expression: "singleProperty"
            }

            // v-model="propertyPath.propertyKey"
            model: {
                value: (propertyPath.propertyKey),
                callback: function($v) {
                    $set(propertyPath, "propertyKey", $v)
                },
                expression: "propertyPath.propertyKey"
            }
        })], 1)
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

由此我们看出,组件的v-model在代码生成阶段,最终会转换为render函数里组件节点数据对象里的model相关内容。

PS: 下一步可跳转到本章的“运行时阶段-组件的v-model”继续阅读,保持阅读的连贯性。

select 元素

若元素是select元素,则将生成select元素v-model指令的代码:往元素上添加change事件及事件处理方法。

function genSelect (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
) {
  const number = modifiers && modifiers.number
  const selectedVal = `Array.prototype.filter` +
    `.call($event.target.options,function(o){return o.selected})` +
    `.map(function(o){var val = "_value" in o ? o._value : o.value;` +
    `return ${number ? '_n(val)' : 'val'}})`

  const assignment = '$event.target.multiple ? $selectedVal : $selectedVal[0]'
  let code = `var $selectedVal = ${selectedVal};`
  code = `${code} ${genAssignmentCode(value, assignment)}`
  addHandler(el, 'change', code, null, true)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

我们看到,拼接好事件处理方法的函数体之后,会调用addHandler给元素添加change事件,最终将通过addEventListener添加到select元素的 DOM 节点上。addHandler之后的处理,可参考event 之 addhandler

需要注意的是,在model函数内调用genSelect后,将返回将是true,这也意味着select元素的v-model指令,需要运行时,因此会将v-model指令的相关数据放置在元素的数据对象的data.directives选项里。

TODO: 为什么需要运行时?

我们通过简单的示例来说明生成的代码是什么样的。

// 源码
const ExampleComp = {
  template: `
      <select v-model="value">
        <option value="1">1</option>
        <option value="2">2</option>
        <option value="3">3</option>
        <option value="4">4</option>
      </select>
  `,
  data () {
    return {
      value: 1
    }
  },
  mounted () {
    console.log(this.$options.render)
  }
}

new Vue({
  el: '#app',
  store,
  components: { ExampleComp },
  template: '<ExampleComp></ExampleComp>'
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ExampleComp 组件的 render 函数
(function anonymous() {
    with (this) {
        return _c(
            'select',
            // select 元素的数据对象
            {
                directives: [{
                    name: "model",
                    rawName: "v-model",
                    value: (value),
                    expression: "value"
                }],
                on: {
                    "change": function($event) {
                        var $selectedVal = Array.prototype.filter.call($event.target.options, function(o) {
                            return o.selected
                        }).map(function(o) {
                            var val = "_value"in o ? o._value : o.value;
                            return val
                        });
                        value = $event.target.multiple ? $selectedVal : $selectedVal[0]
                    }
                }
            },
            // select 元素的子元素
            [
                _c('option', {
                    attrs: {
                        "value": "1"
                    }
                }, [_v("1")]), _v(" "),
                 _c('option', {
                    attrs: {
                        "value": "2"
                    }
                }, [_v("2")]), _v(" "),
                _c('option', {
                    attrs: {
                        "value": "3"
                    }
                }, [_v("3")]), _v(" "),
                _c('option', {
                    attrs: {
                        "value": "4"
                    }
                }, [_v("4")])
            ]
        )
    }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

默认的 input、textarea 元素

若元素是input元素且不是radiocheckbox,或元素是textarea元素,就会走该逻辑:

  1. (非生产环境)对同时使用v-model指令和v-bind:value指令且没使用v-bind:type的情况给予警告
  2. 判断是否需要进行composition处理open in new window
  3. 判断事件的类型
    • 若有lazy修饰符,则使用change事件
    • 若没有lazy修饰符
      • 若不是range,则使用input事件
      • 若是range,则需要在运行时确定事件类型
  4. 生成事件处理方法的函数体部分
  5. 往元素上添加名为valuedomProps
  6. 往元素上添加事件及事件处理方法
  7. 若存在trimnumber修饰符,则添加blur事件,在blur事件发生时,对组件进行强制刷新

提示

第 5 步添加的value是添加到元素数据对象的domProps上的

function genDefaultModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  const type = el.attrsMap.type

  // warn if v-bind:value conflicts with v-model
  // except for inputs with v-bind:type
  if (process.env.NODE_ENV !== 'production') {
    const value = el.attrsMap['v-bind:value'] || el.attrsMap[':value']
    const typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']
    if (value && !typeBinding) {
      const binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'
      warn(
        `${binding}="${value}" conflicts with v-model on the same element ` +
        'because the latter already expands to a value binding internally'
      )
    }
  }

  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) {
    // composing 用于处理中文输入截断问题,详见:https://segmentfault.com/a/1190000009246058
    code = `if($event.target.composing)return;${code}`
  }

  // 添加名为 value 的 prop
  addProp(el, 'value', `(${value})`)
  addHandler(el, event, code, null, true)
  if (trim || number) {
    addHandler(el, 'blur', '$forceUpdate()')
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

运行时阶段

组件的 v-model

在运行时阶段,组件的v-model将被添加为组件的自定义事件,每次触发v-model对应的事件处理方法,将改变v-model表达式的值,进而在组件外部看来,组件的v-model实现了数据的双向绑定。

将组件的 v-model 添加为自定义事件

在运行时阶段生成组件的 VNode 时,会对v-model生成的代码(在data.model里)进行处理,处理过程在transformModel函数里,包括:

  1. 从组件的选项对象里获取自定义的model选项,即model.propmodel.event,若不存在则使用value作为prop的名称,input作为event的事件类型
  2. v-model的表达式作为data.props[prop]的值
  3. data.props上添加新的prop
  4. data.on上添加新的事件和事件处理方法,事件名为event的值,事件处理方法为data.model.callback

因为组件的data.on之后将作为listeners,因此对组件上的v-model的处理实际上是采用的自定义的事件,而不是原生事件。

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {
  // ...

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // ...

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
 * 将 v-model 转换到子组件的 prop、event
 * @param {*} options 组件选项对象
 * @param {*} data 组件数据对象(从模块解析而来的数据 或 调用 createElement 传入的数据对象)
 */
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
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

如何触发组件 v-model 的自定义事件

如何触发组件v-model的自定义事件,相对比较简单,官方已经给出了示例:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

我们看到,当想要改变组件上的v-model的表达式时,需要显式的调用vm.$emit去触发v-model自定义事件,并传入要变更的值,v-model的自定义事件处理方法将执行并更改v-model表达式的值。

总结

v-model的本质是都将指令最终转换为事件。

  • 动态组件、自定义组件:转换为组件的自定义事件,需要通过vm.$emit触发自定义事件
  • select元素:转换为 DOM 原生change事件,在视图改变时,自动触发change事件