文章写做: Markdown Nice
做者:wangly
发布地址:掘金,语雀
声明:转载注明做者以及地址
此次也时髦的对文章样式进行了一些更改。但愿你们可以喜欢绿绿的。javascript
哈喽,你们好呀。我是wangly。 一名一年经验的前端老倒霉蛋了,前两篇文章很是感谢你们的支持,为了感谢你们,此次给掘友们带来了一篇关于Vue中常用到 v-model 指令的源码分析,充分的给你们说说,碰到相似的面试题和工做上碰到的问题扫盲。但愿看完以后能对你有帮助。本篇文章须要有必定的基础,若是看不懂的话,反复品读,你会有一个成长。前端
不看源码,咱们只会知晓它表面的工做流程,而不知晓其内部的运转原理。就会有一种,知其然,而不知其因此然的感受。当某天面试官问你这个东西的时候,你只能回答出它的使用流程,而 get 不到深度,就会给人一种模棱两可的感受。千里执行,始于足下,跟着我一块儿探索它的奥秘吧。vue
v-model自己是一个指令语法糖,来为input 和 指定的变量作一个双向绑定的过程,下面咱们来看下model指令,它获得了什么东西。请看源码(这里使用打包后的代码, 更加清晰)java
// model 函数
function model ( el, dir, _warn ) {
console.log(el)
console.log(dir)
console.log(_warn)
}
复制代码
打印结果以下web
打印出现的结果给各位截个图,其中:面试
ASTElement
AST语法元素
ASTDirection AST指令
下面的代码,主要是用来v-model
绑定的元素获取一些基本信息。express
data
的属性名称。
v-model.lazy="msg"
的修饰符会生成一个对象, { lazy: true }表示
lazy
修饰符存在。
v-model
绑定的标签名称。
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)
复制代码
这里作了个判断,当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
的时候,控制台就直接打印了一条错误。app
在Vue中,v-model
先判断,当前元素是标签仍是组件,若是是组件,就调用genComponentModel
来去处理这个问题。组件v-model额外运行时,就返回。先对组件判断,在而后对原生标签作处理。如input
,select
,checkbox
等标签的双向绑定。下面给你们整理一下对应的处理方式吧。我想拆开来你们都能看懂。编辑器
// 判断 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
的第一句话,就是将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)

if (res.key === null) { // value = xxxx return `${value}=${assignment}` } else { // $set()方式 return `$set(${res.exp}, ${res.key}, ${assignment})` } } 复制代码
在genAssignmentCode
方法中,调用了一个parseModel
方法。它的做用主要是作一个解析的过程,这里就不去作介绍了。和JSON.parse做用相同。转换前,转换后:
根据上图,我想你已经知道它的做用了。没错。用来获取当前绑定的数据模型。对属性和对象属性的作一个区分。由于咱们都知道,对象属性更改有可能会丢失响应式,为了以防万一,因此才使用$set()
的方式。到了这里,我想你也应该知道genAssignmentCode
是用来干吗的吧?一句话总结:
若是是属性,就返回
value = assignment
,若是是对象属性,就使用set('导出模型的exp', '导出模型的key', assignment)的方式。
导出后的code,除了range
须要经历过needCompositionGuard
的过滤。为code添加$event.target.composing
,这个实际上是对输入法IME问题
的解决。防止非必要的软更新问题。
什么是IME问题:查看
当code
生成完毕后,那就开始对el进行改造,改造的过程分为两个方法addProp
,addHeader
。咱们分别来看看下它作了什么吧。
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
主要是将上面生成的code模板,添加给元素的event
事件。以下图,能够看出,el的event
的下面的事件值作一个处理。这样在el中就会绑定一个事件。咱们能够当作以下DOM:
// 转换前 <input type="text" v-model="msg"> 复制代码// 转换后 <input type="text" :value="msg" @input="if(event.target.value"> 复制代码
经过上面的默认事件,我想你对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) } 复制代码
多选框的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
)
复制代码
处理单选按钮的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)
}
复制代码
最后一个就是组件的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拉我上岸。非外包。