背景
相信用过Vue的人都对v-model这个指令烂熟于心了,如果不知道,可以看看下面这段官方的描述。
你可以用
v-model指令在表单<input>、<textarea>及<select>元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但v-model本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。
而针对不同的元素v-model还会有不一样的处理,具体如下:
- 针对text 和 textarea 元素
<input v-model="model" type="text"/>
// 相当于
<input :value="model" @input="model = $event.target.value"/>
- 针对checkbox 和 radio
<input v-model="model" type="checkbox" />
// 相当于
<input :checked="model @change="model = $event.target.checked"/>
- 针对select
<select v-model="model" />
<option>A</option>
<option>C</option>
</select>
// 相当于
<select :value="model" @change="model = $event.target.value">
<option>A</option>
<option>C</option>
</select>
相信道理大家都懂,而我们这次要说的是checkbox。通常我们使用checkbox的时候,通过v-model绑定的数据可以分以下两种情况:
- 非数组的情况,checked的值就看v-model的值是true还是false。
- 如果v-model的值是数组,checked的值则看:value的值在不在数组里。
平常使用的时候也没想
它为毛这么智能,以前看源码看完就忘了,今天就让我们来带着问题找答案,来看一看它里面的🤏🏻细节,印象更深刻。
Vue的核心流程
众所周知,vue的核心流程大致上可以分成如下几步所示:
显然,我们在模板里面写的v-model要经过模板编译这一步,如红色箭头所示,所以我们要找的地方就是compiler的部分,而compiler的职责就是将模板编译成render函数。
编辑器部分的结构大致如下:
├── src
│ ├── compiler -------------------------- 编译器代码的存放目录
│ ├── ├── codegen ----------------------- 根据AST生成目标平台代码
│ ├── ├── parser ------------------------ 解析原始代码并生成AST
│ ├── index.js -------------------------- 入口
入口源码在 src/compiler/index.js,如下:
import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
`const ast = parse(template.trim(), options)`
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
实际上总的来说它做了三件事:
- parse,将模板字符串转换成ast。
- optimize,对第1步得到的ast进行优化。
- generate,根据ast生成render函数。
而在parse的时候,大致上是这么一个处理流程:
parseHtml -> 触发开始标签的处理钩子(start)-> 处理v-for、v-once、v-if等指令 -> 处理 -> 调用processAttr处理v-bind、v-on指令 -> 处理v-model和其他自定义指令
为了文章结构看起来更清晰,以下代码均经过简化,详细代码可自行看相关路径的文件,而且本文代码都加了比较详细的注释,相信阅读起来相对比较容易理解
- parseHTML,触发start钩子,路径
src/compiler/parser/index.js
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
`start (tag, attrs, unary) {
// arrts就是标签的属性,而v-model也是标签上的属性,自然也是在这处理了
// 省略...
closeElement(element);
},`
end () {
// 省略...
},
chars (text: string) {
// 省略...
},
comment (text: string) {
// 省略...
}
})
- closeElement,路径
src/compiler/parser/index.js
function closeElement (element) {
if (!inVPre && !element.processed) {
// 这一步继续处理v-model
`element = processElement(element, options)`
}
// 省略...
}
- processElement,路径
src/compiler/parser/index.js
export function processElement (
element: ASTElement,
options: CompilerOptions
) {
// 省略...
// 最后真正处理的地方
`processAttrs(element)`
return element
}
- processAttrs,路径
src/compiler/parser/index.js
function processAttrs (el) {
const list = el.attrsList
// 实际上arrtsList是如下结构的东西
//el.attrsList = [
// {
// name: 'v-model',
// value: 'model'
// },
// ....
//]
let i, l, name, rawName, value, modifiers, isProp, syncGen
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name
value = list[i].value
// 判断是否是指令 dirRE 正则用来匹配一个字符串是否以 `v-`、`@` 或 `:` 开头
if (dirRE.test(name)) {
// 省略...
if (propBindRE.test(name)) { // 设置.prop修饰符
// 省略...
} else if (modifiers) {
name = name.replace(modifierRE, '')
}
if (bindRE.test(name)) { // 处理v-bind指令
// 省略...
} else if (onRE.test(name)) { // 处理v-on指令
// 省略...
} else { // 处理其他指令,v-model也是在这里处理的的
name = name.replace(dirRE, '')
// parse arg
const argMatch = name.match(argRE)
const arg = argMatch && argMatch[1]
if (arg) {
name = name.slice(0, -(arg.length + 1))
}
// 在这里对v-model做处理了
`addDirective(el, name, rawName, value, arg, modifiers, list[i])`
}
} else { // 处理非指令属性
// 省略...
}
}
}
实际上处理完v-bind和v-on指令后,就剩下 v-text、v-html、v-show、v-cloak 以及 v-model这5个内置的指令和用户自定义的指令没被处理了,当处理v-model和其他剩余指令的时候,其实就是是往元素描述对象上增加了el.directives属性,用于后面生成render函数的时候使用。
以v-model="model"为例,addDirective最终生成了如下结构的东西:
el.directives = [
{
name: 'model',
rawName: 'v-model',
value: model,
arg: undefined,
modifiers: undefined
}
...
];
而这个东西有什么作用呢?其实做这个解析是为了给后面generate的时候用的。所以接下来,就到generate部分的源码了。
入口是src/compiler/codegen/index.js
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}}`,
staticRenderFns: state.staticRenderFns
}
genElement
export function genElement (el: ASTElement, state: CodegenState): string {
// 此处省略一堆if else
// 上面那一堆if else 里面的genXXX都是对应之前parse阶段的processXXX
// 作用是处理v-if v-for那些指令之类的,不是本文的重点,可以忽略,
} else {
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
// 而处理v-model的关键是如下这个genData
`data = genData(el, state)`
}
}
return code
}
// 省略...
}
genData
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// 看名字就知道是真正处理剩下那5个指令的地方了
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','
}
genDirectives
function genDirectives (el: ASTElement, state: CodegenState): string | void {
// 取出指令,这里就是刚刚parse阶段从标签属性那里解析出来的指令
const dirs = el.directives
//el.directives = [
// {
// name: 'model',
// rawName: 'v-model',
// value: model,
// arg: undefined,
// modifiers: undefined
// }
// ...
//];
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
// state是方法的入参,里面保存着相关指令的处理方法,
// 比如v-html,就调用html方法处理,其他类似
// 而这个state怎么来的呢,其实是从编译选项生成的,比如web平台,就用web平台相关的处理函数
// 如果是weex,就用它相关的处理函数
// state的结构如下
// state = {
// ...
// directives: {
// html: function() {}
// text: function() {}
// model: function() {}
// ...
// }
// ...
// }
// 因此,对于v-model,这里的state.direcitves[dir.name]就相当于
// state.directives['model']
`const gen: DirectiveFunction = state.directives[dir.name]`
// 省略...
}
}
因为我们是web平台,低啊用的是web平台的model方法,接下来我们移步到platform相关的目录
├── src
│ ├── platform -------------------------- 存放平台相关的代码
│ ├── ├── web --------------------------- web平台
│ ├── ├── ├── compiler ------------------ web平台编译相关
│ ├── ├── ├── ├── directives ------------ 平台相关的指令的处理方法
│ ├── ├── ├── ├── ├── html.js ----------- 处理v-html
│ ├── ├── ├── ├── ├── index.js ---------- 入口,统一导出
│ ├── ├── ├── ├── ├── model.js ---------- 处理v-model,就是我们要找的地方
│ ├── ├── ├── ├── ├── text.js ----------- 处理v-text
│ ├── ├── weex -------------------------- weex
显然,model.js就是我们要找的地方,路径src/platform/web/compiler/directives/model.js
到这里就跟我们最开头那段官方的描述一样了,针对不同的元素做不同的处理,本次我们看的是针对checkbox的处理,其他元素甚至更简单,可自行分析
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 (el.component) { // 处理自定义组件上的v-model
genComponentModel(el, value, modifiers)
return false
} else if (tag === 'select') { // 处理select上的v-model
genSelect(el, value, modifiers)
} else if (tag === 'input' && type === 'checkbox') {
// 本文的重点,找的好苦,处理checkbox上的v-model
`genCheckboxModel(el, value, modifiers)`
} else if (tag === 'input' && type === 'radio') { // 处理radio上的v-model
genRadioModel(el, value, modifiers)
} else if (tag === 'input' || tag === 'textarea') { // input和textarea上的v-model
genDefaultModel(el, value, modifiers)
} else if (!config.isReservedTag(tag)) { // 处理非保留标签上的v-model
genComponentModel(el, value, modifiers)
return false
}
// 省略...
return true
}
接下来重点看genCheckboxModel,主角终于登场了
function genCheckboxModel (
el: ASTElement,
value: string,
modifiers: ?ASTModifiers
) {
const number = modifiers && modifiers.number
// 获取:value v-bind:value的值
// 比如<input :value="xxx" /> 这个方法就可以拿到xxx
const valueBinding = getBindingAttr(el, 'value') || 'null'
const trueValueBinding = getBindingAttr(el, 'true-value') || 'true'
const falseValueBinding = getBindingAttr(el, 'false-value') || 'false'
addProp(el, 'checked',
`Array.isArray(${value})` +
`?_i(${value},${valueBinding})>-1` + (
trueValueBinding === 'true'
? `:(${value})`
: `:_q(${value},${trueValueBinding})`
)
)
addHandler(el, 'change',
`var $$a=${value},` +
'$$el=$event.target,' +
`$$c=$$el.checked?(${trueValueBinding}):(${falseValueBinding});` +
'if(Array.isArray($$a)){' +
`var $$v=${number ? '_n(' + valueBinding + ')' : valueBinding},` +
'$$i=_i($$a,$$v);' +
`if($$el.checked){$$i<0&&(${genAssignmentCode(value, '$$a.concat([$$v])')})}` +
`else{$$i>-1&&(${genAssignmentCode(value, '$$a.slice(0,$$i).concat($$a.slice($$i+1))')})}` +
`}else{${genAssignmentCode(value, '$$c')}}`,
null, true
)
}
上述代码其实就只做了两件事:
- addProp,给元素增加checked属性,也就是说当你在checkbox上使用v-model的时候,最后转换出来的标签是这样子的
<input type="checkbox" checked="xxx" />,而这个checked的值又是怎么来的呢?我们先将addProp后面的那坨字符串稍微翻译一下,变成下面的样子就可以理解了
// 上面那坨字符串等价于下面的代码
// 如果v-model绑定的是数组,则看:value的值在不在数组里,在则checked=true
// 如果v-model的值不是数组,则checked = :value的值
if (Array.isArray(value)) {
// looseIndexOf就是_i那个函数,意思是在找出valueBinding在value数组中的索引
return looseIndexOf(value, valueBinding) > -1;
} else {
if(trueValueBinding === 'true') {
return value;
}
return looseEqual(value, trueValueBinding);
}
- addHandler,给元素添加事件,也就是说当你在checkbox上使用v-model的时候,最后转换出来的标签是这样子的
<input type="checkbox" onchange="handleChange">,而这个change的处理函数又是什么样子的呢?我们也还是先将addHandler后面的那坨字符串翻译一下,变成下面的样子
// onchange绑定的方法是这样的
function change($event) {
var $$a = value; // 这个value就是v-model绑定的值
var $$el = $event.target;
var $$c = $$el.checked ? trueValueBinding : falseValueBinding;
// 如果v-model绑定的是数组
if (Array.isArray($$a)) {
// 取出:value的值 也就是标签本身value的值
var $$v = valueBinding;
// 看标签本身value的值在不在v-model绑定的数组里
var $$i = looseIndexOf($$a, $$v); // looseIndexOf(value, valueBinding) 返回在v-model数组中的索引
// checkbox选中的情况
if ($$el.checked) {
// value的值不在v-model绑定的那个数组里
if ($$i<0) {
// 将这个checkbox的value添加到v-model绑定的数组里
genAssignmentCode(value, '$$a.concat([$$v])');
// 实际上就是 model = value.concat([valueBinding]);
}
} else { // checkbox未选中的情况
// value的值在v-model绑定的那个数组里
if ($$i > -1) {
// 将这个checkbox的value从v-model绑定的数组里移除
genAssignmentCode(value, '$$a.slice(0,$$i).concat($$a.slice($$i+1))');
// 实际上就是 model = value.slice(0, $$i).concat(value.slice($$i+1));
}
}
// 如果v-model绑定的不是数组
} else {
// 直接赋值 value = $$c,这个$$c就是当前的选中状态
genAssignmentCode(value, '$$c')
// 实际上就是 model = true | false
}
}
看完这里,就已经首尾呼应上了,大家可以回想一下一开始描述的问题,为毛这个checkbox上的v-model这么智能,相信大家已经找到答案了,如果还没有,多看两遍。