v-model數據綁定分析
v-model數據綁定分析
v-model
是Vue
提供的指令,其主要作用是可以實現在表單<input>
、<textarea>
及<select>
等元素以及組件上創建雙向數據綁定,其本質上就是一種語法糖,既可以直接定義在原生表單元素,也可以支持自定義組件。在組件的實現中,可以配置子組件接收的prop
名稱,以及派發的事件名稱實現組件內的v-model
雙向綁定。
描述
可以用v-model
指令在表單<input>
、<textarea>
及<select>
元素上創建雙向數據綁定,其會根據控件類型自動選取正確的方法來更新元素,以<input>
作為示例使用v-model
。
<!DOCTYPE html>
<html>
<head>
<title>Vue</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="//cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: "#app",
data: {
msg: ""
},
template: `
<div>
<div>Message is: {{ msg }}</div>
<input v-model="msg">
</div>
`
})
</script>
</html>
當不使用v-model
語法糖時,可以自行實現一個雙向綁定,實際上v-model
在內部為不同的輸入元素使用不同的property
並拋出不同的事件:
input
和textarea
元素使用value property
和input
事件。checkbox
和radio
元素使用checked property
和change
事件。select
元素將value
作為prop
並將change
作為事件。
同樣以<input>
作為示例而不使用v-model
實現雙向綁定。
<!DOCTYPE html>
<html>
<head>
<title>Vue</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="//cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: "#app",
data: {
msg: ""
},
template: `
<div>
<div>Message is: {{ msg }}</div>
<input :value="msg" @input="msg = $event.target.value">
</div>
`
})
</script>
</html>
對於v-model
還有修飾符用以控制用戶輸入:
.trim
: 輸入首尾空格過濾。.lazy
: 取代input
事件而監聽change
事件。.number
: 輸入字符串轉為有效的數字,如果這個值無法被parseFloat()
解析,則會返回原始的值。
<!DOCTYPE html>
<html>
<head>
<title>Vue</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="//cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: "#app",
data: {
msg: 0
},
template: `
<div>
<div>Message is: {{ msg }}</div>
<div>Type is: {{ typeof(msg) }}</div>
<input v-model.number="msg" type="number">
</div>
`
})
</script>
</html>
當使用自定義組件時,在組件上的v-model
默認會利用名為value
的prop
和名為input
的事件,但是像單選框、複選框等類型的輸入控件可能會將value attribute
用於不同的目的,此時可以使用model
選項可以用來避免這樣的衝突。
<!DOCTYPE html>
<html>
<head>
<title>Vue</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="//cdn.bootcss.com/vue/2.4.2/vue.js"></script>
<script type="text/javascript">
Vue.component("u-input", {
model: {
prop: "message",
event: "input"
},
props: {
message: {
type: String
},
},
template: `
<div>
<input :value="message" @input="$emit('input', $event.target.value)">
</div>
`
})
var vm = new Vue({
el: "#app",
data: {
msg: ""
},
template: `
<div>
<div>Message is: {{ msg }}</div>
<u-input v-model="msg"></u-input>
</div>
`
})
</script>
</html>
分析
Vue
源碼的實現比較複雜,會處理各種兼容問題與異常以及各種條件分支,文章分析比較核心的代碼部分,精簡過後的版本,重要部分做出注釋,commit id
為ef56410
。
v-model
屬於Vue
的指令,所以從編譯階段開始分析,在解析到指令之前,Vue
的解析階段大致流程:解析模版字符串生成AST
、優化語法樹AST
、生成render
字符串。
// dev/src/compiler/index.js line 11
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options) // 生成AST
if (options.optimize !== false) {
optimize(ast, options) // 優化AST
}
const code = generate(ast, options) // 生成代碼 即render字符串
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
對指令的處理就在生成render
字符串的過程,也就是generate
函數的處理過程,在generate
中調用genElement -> genData -> genDirectives
,文章主要從genDirectives
函數進行分析。
// dev/src/compiler/codegen/index.js line 43
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`, // render字符串
staticRenderFns: state.staticRenderFns
}
}
// dev/src/compiler/codegen/index.js line 55
export function genElement (el: ASTElement, state: CodegenState): string {
// ...
data = genData(el, state)
// ...
}
// dev/src/compiler/codegen/index.js line 219
export function genData (el: ASTElement, state: CodegenState): string {
// ...
const dirs = genDirectives(el, state)
// ...
}
在生成AST
階段,也就是parse
階段,v-model
被當做普通的指令解析到el.directives
中,genDrirectives
方法就是遍歷el.directives
,然後獲取每一個指令對應的方法,對於v-model
而言,在此處獲取的是{name: "model", rawName: "v-model" ...}
,通過state
找到model
指令對應的方法model()
並執行該方法。
// dev/src/compiler/codegen/index.js line 309
function genDirectives (el: ASTElement, state: CodegenState): string | void {
const dirs = el.directives // 獲取指令
if (!dirs) return
let res = 'directives:['
let hasRuntime = false
let i, l, dir, needRuntime
for (i = 0, l = dirs.length; i < l; i++) { // 遍歷指令
dir = dirs[i]
needRuntime = true
const gen: DirectiveFunction = state.directives[dir.name] // 對於v-model來說 const gen = state.directives["model"];
if (gen) {
// compile-time directive that manipulates AST.
// returns true if it also needs a runtime counterpart.
needRuntime = !!gen(el, dir, state.warn)
}
if (needRuntime) {
hasRuntime = true
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${
dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
}
if (hasRuntime) {
return res.slice(0, -1) + ']'
}
}
model
方法主要是根據傳入的參數對tag
的類型進行判斷,調用不同的處理邏輯。
// dev/src/platforms/web/compiler/directives/model.js line 14
export default function model (
el: ASTElement,
dir: ASTDirective,
_warn: Function
): ?boolean {
warn = _warn
const value = dir.value
const modifiers = dir.modifiers
const tag = el.tag
const type = el.attrsMap.type
if (process.env.NODE_ENV !== 'production') {
// inputs with type="file" are read only and setting the input's
// value will throw an error.
if (tag === 'input' && type === 'file') {
warn(
`<${el.tag} v-model="${value}" type="file">:\n` +
`File inputs are read only. Use a v-on:change listener instead.`,
el.rawAttrsMap['v-model']
)
}
}
// 分支處理
if (el.component) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {
genSelect(el, value, modifiers)
} else if (tag === 'input' && type === 'checkbox') {
genCheckboxModel(el, value, modifiers)
} else if (tag === 'input' && type === 'radio') {
genRadioModel(el, value, modifiers)
} else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers)
} else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers)
// component v-model doesn't need extra runtime
return false
} else if (process.env.NODE_ENV !== 'production') {
warn(
`<${el.tag} v-model="${value}">: ` +
`v-model is not supported on this element type. ` +
'If you are working with contenteditable, it\'s recommended to ' +
'wrap a library dedicated for that purpose inside a custom component.',
el.rawAttrsMap['v-model']
)
}
// ensure runtime directive metadata
return true
}
genDefaultModel
函數先處理了modifiers
修飾符,其不同主要影響的是event
和valueExpression
的值,對於<input>
標籤event
為input
,valueExpression
為$event.target.value
,然後去執行genAssignmentCode
去生成代碼,以及添加屬性值與事件處理。
// dev/src/platforms/web/compiler/directives/model.js line 127
function genDefaultModel (
el: ASTElement,
value: string,
modifiers: ?ASTModifiers
): ?boolean {
const type = el.attrsMap.type
// warn if v-bind:value conflicts with v-model
// except for inputs with v-bind:type
// value與v-model衝突則發出警告
if (process.env.NODE_ENV !== 'production') {
const value = el.attrsMap['v-bind:value'] || el.attrsMap[':value']
const typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']
if (value && !typeBinding) {
const binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'
warn(
`${binding}="${value}" conflicts with v-model on the same element ` +
'because the latter already expands to a value binding internally',
el.rawAttrsMap[binding]
)
}
}
// 修飾符處理
const { lazy, number, trim } = modifiers || {}
const needCompositionGuard = !lazy && type !== 'range'
const event = lazy
? 'change'
: type === 'range'
? RANGE_TOKEN
: 'input'
let valueExpression = '$event.target.value'
if (trim) {
valueExpression = `$event.target.value.trim()`
}
if (number) {
valueExpression = `_n(${valueExpression})`
}
let code = genAssignmentCode(value, valueExpression)
if (needCompositionGuard) {
code = `if($event.target.composing)return;${code}`
}
addProp(el, 'value', `(${value})`)
addHandler(el, event, code, null, true)
if (trim || number) {
addHandler(el, 'blur', '$forceUpdate()')
}
}
// dev/src/compiler/directives/model.js line 36
export function genAssignmentCode (
value: string,
assignment: string
): string {
const res = parseModel(value)
if (res.key === null) {
return `${value}=${assignment}`
} else {
return `$set(${res.exp}, ${res.key}, ${assignment})`
}
}
每日一題
//github.com/WindrunnerMax/EveryDay
參考
//cn.vuejs.org/v2/api/#v-model
//www.jianshu.com/p/19bb4912c62a
//www.jianshu.com/p/0d089f770ab2
//cn.vuejs.org/v2/guide/forms.html
//juejin.im/post/6844903784963899400
//juejin.im/post/6844903999414485005
//segmentfault.com/a/1190000021516035
//segmentfault.com/a/1190000015848976
//github.com/haizlin/fe-interview/issues/560
//ustbhuangyi.github.io/vue-analysis/v2/extend/v-model.html