• v-model
    • 表单元素
    • 组件
    • 总结

    v-model

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

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

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

    也是先从编译阶段分析,首先是 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. for (i = 0, l = dirs.length; i < l; i++) {
    8. dir = dirs[i]
    9. needRuntime = true
    10. const gen: DirectiveFunction = state.directives[dir.name]
    11. if (gen) {
    12. // compile-time directive that manipulates AST.
    13. // returns true if it also needs a runtime counterpart.
    14. needRuntime = !!gen(el, dir, state.warn)
    15. }
    16. if (needRuntime) {
    17. hasRuntime = true
    18. res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
    19. dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
    20. }${
    21. dir.arg ? `,arg:"${dir.arg}"` : ''
    22. }${
    23. dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
    24. }},`
    25. }
    26. }
    27. if (hasRuntime) {
    28. return res.slice(0, -1) + ']'
    29. }
    30. }

    genDrirectives 方法就是遍历 el.directives,然后获取每一个指令对应的方法 const gen: DirectiveFunction = state.directives[dir.name],这个指令方法实际上是在实例化 CodegenState 的时候通过 option传入的,这个 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. )
    19. }
    20. }
    21. if (el.component) {
    22. genComponentModel(el, value, modifiers)
    23. // component v-model doesn't need extra runtime
    24. return false
    25. } else if (tag === 'select') {
    26. genSelect(el, value, modifiers)
    27. } else if (tag === 'input' && type === 'checkbox') {
    28. genCheckboxModel(el, value, modifiers)
    29. } else if (tag === 'input' && type === 'radio') {
    30. genRadioModel(el, value, modifiers)
    31. } else if (tag === 'input' || tag === 'textarea') {
    32. genDefaultModel(el, value, modifiers)
    33. } else if (!config.isReservedTag(tag)) {
    34. genComponentModel(el, value, modifiers)
    35. // component v-model doesn't need extra runtime
    36. return false
    37. } else if (process.env.NODE_ENV !== 'production') {
    38. warn(
    39. `<${el.tag} v-model="${value}">: ` +
    40. `v-model is not supported on this element type. ` +
    41. 'If you are working with contenteditable, it\'s recommended to ' +
    42. 'wrap a library dedicated for that purpose inside a custom component.'
    43. )
    44. }
    45. // ensure runtime directive metadata
    46. return true
    47. }

    也就是说我们执行 needRuntime = !!gen(el, dir, state.warn) 就是在执行 model 函数,它会根据 AST 元素节点的不同情况去执行不同的逻辑,对于我们这个 case 而言,它会命中 genDefaultModel(el, value, modifiers) 的逻辑,稍后我们也会介绍组件的处理,其它分支同学们可以自行去看。我们来看一下 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. )
    18. }
    19. }
    20. const { lazy, number, trim } = modifiers || {}
    21. const needCompositionGuard = !lazy && type !== 'range'
    22. const event = lazy
    23. ? 'change'
    24. : type === 'range'
    25. ? RANGE_TOKEN
    26. : 'input'
    27. let valueExpression = '$event.target.value'
    28. if (trim) {
    29. valueExpression = `$event.target.value.trim()`
    30. }
    31. if (number) {
    32. valueExpression = `_n(${valueExpression})`
    33. }
    34. let code = genAssignmentCode(value, valueExpression)
    35. if (needCompositionGuard) {
    36. code = `if($event.target.composing)return;${code}`
    37. }
    38. addProp(el, 'value', `(${value})`)
    39. addHandler(el, event, code, null, true)
    40. if (trim || number) {
    41. addHandler(el, 'blur', '$forceUpdate()')
    42. }
    43. }

    genDefaultModel 函数先处理了 modifiers,它的不同主要影响的是 eventvalueExpression 的值,对于我们的例子,eventinputvalueExpression$event.target.value。然后去执行 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. const res = parseModel(value)
    9. if (res.key === null) {
    10. return `${value}=${assignment}`
    11. } else {
    12. return `$set(${res.exp}, ${res.key}, ${assignment})`
    13. }
    14. }

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

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

    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
    2. v-bind:value="message"
    3. v-on:input="message=$event.target.value">

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

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

    1. if (needRuntime) {
    2. hasRuntime = true
    3. res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
    4. dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
    5. }${
    6. dir.arg ? `,arg:"${dir.arg}"` : ''
    7. }${
    8. dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
    9. }},`
    10. }

    对我们的例子而言,最终生成的 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. }

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

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

    组件

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

    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;而子组件定义了一个 valueprop,并且在 input 事件的回调函数中,通过 this.$emit('input', e.target.value) 派发了一个事件,为了让 v-model 生效,这两点是必须的。

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

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

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

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

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

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

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

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

    那么父组件最终生成的 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. if (isDef(data.model)) {
    11. transformModel(Ctor.options, data)
    12. }
    13. // extract props
    14. const propsData = extractPropsFromVNodeData(data, Ctor, tag)
    15. // ...
    16. // extract listeners, since these needs to be treated as
    17. // child component listeners instead of DOM listeners
    18. const listeners = data.on
    19. // ...
    20. const vnode = new VNode(
    21. `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    22. data, undefined, undefined, undefined, context,
    23. { Ctor, propsData, listeners, tag, children },
    24. asyncFactory
    25. )
    26. return vnode
    27. }

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

    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.props = {}))[prop] = data.model.value
    7. const on = data.on || (data.on = {})
    8. if (isDef(on[event])) {
    9. on[event] = [data.model.callback].concat(on[event])
    10. } else {
    11. on[event] = data.model.callback
    12. }
    13. }

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

    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. function transformModel (options, data: any) {
    2. const prop = (options.model && options.model.prop) || 'value'
    3. const event = (options.model && options.model.event) || 'input'
    4. // ...
    5. }

    也就是说可以在定义子组件的时候通过 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 作为其它的用途。

    总结

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

    原文: https://ustbhuangyi.github.io/vue-analysis/extend/v-model.html