上一篇:Vue2.x 源码 - event
v-model
是 Vue 提供的双向绑定(数据驱动 DOM,DOM 变化影响数据)的全局指令,可以作用在表单元素上以及组件上;
1、在v-model
的parse
解析阶段,它会在processElement
方法中调用processAttrs
来处理标签上面解析的各种属性;在 src/compiler/parser/index.js
文件中:
export const dirRE = process.env.VBIND_PROP_SHORTHAND
? /^v-|^@|^:|^\.|^#/
: /^v-|^@|^:|^#/
const argRE = /:(.*)$/
function processAttrs (el) {
const list = el.attrsList
let i, l, name, rawName, value, modifiers, syncGen, isDynamic
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
if (dirRE.test(name)) {
el.hasBindings = true
// modifiers省略代码
if (bindRE.test(name)) {
// v-bind省略代码
} else if (onRE.test(name)) {
// v-on省略代码
} else {
// normal directives
name = name.replace(dirRE, '')
// parse arg
const argMatch = name.match(argRE)
let arg = argMatch && argMatch[1]
isDynamic = false
if (arg) {
name = name.slice(0, -(arg.length + 1))
if (dynamicArgRE.test(arg)) {
arg = arg.slice(1, -1)
isDynamic = true
}
}
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
if (process.env.NODE_ENV !== 'production' && name === 'model') {
checkForAliasModel(el, value)
}
}
} else {
// ...省略代码
}
}
}
processAttrs
方法首先使用dirRE
正则表达式把v-model
字符串中的v-
前缀去掉,此时name
的值就变成了model
;紧接着,它又使用了argRE
正则表达式来匹配指令参数,处理完毕后,调用addDirective
方法,给ast
对象添加directives
属性:
export function addDirective (
el: ASTElement,
name: string,
rawName: string,
value: string,
arg: ?string,
isDynamicArg: boolean,
modifiers: ?ASTModifiers,
range?: Range
) {
(el.directives || (el.directives = [])).push(rangeSetItem({
name,
rawName,
value,
arg,
isDynamicArg,
modifiers
}, range))
el.plain = false
}
2、接下来在codegen
代码生成阶段,会在genData
方法中调用genDirectives
来处理指令,在 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) {
// 操作AST的编译时指令,如果它也需要运行时对应的对象,则返回true
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) + ']'
}
}
genDrirectives
⽅法就是遍历 el.directives
,然后获取每⼀个指令对应的⽅法 state.directives[dir.name]
,这个指令⽅法实际上是在实例化 CodegenState
的时候通过 option
传⼊的,这个 option
就是编译相关的配置,它在不同的平台 下配置不同,在 web 环境下的定义在 src/platforms/web/compiler/options.js
下:
import directives from './directives/index'
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.`,
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
}
在model
方法中,首先判断如果在type='file'
的input
标签上使用了v-model
,那么会在开发环境提示错误信息,因为附件是只读的;随后根据标签类型,分别调用对应的方法,v-model
指令会命中genDefaultModel
;
function genDefaultModel (
el: ASTElement,
value: string,
modifiers: ?ASTModifiers
): ?boolean {
const type = el.attrsMap.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',
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()')
}
}
genDefaultModel
函数先处理了 modifiers
,它的不同主要影响的是 event
和 valueExpression
的值;然后去执⾏ genAssignmentCode
去⽣成代码,它的定义在 src/compiler/directives/model.js
中:
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})`
}
}
方法⾸先对 v-model
对应的 value
做了解析,最后根据res.key
来拼接并返回不同的 code
;
code
⽣成完后,⼜执⾏了 2 句⾮常关键的代码:
addProp
:调用addProp
是为了给ast
添加一个值为value
的props
属性;
addHandler
:调用addHandler
是为了给ast
添加一个事件监听,至于到底监听什么事件取决于v-model
作用于什么标签;
从以上分析中我们可以看出来:v-model处理双向绑定,本质上就是一种语法糖,它负责监听用户的输入事件然后更新数据,并对一些极端场景做了一些特殊处理;
回到 genData
方法中,执行完 genDirectives
之后,当前ast
对象中多了props
和events
属性;
v-model
可以绑定的表单元素有 select 、input、textarea、radio、checkbox
,在上面介绍 v-model 解析 的时候基本上就已经包括了这部分的内容,这里就根据具体表单元素说明一下它的流程(以 input、textarea 为例);
1、parse
解析阶段将 v-model
指令保存到 ast
的 directive
数组中;
2、在codegen
代码生成阶段,调用genData
来处理directives
指令、props
属性以及events
事件;
1、首先调用 genDirectives 方法来处理指令,在这个方法中会调用一个与平台相关的 model 方法,在这个 model 方法中会根据不同元素标签的类型来分别处理;
2、然后调用 genDefaultModel 方法,它主要做四件事情:异常处理、修饰符处理、添加 props 属性以及添加 event 事件;
父组件
<template>
<child v-model='msg'></child>
</template>
<script>
import child from './child.vue';
export default {
components:{ child };
data(){
return {
msg:0
}
}
}
</script>
子组件
<template>
<div>
<el-input :value='msg' @input='change($event)'></el-input>
</div>
</template>
<script>
export default {
name:'child',
props:{
msg:[Number,String]
},
methods:{
change(e){
this.$emit('input',e);
}
}
}
</script>
1、在父组件编译阶段,会解析 v-model
指令,依然会执⾏ genData
函数中的 genDirectives
函数接着执⾏src/platforms/web/compiler/directives/model.js
中定义的 model
函数,并命中如下逻辑:
if (el.component) {
genComponentModel(el, value, modifiers)
//组件v-model不需要额外的运行
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: JSON.stringify(value),
callback: `function (${baseValueExpression}) {${assignment}}`
}
}
调用genComponentModel
方法后,当前ast
对象多了一个model
属性;
2、然后在创建⼦组件 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 {
// ...省略代码
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// ...省略代码
}
当执行到createComponent
方法的时候,由于我们存在model
属性,所以会调用transformModel
来处理这部分的逻辑,我们来看一下transformModel
这个方法的代码:
function transformModel (options, data: any) {
const prop = (options.model && options.model.prop) || 'value'
const event = (options.model && options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {
if (
Array.isArray(existing)
? existing.indexOf(callback) === -1
: existing !== callback
) {
on[event] = [callback].concat(existing)
}
} else {
on[event] = callback
}
}
transformModel
逻辑很简单,给 data.props
添加 data.model.value
,并且给 data.on
添加 data.model.callback
;
回到最上面的父子组件传值案例:
1、父组件通过 prop 传值给子组件;
2、子组件通过 input 事件出发子组件值的变化,然后调用 $emit 传递 input 事件将值传递给父组件;
3、父组件通过 imput 事件来接收子组件传递过来的值;
.number和.trim修饰符
对于.number
修饰符和.trim
修饰符的处理非常简单,在genDefaultModel
方法中其逻辑如下(其它地方处理过程类似):
const { number, trim } = modifiers || {}
let valueExpression = '$event.target.value'
if (trim) {
valueExpression = `$event.target.value.trim()`
}
if (number) {
valueExpression = `_n(${valueExpression})`
}
当提供了.number修饰符时,使用了_n工具函数进行包裹,_n工具函数就是toNumber方法的缩写形式;
.lazy修饰符
lazy
的作用是:在默认情况下,v-model
在每次 input
事件触发后将输入框的值与数据进行同步。你可以添加 lazy
修饰符,从而转为在 change
事件之后进行同步;
在codegen
代码生成后,它们生成的render
函数on
事件部分分别如下:
// normal
const normalRender = `
on:{
input:function($event){
if($event.target.composing)return;
msg=$event.target.value
}
}
`
// lazy
const lazyRender = `
on:{
change:function($event){
msg=$event.target.value
}
}
`
正如官网介绍中的那样,使用lazy修饰符后,它由监听input事件变成了监听change事件;
v-model 的实现需要一个属性和一个事件监听;它的本质是一种语法糖,可以支持原生表单元素,也可以支持自定义组件;
下一篇:Vue2.x 源码 - ref 和 $refs添加链接描述