欢迎你
文章写作: Markdown Nice
作者:wangly
发布地址:掘金,语雀
声明:转载注明作者以及地址
这次也时髦的对文章样式进行了一些更改。希望大家能够喜欢绿绿的。
哈喽,大家好呀。我是wangly。 一名一年经验的前端老倒霉蛋了,前两篇文章非常感谢大家的支持,为了感谢大家,这次给掘友们带来了一篇关于Vue中经常使用到 v-model 指令的源码分析,充分的给大家说说,碰到类似的面试题和工作上碰到的问题扫盲。希望看完之后能对你有帮助。本篇文章需要有一定的基础,如果看不懂的话,反复品读,你会有一个成长。
为什么要看源码?
不看源码,我们只会知晓它表面的工作流程,而不知晓其内部的运转原理。就会有一种,知其然,而不知其所以然的感觉。当某天面试官问你这个东西的时候,你只能回答出它的使用流程,而 get 不到深度,就会给人一种模棱两可的感觉。千里执行,始于足下,跟着我一起探索它的奥秘吧。
劝退三连
- v-model 做了什么?
- v-model 在什么场景下能用,什么场景下不能用?
- v-model 解决了什么问题?
开始发车咯
1.入口函数
v-model本身是一个指令语法糖,来为input 和 指定的变量做一个双向绑定的过程,下面我们来看下model指令,它得到了什么东西。请看源码(这里使用打包后的代码, 更加清晰)
// model 函数
function model (
el,
dir,
_warn
) {
console.log(el)
console.log(dir)
console.log(_warn)
}
打印结果如下
打印出现的结果给各位截个图,其中:
- el 为
ASTElement
AST语法元素 - dir 为
ASTDirection AST指令
- _warn 为 一个警告函数
2.获取v-model元素需要用到的一些属性
下面的代码,主要是用来v-model
绑定的元素获取一些基本信息。
- value: 绑定
data
的属性名称。 - modifiers: 修饰符对象,如
v-model.lazy="msg"
的修饰符会生成一个对象, { lazy: true }表示lazy
修饰符存在。 - tag:
v-model
绑定的标签名称。 - type: 元素的
attribute
中的type类型
// 绑定`data`的属性名称
var value = dir.value;
// 修饰符列表
var modifiers = dir.modifiers;
//标签名称,
var tag = el.tag;
// 元素的类型
var type = el.attrsMap.type; // 标签类型
console.log(value, modifiers, tag, type)
3.当 input类型为file的时候
这里做了个判断,当input且类型是file
文件的话,则抛出一个警告。用来警示开发者。
{
// 类型为file的input是只读的,设置input的值可能会导致错误
if (tag === 'input' && type === 'file') {
warn$1(
// error 信息
"<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +
"File inputs are read only. Use a v-on:change listener instead.",
el.rawAttrsMap['v-model']
);
}
}
当我们做一个file
去使用v-model
的时候,控制台就直接打印了一条错误。
4.根据不同形式,做不同的处理
在Vue中,v-model
先判断,当前元素是标签还是组件,如果是组件,就调用genComponentModel
来去处理这个问题。组件v-model额外运行时,就返回。先对组件判断,在然后对原生标签做处理。如input
,select
,checkbox
等标签的双向绑定。下面给大家整理一下对应的处理方式吧。我想拆开来大家都能看懂。
- 组件: genComponentModel( el: ASTElement, value: string, modifiers: ?ASTModifiers)
- select下拉选择框:getSelect( el: ASTElement, value: string, modifiers: ?ASTModifiers)
- checkbox多选框: genCheckboxModel( el: ASTElement, value: string, modifiers: ?ASTModifiers)
- Radio单选按钮:genRadioModel( el: ASTElement, value: string, modifiers: ?ASTModifiers)
- input & textarea (默认Model处理):genDefaultModel( el: ASTElement, value: string, modifiers: ?ASTModifiers)
- 绑定的元素不支持v-model,则会提示错误。v-model不支持该元素。如下图
// 判断 el 是否是组件
if (el.component) {
genComponentModel(el, value, modifiers);
// component v-model doesn't need extra runtime
return false
} else if (tag === 'select') {
// 处理Select
genSelect(el, value, modifiers);
} else if (tag === 'input' && type === 'checkbox') {
// 处理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);
return false
} else {
warn$1(
// 如果不在处理范内,提示错误。v-model不支持该元素
"<" + (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']
);
}
默认处理方式genDefaultModel
genDefaultModel
主要是用来处理基本文本框和多选文本框。
处理实例:
在genDefaultModel
的第一句话,就是将el
的attribute
的type
值。因为其中有一个新加入的range
与其他的值是不一样的。需要额外做出处理
var type = el.attrsMap.type;
其次,需要判断v-bind:值与v-model是否冲突,如果冲突就会将错误添加到堆栈当中。所以我们在控制台可以看到冲突的提示
// 如果v-bind:值与v-model冲突,则发出警告
// 除了带有v-bind的输入:type
{
var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value']
var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']
if (value$1 && !typeBinding) {
var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'
warn$1(
binding +
'="' +
value$1 +
'" conflicts with v-model on the same element ' +
'because the latter already expands to a value binding internally',
el.rawAttrsMap[binding]
)
}
}
其次,在获取当前修饰符的状态去生成表达式,下面对modifiers
进行获取,如果modifiers
为undefined
的话,那么它就是一个空对象。
// 获取修饰符列表
var ref = modifiers || {}
// 懒加载修饰符
var lazy = ref.lazy
// 数字修饰符
var number = ref.number
// 空格过滤修饰符
var trim = ref.trim
// 在未打包下是这样的
const { lazy, number, trim } = modifiers || {}
当获取到了修饰符的状态后,下一步开始生成event
事件,因为其中有一些事件是vue自己定义的,比如:
// RANGE
export const RANGE_TOKEN = '__r'
// CHECK & RADIO
export const CHECKBOX_RADIO_TOKEN = '__c'
通过event,生成code代码模板。这里会对修饰符进行一个判定。默认的event
为input
,如果是lazy
的话就使用change
事件。不是的话对range
做判断。如果type是range的话就使用RANGE_TOKEN
反之则就是input
了。当生成了事件名后,根据不同的修饰符生成对应的value表现模板,通过genAssignmentCode
方法,获取代码字符串。
// 非懒加载进度条时候
const needCompositionGuard = !lazy && type !== 'range'
// event 事件名称
const event = lazy ? 'change' : type === 'range' ? RANGE_TOKEN : 'input'
// value模板。默认情况下,作为
let valueExpression = '$event.target.value'
if (trim) {
// trim事件
valueExpression = `$event.target.value.trim()`
}
if (number) {
// _n($event.target.value)
valueExpression = `_n(${valueExpression})`
}
// 获取code
let code = genAssignmentCode(value, valueExpression)
// 如果是range,那么需要对range的composing进行判断。
if (needCompositionGuard) {
code = `if($event.target.composing)return;${code}`
}
给出一个默认的实例,genAssignmentCode
默认两个参数,value
assignment
,我们可以看一下,它做了什么,有什么用?
// @ Function
export function genAssignmentCode (
value: string,
assignment: string
): string {
const res = parseModel(value)
![](https://imgkr.cn-bj.ufileos.com/783a46d8-0d4a-4ea7-9880-562b99f36f9d.png)
if (res.key === null) {
// value = xxxx
return `${value}=${assignment}`
} else {
// $set()方式
return `$set(${res.exp}, ${res.key}, ${assignment})`
}
}
在genAssignmentCode
方法中,调用了一个parseModel
方法。它的作用主要是做一个解析的过程,这里就不去做介绍了。和JSON.parse作用相同。转换前,转换后:
- 单独msg
- 对象中的msg
根据上图,我想你已经知道它的作用了。没错。用来获取当前绑定的数据模型。对属性和对象属性的做一个区分。因为我们都知道,对象属性更改有可能会丢失响应式,为了以防万一,所以才使用$set()
的方式。到了这里,我想你也应该知道genAssignmentCode
是用来干嘛的吧?一句话总结:
如果是属性,就返回
value = assignment
,如果是对象属性,就使用set('导出模型的exp', '导出模型的key', assignment)的方式。
导出后的code,除了range
需要经历过needCompositionGuard
的过滤。为code添加$event.target.composing
,这个其实是对输入法IME问题
的解决。防止非必要的软更新问题。
什么是IME问题:查看
当code
生成完毕后,那就开始对el进行改造,改造的过程分为两个方法addProp
,addHeader
。我们分别来看看下它做了什么吧。
addProp
addProp
方法主要是对el
的props
的属性添加,来看一下,addProp做了什么吧。
function addProp (el, name, value, range, dynamic) {
(el.props || (el.props = [])).push(rangeSetItem({ name: name, value: value, dynamic: dynamic }, range));
el.plain = false;
}
可以看到,它主要就是给props添加一些属性。看下图可以看到,el中props中数据更换为了传递进来的参数了。
addHeadle
addHeadle
主要是将上面生成的code模板,添加给元素的event
事件。如下图,可以看出,el的event
的下面的事件值做一个处理。这样在el中就会绑定一个事件。我们可以看成如下DOM:
// 转换前 <input type="text" v-model="msg">
// 转换后 <input type="text" :value="msg" @input="if(event.target.value">
genSelect下拉选择框
通过上面的默认事件,我想你对v-model的大致流程有了基本的概念,那么就来聊一聊下拉选择框的问题吧。相对于input默认的流程,select
的话就少了addProp
,只有一个addHeader
的方法。在一开始有一个selectVal
保存默认的val。可以根据下面代码,看下转换前,转换后的代码
// 源码
var number = modifiers && modifiers.number
// 默认数据
var selectedVal =
'Array.prototype.filter' +
'.call($event.target.options,function(o){return o.selected})' +
'.map(function(o){var val = "_value" in o ? o._value : o.value;' +
'return ' +
(number ? '_n(val)' : 'val') +
'})'
// 生成后的代码
Array.prototype.filter
.call($event.target.options, function (o) {
return o.selected
})
.map(function (o) {
var val = '_value' in o ? o._value : o.value
return val
})
其次是assignment
的代码模板,根据$event.target.multiple
来去判断究竟是?selectedVal
还是 ?selectedVal[0]
。
var assignment = '$event.target.multiple ? $selectedVal : $selectedVal[0]';
最后就是生成code,并且将code和el的methods绑定。
var code = "var ?selectedVal = " + selectedVal + ";";
code = code + " " + (genAssignmentCode(value, assignment));
addHandler(el, 'change', code, null, true);
这是最后生成绑定的code:
var $selectedVal = Array.prototype.filter
.call($event.target.options, function (o) {
return o.selected
})
.map(function (o) {
var val = '_value' in o ? o._value : o.value
return val
})
msg = $event.target.multiple ? $selectedVal : $selectedVal[0]
贴上genSelect的代码
function genSelect (
el: ASTElement,
value: string,
modifiers: ?ASTModifiers
) {
// 获取numver指令
const number = modifiers && modifiers.number
// selectVal函数模板
const selectedVal = `Array.prototype.filter` +
`.call($event.target.options,function(o){return o.selected})` +
`.map(function(o){var val = "_value" in o ? o._value : o.value;` +
`return ${number ? '_n(val)' : 'val'}})`
// assignment获取值
const assignment = '$event.target.multiple ? $selectedVal : $selectedVal[0]'
// 生成code
let code = `var $selectedVal = ${selectedVal};`
code = `${code} ${genAssignmentCode(value, assignment)}`
// 添加事件并将code模板加入进去
addHandler(el, 'change', code, null, true)
}
genCheckboxModel多选框
多选框的v-model
有了一个新的方法getBindingAttr
,那么这个方法是用来干什么的呢? 其实主要是用来处理v-bind
的数据。通过getAndRemoveAttr
来去数据对val进行处理,其中主要是对v-bind
和 : + msg
两种方式的数据处理,如下图:
getAndRemoveAttr
只会从数组attrsList
中删除attr,不会被processAttrs
处理。随后将el.attrsMap[name]
拿出来,
function getBindingAttr(el, name, getStatic) {
// 获取绑定的value(动态的)
var dynamicValue =
getAndRemoveAttr(el, ':' + name) || getAndRemoveAttr(el, 'v-bind:' + name)
// 根据value进行处理
if (dynamicValue != null) {
return parseFilters(dynamicValue)
} else if (getStatic !== false) {
var staticValue = getAndRemoveAttr(el, name)
if (staticValue != null) {
console.log(JSON.stringify(staticValue))
return JSON.stringify(staticValue)
}
}
}
随后就是添加Prop
Handle
的操作,这个参考上面的处理方式,做一些处理,处理后的event
会有一个change
事件,作为值修改的方法:
var $a = msg,
$el = $event.target,
$c = $el.checked ? true : false
if (Array.isArray($a)) {
var $v = '1',
$i = _i($a, $v)
if ($el.checked) {
$i < 0 && (msg = $a.concat([$v]))
} else {
$i > -1 && (msg = $a.slice(0, $i).concat($a.slice($i + 1)))
}
} else {
msg = $c
}
添加props和handle的源码,参考上面的分析。这里就不多做赘述,只要知道,往prop添加了什么?handle的方法是什么?内容是什么?
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
)
genRadioModel单选框
处理单选按钮的v-model就没有那么多的花花肠子,如果理解了上面checkbox和input的解析,对于radio
,就是获取bangding
的value
。随后做修饰符的处理。然后按照套路一般添加Prop
和事件handle
。
function genRadioModel(el, value, modifiers) {
// 获取修饰符
var number = modifiers && modifiers.number
// 绑定的value值
var valueBinding = getBindingAttr(el, 'value') || 'null'
// number修饰符和非number修饰符下的区别.生成value处理方式
valueBinding = number ? '_n(' + valueBinding + ')' : valueBinding
// 添加prop
addProp(el, 'checked', '_q(' + value + ',' + valueBinding + ')')
// 添加事件
addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true)
}
genComponentModel组件
最后一个就是组件的v-model
的绑定的了。首先,需要知道如何实现组件的v-model
,这里给一个基本的demo。
点击后:
<div id="app">
<my-component v-model="title"></my-component>
</div>
<script src="./dist/vue.js"></script>
<script>
Vue.component('my-component', {
template: `<div>
{{value}}
<button @click="handleInput">提交input</button>
</div>`,
props: ['value'],
methods: {
handleInput() {
this.$emit('input', '我触发了input emit'); //触发 input 事件,并传入新值
}
}
});
new Vue({
el: '#app',
data: {
title: '我是默认'
}
})
</script>
可以看到,当在组件中定义prop
存在value
的时候,将修改的值通过$emit
发布input
事件发布。从而可以通过v-model
来做一个双向绑定。那么我们探究下组件内的v-model
做了一些什么吧。
// 解构指令
const { number, trim } = modifiers || {}
// 基本value模板
const baseValueExpression = '$v'
let valueExpression = baseValueExpression
// trim下的模板语法
if (trim) {
valueExpression =
`(typeof ${baseValueExpression} === 'string'` +
`? ${baseValueExpression}.trim()` +
`: ${baseValueExpression})`
}
// number下的模板,执行了_n的代理方法toNumber
if (number) {
valueExpression = `_n(${valueExpression})`
}
// 获取code模板
const assignment = genAssignmentCode(value, valueExpression)
// 对el的model进行修改
el.model = {
value: `(${value})`,
expression: JSON.stringify(value),
callback: `function (${baseValueExpression}) {${assignment}}`,
}
其大部分都是在渲染code模板,为下面le
的model
的做准备。组件和元素标签不一样,所以组件的模板就没有addProps
, addHanndle
这两个步骤。取而代之的是是对el.model
的修改。
尾篇
本文所述的v-model
只是单独的源码分析,其实很多内容在渲染后的模板还是要从一开始开始,模板的渲染,如果不看其他的源码压根就不明白,举个例子:
number
修饰符下都会给默认的valueExpression
添加一个_n()
其实就是一个函数,那么这个函数是干什么的?
valueExpression = "_n(" + valueExpression + ")";
我们可以看到这个方法,其中_n
指向了toNumber
。
function installRenderHelpers(target) {
target._o = markOnce
// _n
target._n = toNumber
......
}
toNumber
只是做一个很简单的事情,将传入的字符串转换为Int
也就是number
,如果转换失败就返回原来的字符串。
function toNumber (val) {
var n = parseFloat(val);
return isNaN(n) ? val : n
}
总结
vue
的源码很长,很晦涩。很多人只是看了一些免费视频的分析,如:xxxxVue源码解析
。其实内容无非就是讲了一些vue响应式MVVM
浅显的概念,就觉得vue不过如此。殊不知,只是夜郎自大。精心啃读vue
的源码,会对工作中使用vue
出现的一些问题。快速的找到解决方案。本篇文章只是对v-model
的简单的理解。如果面试官问到你,如果你看完,说不定能够吹半小时呢。当然,具体深入,还需要去理解渲染的模板具体做了什么。本来是准备通篇详解。后面发现这样写的话就脱离了本文的范畴。属于离题,超纲。
所以,如果你觉得技术停滞不前,不妨将vue
反复细品。
如果对你有帮助可以评论
点赞
收藏
三连。
有意换坑:
学历:专科
经验: 一年
目标地:上海
,杭州
,深圳
,广州
薪资: 8K ~ 12K
欢迎远程boss拉我上岸。非外包。