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