数据响应是通过数据的改变去驱动 DOM 视图的变化
双向绑定除了数据驱动 DOM 外, DOM 的变化反过来影响数据,是一个双向关系
Vue 中通过 v-model 来实现双向绑定
v-model 即可以作用在普通表单元素上,又可以作用在组件上,它其实是一个语法糖

表单元素

例子

  1. let vm = new Vue({
  2. el: '#app',
  3. template: '<div>'
  4. + '<input v-model="message" placeholder="edit me">' +
  5. '<p>Message is: {{ message }}</p>' +
  6. '</div>',
  7. data() {
  8. return {
  9. message: ''
  10. }
  11. }
  12. })

从编译阶段分析,首先是 parse 阶段, v-model 被当做普通的指令解析到 el.directives 中,然后在 codegen 阶段,执行 genData 的时候,会执行 const dirs = genDirectives(el, state)
定义在 src/compiler/codegen/index.js 中

  1. function genDirectives (el: ASTElement, state: CodegenState): string | void {
  2. const dirs = el.directives
  3. if (!dirs) return
  4. let res = 'directives:['
  5. let hasRuntime = false
  6. let i, l, dir, needRuntime
  7. // 遍历el.directives
  8. for (i = 0, l = dirs.length; i < l; i++) {
  9. dir = dirs[i]
  10. needRuntime = true
  11. // 获取每一个指令对应的方法
  12. // 指令方法实际上是在实例化CodegenState时通过option传入的,option是编译相关的配置,它在不同的平台下配置不同
  13. const gen: DirectiveFunction = state.directives[dir.name]
  14. if (gen) {
  15. // compile-time directive that manipulates AST.
  16. // returns true if it also needs a runtime counterpart.
  17. needRuntime = !!gen(el, dir, state.warn) // model函数
  18. }
  19. // 根据指令生成一些data代码
  20. if (needRuntime) {
  21. hasRuntime = true
  22. res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
  23. dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
  24. }${
  25. dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
  26. }${
  27. dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
  28. }},`
  29. }
  30. }
  31. if (hasRuntime) {
  32. return res.slice(0, -1) + ']'
  33. }
  34. }

option在web环境下的定义在src/platforms/web/compiler/options.js中

  1. export const baseOptions: CompilerOptions = {
  2. expectHTML: true,
  3. modules,
  4. directives,
  5. isPreTag,
  6. isUnaryTag,
  7. mustUseProp,
  8. canBeLeftOpenTag,
  9. isReservedTag,
  10. getTagNamespace,
  11. staticKeys: genStaticKeys(modules)
  12. }

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

  1. export default {
  2. model,
  3. text,
  4. html
  5. }

v-model对应的 directive 函数是在 src/platforms/web/compiler/directives/model.js 中定义的 model 函数

  1. export default function model (
  2. el: ASTElement,
  3. dir: ASTDirective,
  4. _warn: Function
  5. ): ?boolean {
  6. warn = _warn
  7. const value = dir.value
  8. const modifiers = dir.modifiers
  9. const tag = el.tag
  10. const type = el.attrsMap.type
  11. if (process.env.NODE_ENV !== 'production') {
  12. // inputs with type="file" are read only and setting the input's
  13. // value will throw an error.
  14. if (tag === 'input' && type === 'file') {
  15. warn(
  16. `<${el.tag} v-model="${value}" type="file">:\n` +
  17. `File inputs are read only. Use a v-on:change listener instead.`,
  18. el.rawAttrsMap['v-model']
  19. )
  20. }
  21. }
  22. // 根据AST元素节点的不同情况去执行不同的逻辑
  23. if (el.component) {
  24. genComponentModel(el, value, modifiers)
  25. // component v-model doesn't need extra runtime
  26. return false
  27. } else if (tag === 'select') {
  28. genSelect(el, value, modifiers)
  29. } else if (tag === 'input' && type === 'checkbox') {
  30. genCheckboxModel(el, value, modifiers)
  31. } else if (tag === 'input' && type === 'radio') {
  32. genRadioModel(el, value, modifiers)
  33. } else if (tag === 'input' || tag === 'textarea') {
  34. // 例子执行到这里
  35. genDefaultModel(el, value, modifiers)
  36. } else if (!config.isReservedTag(tag)) {
  37. genComponentModel(el, value, modifiers)
  38. // component v-model doesn't need extra runtime
  39. return false
  40. } else if (process.env.NODE_ENV !== 'production') {
  41. warn(
  42. `<${el.tag} v-model="${value}">: ` +
  43. `v-model is not supported on this element type. ` +
  44. 'If you are working with contenteditable, it\'s recommended to ' +
  45. 'wrap a library dedicated for that purpose inside a custom component.',
  46. el.rawAttrsMap['v-model']
  47. )
  48. }
  49. // ensure runtime directive metadata
  50. return true
  51. }

也就是说执行 needRuntime = !!gen(el, dir, state.warn) 就是在执行 model 函数
genDefaultModel

  1. function genDefaultModel (
  2. el: ASTElement,
  3. value: string,
  4. modifiers: ?ASTModifiers
  5. ): ?boolean {
  6. const type = el.attrsMap.type
  7. // warn if v-bind:value conflicts with v-model
  8. // except for inputs with v-bind:type
  9. if (process.env.NODE_ENV !== 'production') {
  10. const value = el.attrsMap['v-bind:value'] || el.attrsMap[':value']
  11. const typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']
  12. if (value && !typeBinding) {
  13. const binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'
  14. warn(
  15. `${binding}="${value}" conflicts with v-model on the same element ` +
  16. 'because the latter already expands to a value binding internally',
  17. el.rawAttrsMap[binding]
  18. )
  19. }
  20. }
  21. // 处理了modeifiers,它的不同主要影响的是event和valueExpression的值
  22. const { lazy, number, trim } = modifiers || {}
  23. const needCompositionGuard = !lazy && type !== 'range'
  24. // 对于当前的例子 event为input valueExpression为$event.target.value
  25. const event = lazy
  26. ? 'change'
  27. : type === 'range'
  28. ? RANGE_TOKEN
  29. : 'input'
  30. let valueExpression = '$event.target.value'
  31. if (trim) {
  32. valueExpression = `$event.target.value.trim()`
  33. }
  34. if (number) {
  35. valueExpression = `_n(${valueExpression})`
  36. }
  37. // 执行genAssignmentCode去生成代码
  38. let code = genAssignmentCode(value, valueExpression)
  39. if (needCompositionGuard) {
  40. // 最终code为 if($event.target.composing) return;message=$event.target.value
  41. code = `if($event.target.composing)return;${code}`
  42. }
  43. addProp(el, 'value', `(${value})`)
  44. addHandler(el, event, code, null, true)
  45. if (trim || number) {
  46. addHandler(el, 'blur', '$forceUpdate()')
  47. }
  48. }

genAssignmentCode定义在src/compiler/directives/model.js中

  1. /**
  2. * Cross-platform codegen helper for generating v-model value assignment code.
  3. */
  4. export function genAssignmentCode (
  5. value: string,
  6. assignment: string
  7. ): string {
  8. // 对v-model对应的value做解析
  9. // 例子中 value就是message res.key为null
  10. const res = parseModel(value)
  11. if (res.key === null) {
  12. return `${value}=${assignment}` // 也就是message=$event.target.value
  13. } else {
  14. return `$set(${res.exp}, ${res.key}, ${assignment})`
  15. }
  16. }

code生成后

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

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

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

其实就是动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message 设置为目标值,这样实际上就完成了数据双向绑定了,所以说 v-model 实际上就是语法糖
对例子而言最终生成的render代码如下

  1. with(this) {
  2. return _c('div',[_c('input',{
  3. directives:[{
  4. name:"model",
  5. rawName:"v-model",
  6. value:(message),
  7. expression:"message"
  8. }],
  9. attrs:{"placeholder":"edit me"},
  10. domProps:{"value":(message)},
  11. on:{"input":function($event){
  12. if($event.target.composing)
  13. return;
  14. message=$event.target.value
  15. }}}),_c('p',[_v("Message is: "+_s(message))])
  16. ])
  17. }

组件

例子

  1. let Child = {
  2. template: '<div>'
  3. + '<input :value="value" @input="updateValue" placeholder="edit me">' +
  4. '</div>',
  5. props: ['value'],
  6. methods: {
  7. updateValue(e) {
  8. this.$emit('input', e.target.value)
  9. }
  10. }
  11. }
  12. let vm = new Vue({
  13. el: '#app',
  14. template: '<div>' +
  15. '<child v-model="message"></child>' +
  16. '<p>Message is: {{ message }}</p>' +
  17. '</div>',
  18. data() {
  19. return {
  20. message: ''
  21. }
  22. },
  23. components: {
  24. Child
  25. }
  26. })

父组件引用 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 函数,并命中如下逻辑

  1. else if (!config.isReservedTag(tag)) {
  2. genComponentModel(el, value, modifiers)
  3. // component v-model doesn't need extra runtime
  4. return false
  5. }

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

  1. /**
  2. * Cross-platform code generation for component v-model
  3. */
  4. export function genComponentModel (
  5. el: ASTElement,
  6. value: string,
  7. modifiers: ?ASTModifiers
  8. ): ?boolean {
  9. const { number, trim } = modifiers || {}
  10. const baseValueExpression = '$$v'
  11. let valueExpression = baseValueExpression
  12. if (trim) {
  13. valueExpression =
  14. `(typeof ${baseValueExpression} === 'string'` +
  15. `? ${baseValueExpression}.trim()` +
  16. `: ${baseValueExpression})`
  17. }
  18. if (number) {
  19. valueExpression = `_n(${valueExpression})`
  20. }
  21. const assignment = genAssignmentCode(value, valueExpression)
  22. el.model = {
  23. value: `(${value})`,
  24. expression: JSON.stringify(value),
  25. callback: `function (${baseValueExpression}) {${assignment}}`
  26. }
  27. }

对例子而言生成的el.model的值为

  1. el.model = {
  2. callback:'function ($$v) {message=$$v}',
  3. expression:'"message"',
  4. value:'(message)'
  5. }

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

  1. // component v-model
  2. if (el.model) {
  3. data += `model:{value:${
  4. el.model.value
  5. },callback:${
  6. el.model.callback
  7. },expression:${
  8. el.model.expression
  9. }},`
  10. }

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

  1. with(this){
  2. return _c('div',[_c('child',{
  3. model:{
  4. value:(message),
  5. callback:function ($$v) {
  6. message=$$v
  7. },
  8. expression:"message"
  9. }
  10. }),
  11. _c('p',[_v("Message is: "+_s(message))])],1)
  12. }

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

  1. export function createComponent (
  2. Ctor: Class<Component> | Function | Object | void,
  3. data: ?VNodeData,
  4. context: Component,
  5. children: ?Array<VNode>,
  6. tag?: string
  7. ): VNode | Array<VNode> | void {
  8. // ...
  9. // transform component v-model data into props & events
  10. // 对data.model的情况做处理
  11. if (isDef(data.model)) {
  12. transformModel(Ctor.options, data)
  13. }
  14. // extract props
  15. const propsData = extractPropsFromVNodeData(data, Ctor, tag)
  16. // ...
  17. // extract listeners, since these needs to be treated as
  18. // child component listeners instead of DOM listeners
  19. const listeners = data.on
  20. // ...
  21. const vnode = new VNode(
  22. `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
  23. data, undefined, undefined, undefined, context,
  24. { Ctor, propsData, listeners, tag, children },
  25. asyncFactory
  26. )
  27. // ...
  28. return vnode
  29. }

