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

这是一个非常简单的演示,我们在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 }
13 directives定义在src/platforms/web/compiler/directives/index.js中:
14 
15 
16 export default {
17   model,
18   text,
19   html
20 }
21 // 那么对于v-model而言,对应的directive函数是在src/platforms/web/compiler/directives/model.js中定义的model函数:
22 
23 export default function model (
24   el: ASTElement,
25   dir: ASTDirective,
26   _warn: Function
27 ): ?boolean {
28   warn = _warn
29   const value = dir.value
30   const modifiers = dir.modifiers
31   const tag = el.tag
32   const type = el.attrsMap.type
33 34   if (process.env.NODE_ENV !== 'production') {
35     // inputs with type="file" are read only and setting the input's
36     // value will throw an error.
37     if (tag === 'input' && type === 'file') {
38       warn(
39         `<${el.tag} v-model="${value}" type="file">:\n` +
40         `File inputs are read only. Use a v-on:change listener instead.`
41       )
42     }
43   }
44 45   if (el.component) {
46     genComponentModel(el, value, modifiers)
47     // component v-model doesn't need extra runtime
48     return false
49   } else if (tag === 'select') {
50     genSelect(el, value, modifiers)
51   } else if (tag === 'input' && type === 'checkbox') {
52     genCheckboxModel(el, value, modifiers)
53   } else if (tag === 'input' && type === 'radio') {
54     genRadioModel(el, value, modifiers)
55   } else if (tag === 'input' || tag === 'textarea') {
56     genDefaultModel(el, value, modifiers)
57   } else if (!config.isReservedTag(tag)) {
58     genComponentModel(el, value, modifiers)
59     // component v-model doesn't need extra runtime
60     return false
61   } else if (process.env.NODE_ENV !== 'production') {
62     warn(
63       `<${el.tag} v-model="${value}">: ` +
64       `v-model is not supported on this element type. ` +
65       'If you are working with contenteditable, it\'s recommended to ' +
66       'wrap a library dedicated for that purpose inside a custom component.'
67     )
68   }
69 70   // ensure runtime directive metadata
71   return true
72 }

也就是说我们执行needRuntime = !!gen(el, dir, state.warn)就是在执行model函数,它会根据AST元素例程的不同情况去执行不同的逻辑,对于我们这个案例而言,它会命中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  8   // warn if v-bind:value conflicts with v-model
 9   // except for inputs with v-bind:type
10   if (process.env.NODE_ENV !== 'production') {
11     const value = el.attrsMap['v-bind:value'] || el.attrsMap[':value']
12     const typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']
13     if (value && !typeBinding) {
14       const binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'
15       warn(
16         `${binding}="${value}" conflicts with v-model on the same element ` +
17         'because the latter already expands to a value binding internally'
18       )
19     }
20   }
21 22   const { lazy, number, trim } = modifiers || {}
23   const needCompositionGuard = !lazy && type !== 'range'
24   const event = lazy
25     ? 'change'
26     : type === 'range'
27       ? RANGE_TOKEN
28       : 'input'
29 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 38   let code = genAssignmentCode(value, valueExpression)
39   if (needCompositionGuard) {
40     code = `if($event.target.composing)return;${code}`
41   }
42 43   addProp(el, 'value', `(${value})`)
44   addHandler(el, event, code, null, true)
45   if (trim || number) {
46     addHandler(el, 'blur', '$forceUpdate()')
47   }
48 }

 

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还把这一语法糖用在了组件上,接下来我们来分析它的实现。

 

本文转自 http://caibaojian.com/vue-analysis/extend/v-model.html


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM