很多同學在理解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,它的不同主要影響的是event和valueExpression的值,對於我們的示例,event為input,valueExpression為$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.key為null,然后我們就得到${value}=${assignment},也就是message=$event.target.value。然后我們又命中了needCompositionGuard為true的邏輯,所以最終的code為if($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">
其實就是動態綁定了input的value指向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 }
關於事件的處理我們之前的章節已經分析過了,所以對於input的v-model而言,完全就是語法糖,並且對於其他表單元素套路都是一樣,區別在於生成的事件代碼會略有不同。
v-model 除了作用在表單元素上,新版的Vue還把這一語法糖用在了組件上,接下來我們來分析它的實現。
本文轉自