transformModel

  1. // transform component v-model info (value and callback) into
  2. // prop and event handler respectively.
  3. function transformModel (options, data: any) {
  4. const prop = (options.model && options.model.prop) || 'value'
  5. const event = (options.model && options.model.event) || 'input'
  6. // 给data.props添加data.model.value,并且给data.on添加data.model.callback
  7. ;(data.attrs || (data.attrs = {}))[prop] = data.model.value
  8. const on = data.on || (data.on = {})
  9. const existing = on[event]
  10. const callback = data.model.callback
  11. if (isDef(existing)) {
  12. if (
  13. Array.isArray(existing)
  14. ? existing.indexOf(callback) === -1
  15. : existing !== callback
  16. ) {
  17. on[event] = [callback].concat(existing)
  18. }
  19. } else {
  20. on[event] = callback
  21. }
  22. }

对例子而言扩展结果如下

  1. data.props = {
  2. value: (message),
  3. }
  4. data.on = {
  5. input: function ($$v) {
  6. message=$$v
  7. }
  8. }

其实就相当于这样编写父组件

  1. let vm = new Vue({
  2. el: '#app',
  3. template: '<div>' +
  4. '<child :value="message" @input="message=arguments[0]"></child>' +
  5. '<p>Message is: {{ message }}</p>' +
  6. '</div>',
  7. data() {
  8. return {
  9. message: ''
  10. }
  11. },
  12. components: {
  13. Child
  14. }
  15. })

