v-model
vue提供了一些好用的extend,比如keep-alive,slot,transition等等,使用频率也非常高。但是在使用过程中,往往会出现我们以为的bug,所以这一节有必要分析一下extend中的细节。先从最基础的v-model开始。
表单元素
在vue中,通过v-model来实现双向绑定,下面是一个使用v-model的input元素示例。
let vm = new Vue({
el: '#app',
template: '<div>'
+ '<input v-model="message" placeholder="edit me">' +
'<p>Message is: {{ message }}</p>' +
'</div>',
data() {
return {
message: ''
}
}
})从编译阶段分析,首先是parse阶段,v-model 会被当做普通的指令解析到 el.directives 中,在后面执行genData的时候,会执行const dirs = genDirectives(el, state),定义在src/compiler/codegen/index.js中。
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]
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.arg}"` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
}
if (hasRuntime) {
return res.slice(0, -1) + ']'
}
}它所做的就是遍历所有的元素el.directives,获取每一个指令对应的方法 const gen: DirectiveFunction = state.directives[dir.name],这个指令方法实际上是在实例化 CodegenState 的时候通过 option 传入的,这个 option 就是编译相关的配置,它在不同的平台下配置不同,在 web 环境下的定义在 src/platforms/web/compiler/options.js下:
export const baseOptions: CompilerOptions = {
expectHTML: true,
modules,
directives,
isPreTag,
isUnaryTag,
mustUseProp,
canBeLeftOpenTag,
isReservedTag,
getTagNamespace,
staticKeys: genStaticKeys(modules)
}directives 定义在 src/platforms/web/compiler/directives/index.js 中:
export default {
model,
text,
html
}那么对于 v-model 而言,对应的 directive 函数是在 src/platforms/web/compiler/directives/model.js 中定义的 model 函数:
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.`
)
}
}
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.'
)
}
// ensure runtime directive metadata
return true
}得到了指令函数,直接执行了needRuntime = !!gen(el, dir, state.warn),从我们已给出的实例中进入genDefaultModel。
else if (tag === 'input' || tag === 'textarea') {
genDefaultModel(el, value, modifiers)
}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
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'
)
}
}
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()')
}
}可以看到先处理了modifiers,其中的属性影响到valueExpression的设置,接着调用了genAssignmentCode来生成赋值代码。
/**
* Cross-platform codegen helper for generating v-model value assignment code.
*/
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})`
}
}parseModel的定义在同文件中,
export function parseModel (val: string): ModelParseResult {
// Fix https://github.com/vuejs/vue/pull/7730
// allow v-model="obj.val " (trailing whitespace)
val = val.trim()
len = val.length
if (val.indexOf('[') < 0 || val.lastIndexOf(']') < len - 1) {
index = val.lastIndexOf('.')
if (index > -1) {
return {
exp: val.slice(0, index),
key: '"' + val.slice(index + 1) + '"'
}
} else {
return {
exp: val,
key: null
}
}
}
}这里parseModel(value),value传递的是message返回的res.key为null,因此得到${value}=${assignment},也就是message=$event.target.value,回到上面的逻辑,继续命中了needCompositionGuard,所以最后的code是if($event.target.composing)return;message=$event.target.value,(这个composing属性用于指示输入框(例如<input>或<textarea>)是否处于“组成”状态。当event.target.composing为true时,开发人员可以选择暂时忽略input事件,直到输入法完成组成操作并最终确定输入的文字。这可以防止在用户输入过程中过早地对输入进行处理)。
code执行完成后,继续执行了input实现v-model最重要的部分,
addProp(el, 'value', `(${value})`)
addHandler(el, event, code, null, true)给el添加了一个prop,相当于在input上动态绑定了value,接着又添加了事件处理,相当于在input上绑定了input事件。所以这样实现了数据双向绑定。
<input
v-bind:value="message"
v-on:input="message=$event.target.value">再回到 genDirectives,它接下来的逻辑就是根据指令生成一些 data 的代码:
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.arg}"` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}对于上面给出的例子,最终生成的render是:
with(this) {
return _c('div',[_c('input',{
directives:[{
name:"model",
rawName:"v-model",
value:(message),
expression:"message"
}],
attrs:{"placeholder":"edit me"},
domProps:{"value":(message)},
on:{"input":function($event){
if($event.target.composing)
return;
message=$event.target.value
}}}),_c('p',[_v("Message is: "+_s(message))])
])
}组件
依旧给出一个示例:
let Child = {
template: '<div>'
+ '<input :value="value" @input="updateValue" placeholder="edit me">' +
'</div>',
props: ['value'],
methods: {
updateValue(e) {
this.$emit('input', e.target.value)
}
}
}
let vm = new Vue({
el: '#app',
template: '<div>' +
'<child v-model="message"></child>' +
'<p>Message is: {{ message }}</p>' +
'</div>',
data() {
return {
message: ''
}
},
components: {
Child
}
})从编译阶段分析,同上,在编译阶段都会解析v-model指令,接着执行 src/platforms/web/compiler/directives/model.js 中定义的 model 函数,并命中如下逻辑,
else if (!config.isReservedTag(tag)) {
genComponentModel(el, value, modifiers);
return false
}genComponentModel 函数定义在 src/compiler/directives/model.js 中:
export function genComponentModel (
el: ASTElement,
value: string,
modifiers: ?ASTModifiers
): ?boolean {
const { number, trim } = modifiers || {}
const baseValueExpression = '$$v'
let valueExpression = baseValueExpression
if (trim) {
valueExpression =
`(typeof ${baseValueExpression} === 'string'` +
`? ${baseValueExpression}.trim()` +
`: ${baseValueExpression})`
}
if (number) {
valueExpression = `_n(${valueExpression})`
}
const assignment = genAssignmentCode(value, valueExpression)
el.model = {
value: `(${value})`,
expression: `"${value}"`,
callback: `function (${baseValueExpression}) {${assignment}}`
}
}对于给出的例子来看的,生成的el.model值为:
el.model = {
callback:'function ($$v) {message=$$v}',
expression:'"message"',
value:'(message)'
}那么在 genDirectives 之后,genData 函数中有一段逻辑如下:
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}那么父组件最终生成的 render 代码如下:
with(this){
return _c('div',[_c('child',{
model:{
value:(message),
callback:function ($$v) {
message=$$v
},
expression:"message"
}
}),
_c('p',[_v("Message is: "+_s(message))])],1)
}然后在创建子组件 vnode 阶段,会执行 createComponent 函数,它的定义在 src/core/vdom/create-component.js 中:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// ...
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// ...
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// ...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}这里data.model有值,进入transformModel,
// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.props || (data.props = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
if (isDef(on[event])) {
on[event] = [data.model.callback].concat(on[event])
} else {
on[event] = data.model.callback
}
}transformModel就是给 data.props 添加 data.model.value,并且给data.on 添加 data.model.callback
data.props = {
value: (message),
}
data.on = {
input: function ($$v) {
message=$$v
}
}相当于在父组件里这样写
let vm = new Vue({
el: '#app',
template: '<div>' +
'<child :value="message" @input="message=arguments[0]"></child>' +
'<p>Message is: {{ message }}</p>' +
'</div>',
data() {
return {
message: ''
}
},
components: {
Child
}
})当子组件派发input事件时,父组件会在回调中修改message的值,value发生变化,子组件的input也更新。