子组件传递的 value 绑定到当前父组件的 message,同时监听自定义 input 事件,当子组件派发 input 事件的时候,父组件会在事件回调函数中修改 message 的值,同时 value 也会发生变化,子组件的 input 值被更新
这就是典型的 Vue 的父子组件通讯模式,父组件通过 prop 把数据传递到子组件,子组件修改了数据后把改变通过 $emit 事件的方式通知父组件,所以说组件上的 v-model 也是一种语法糖

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

  1. const prop = (options.model && options.model.prop) || 'value'
  2. const event = (options.model && options.model.event) || 'input'

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

  1. let Child = {
  2. template: '<div>'
  3. + '<input :value="msg" @input="updateValue" placeholder="edit me">' +
  4. '</div>',
  5. props: ['msg'],
  6. model: {
  7. prop: 'msg',
  8. event: 'change'
  9. },
  10. methods: {
  11. updateValue(e) {
  12. this.$emit('change', e.target.value)
  13. }
  14. }
  15. }
  16. let vm = new Vue({
  17. el: '#app',
  18. template: '<div>' +
  19. '<child v-model="message"></child>' +
  20. '<p>Message is: {{ message }}</p>' +
  21. '</div>',
  22. data() {
  23. return {
  24. message: ''
  25. }
  26. },
  27. components: {
  28. Child
  29. }
  30. })

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