写文章不容易,点个赞呗兄弟
专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
研究基于 Vue 版本 【2.5.17】
如果你觉得排版难看,请点击 下面链接 或者 拉到 下面 关注公众号 也可以吧
【Vue 原理】VModel – 源码版 之 表单元素绑定流程
今天讲解 v-model 的源码版。首先,兄弟,容我先说几句
v-model 涉及源码很多,篇幅很长,我都已经分了上下 三篇了,依然这么长,但是其实内容都差不多一样,但是我还是毫无保留地给你了。你知道我这篇文章写了多久,一个多星期啊,不是研究多久啊,是写啊写啊,不停地修修改改,一直在想如何才能讲明白
如果你做好了十足的学习准备,会对你事半功倍,如果你只是看看,请看白话版吧,不然估计会越看越烦 …..
如果你看过白话版,估计你会了解今天内容的大概,也能很快就入戏
今天讲解不同表单元素的 Vue 是如何处理的,表单元素有
input、textarea、select、checkbox、radio 五大种
所以,我们把每个表单元素当做一个模块,然后每个模块解决三个问题的流程,来开始我们今天的表演
1、v-model 如何绑定表单值
2、v-model 如何绑定事件
4、v-model 如何双向更新
TIP
下面所有涉及到的源码,为了方便理解,都是简化过的,因为源码太长,所以只保留主要思想,去掉了很多兼容处理以及错误处理
v-model 指令的处理
我们现在假设模板的解析已经到了 解析 v-model 的部分 ….
Vue 会调用 model 方法 来解析 v-model,这个方法里面,针对不同的表单元素,再调用不同的专属方法进行深度解析
function model(el, dir) {
var value = dir.value;
var tag = el.tag;
var type = el.attrsMap.type;
if (tag === 'select') {genSelect(el, value);
}
else if (tag === 'input' && type === 'checkbox') {genCheckboxModel(el, value);
}
else if (tag === 'input' && type === 'radio') {genRadioModel(el, value);
}
else if (tag === 'input' || tag === 'textarea') {genDefaultModel(el, value);
}
}
你也看到了,上面每种表单元素都会使用一个方法来特殊照顾,不过这些方法,作用大致一样
1、给表单元素设置绑定值
2、给表单元素设置事件及回调
所以这里,我们把方法的都设计到的方法以及流程说一下
插播上面的 el 是什么?
el 是 ast,而我的理解就是解析模板后,用树结构来表示某个 dom 节点,这里先不用深究,你就只要知道他是保存解析模板后所有的数据,包括你绑定的事件,绑定的指令,绑定的属性等等,一张图看下
下面所有的处理都是以 el 为基础的
表单元素设置绑定值
什么叫设置绑定值?
首先,比如你给表单元素设置 v-model =”name”,name 是 内部数据吧,所以要把 name 和 表单元素 两个紧紧绑定起来,方便后面进行双向更新
这里讲的是每个表单元素绑定值的流程
他们都会调用 addProp 去保存绑定的属性 然后 绑定属性,流程一样,所以提出来讲,但是具体绑定什么属性,每种元素都不尽相同,在下面表单元素模块会详解
1、调用 addProp,把 value 添加进 el.props
function addProp(el, name, value) {(el.props || (el.props = [])).push({name: name, value: value});
}
2、接下来的解析,el.props 会拼接成进字符串 domProps
function genData$2(el, state) {
var data = '{';
if (el.props) {data += "domProps:{" + (genProps(el.props)) + "},";
}
data = data.replace(/,$/, '') +'}';
return data
}
3、在插入 dom 之前,调用 updateDOMProps,把 上面保存的 domProps 遍历赋值到 dom 上
function updateDOMProps(oldVnode, vnode) {var props = vnode.data.domProps || {};
for (key in props) {cur = props[key];
if (key === 'value') {
elm._value = cur;
elm.value = strCur;
}
else {elm[key] = cur;
}
}
}
表单元素设置事件以及回调
这里讲的是每个表单元素绑定事件的流程
1、拼接事件
每种元素拼接事件都不一样,在下面表单元素模块会详解
2、保存事件名和拼接好的回调
每个元素的 event 事件 和 拼接的回调是不一样,但是他们保存的流程都是一样的,都会调用下面的方法,addHandler 去保存事件
下面 el 是 dom 元素,event 是事件名,code 是拼接的回调
function addHandler(el, name, value) {var events = el.events || (el.events = {});
var newHandler = {value: value.trim()
};
var handlers = events[name];
if (Array.isArray(handlers)) {important ? handlers.unshift(newHandler) : handlers.push(newHandler);
}
else if (handlers) {events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
}
else {events[name] = newHandler;
}
}
3、完善拼接回调
function genData$2(el) {
var data = '{';
if (el.events) {data += (genHandlers(el.events, false)) + ",";
}
data = data.replace(/,$/, '') +'}';
return data
}
genHandlers 遍历 el.event,每一项的回调最外层包上一层 function 字符串,并把 所有事件 逐个拼接成 on 字符串
function genHandlers(events) {
var res = 'on:{';
for (var name in events) {res += "\"" + name + "\":"+ ("function($event){"+ (events[name].value) +";}") +",";
}
return res.slice(0, -1) + '}'
}
转接的 初始数据和结果 像下面这样
4、绑定事件
在插入 dom 之前
会调用到 updateDOMListeners,把 上面保存到 on 的 所有事件,遍历绑定到 dom 上
updateDOMListeners 其实兜兜转转了很多方法 来处理,为了方便理解,已经非常简化,但是意思是不变的
尤大:卧槽,我写几百行,你浓缩成 5 行,你这是要向全国人民谢罪的啊
function updateDOMListeners(vnode) {for (name in vnode.data.on) {vnode.elm.addEventListener(event, handler);
}
}
下面所有例子使用这个 vue 实例,所有绑定 v-model 我都用 name
Input、Textarea
哟哟,看过 model,就知道 这两种元素是使用 genDefaultModel 处理的
function genDefaultModel(el, value, modifiers) {var code = "if($event.target.composing)return;"
+ value + '=$event.target.value;';
addProp(el, 'value', ("(" + value + ")"));
addHandler(el, "input", code, null, true);
}
绑定值
看了上面的函数,你就知道啦,input 和 textarea 调用 addProp 绑定的是 value
拼接事件
其实这里精炼就一句话,比 jio 简单
name = $event.target.value
但是呢!input 这里其实是很复杂的,比如兼容 range 啦,预输入延迟更新啦 等等,但是现在我们不说这些,放到下篇来讲
然后,你能看到,input 和 textarea 一般绑定的是 input 事件,但是也有其他的处理,下篇讲啦
编译后的渲染 render 函数
with(this) {
return _c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (name),
expression: "name"
}],
attrs: {"type": "text"},
domProps: {"value": (name)
},
on: {"input": function($event) {if ($event.target.composing) return;
name = $event.target.value;
}
}
})]
}
双向更新
我们可以看到上面的 render 执行的时候,从实例读取了 name,name 收集到 本组件 watcher
1、内部变化,通知更新 watcher,render 重新执行,获取新的 name,绑定到 dom 元素属性 value
2、外部变化,看上面的回调事件,可以知道直接把 $event.target.value 赋值给 内部值 name
Select
来看看 处理 select 的 genSelect 方法
function genSelect(el, value, modifiers) {
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')
})
${value} = '$event.target.multiple ? $$selectedVal : $$selectedVal[0]'
`
addHandler(el, 'change', code, null, true);
}
绑定值
select 元素绑定的属性是 selectedIndex,但是 select 并没有在 genSelect 方法中调用 addProp 绑定某个属性
那么 select 在哪里设置了呢?Vue 专门使用了方法 setSelected 设置 selectedIndex,这个方法现在不说,你只要知道,他是更新 selectedIndex 的就好了,后面会有一篇专门说
疑惑为什么 select 不像 input 一样直接绑定 value,这样,不是也可以确定选项吗?
按我的理解呢,我觉得应该是原始 select 的 value 只有字符串一类型的值,而 Vue 的 select 支持 数字和字符串两种类型的值啊
拼接事件
观察下面的渲染函数,就可以很清楚地名表,select 的回调是怎么一回事了
1、从所有 option 中 筛选出被选择的 option
2、使用数组保存所有筛选后的 option 的 value
3、判断是否多选,多选返回数组,单选返回数组第一项
然后,你还能知道 select 绑定的是 change 事件
献上 select 的渲染 render 函数
with(this) {
return _c('select', {
directives: [{
name: "model",
rawName: "v-model",
value: (name),
expression: "name"
}],
on: {"change": function($event) {
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
})
name = $event.target.multiple ? $$selectedVal: $$selectedVal[0];
}
}
})
}
双向更新
render 执行时,directive 处从实例读取了 name,name 收集到 本组件 watcher
1、内部变化,通知更新 watcher,上面 render 重新执行,获取新 name,于是更新 select 元素属性 selectedIndex,于是 select 当前选项就改变了
2、外部变化,直接赋值给 绑定值,绑定值变化,通知 watcher 更新,更新完,重新设置 selectedIndex
Checkbox
genCheckboxModel 源码奉上
function genCheckboxModel(el, value, modifiers) {
var valueBinding = el.value || 'null';
var trueValueBinding = el['true-value'] || 'true';
var falseValueBinding = 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&&(${value}=$$a.concat([$$v]))
}else{$$i>-1&&(${value}=$$a.slice(0,$$i).concat($$a.slice($$i+1)))
}
}else{${value} = $$c
}`,null, true
);
}
绑定值
赋值给 checked
看上面的方法就知道啦,调用 addProps,设置 checked 值
拼接事件
哈哈,还是看下面的渲染函数,看下 checkbox 的回调,其实意思就是
1、数组,分是否选择
a. 选择,把当前选项 concat 进数组
b. 取消选择,把当前选项 移除出数组
2、非数组,直接赋值
你还能知道 checkbox 绑定的是 change 事件
来看看 checkbox 的渲染 render 函数
with(this) {
return _c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (name),
expression: "name"
}],
attrs: {
"type": "checkbox",
"value": "1"
},
domProps: {
// _i 方法,作用是,判断第二个参数是否在 第一个参数数组中
"checked": Array.isArray(name) ? _i(name, "1") > -1 : (name)
},
on: {"change": function($event) {
var $$a = name,
$$el = $event.target,
$$c = $$el.checked ? (true) : (false);
if (Array.isArray($$a)) {
var $$v = "1",
$$i = _i($$a, $$v);
if ($$el.checked) {$$i < 0 && (name = $$a.concat([$$v]))
}
else {$$i > -1 && (name = $$a.slice(0, $$i).concat($$a.slice($$i + 1)))
}
}
else {name = $$c};
}
}
})
}
Radio
处理 radio 元素的 genRadioModel 源码
function genRadioModel(el, value) {
var valueBinding = el.value|| 'null';
addProp(el, 'checked',
("_q(" + value + "," + valueBinding + ")"));
addHandler(el, 'change',
`${value} = ${valueBinding}`, null, true);
}
怎么赋值
直接赋值给 checked,你看上面的方法调用 addProp 可以看到
拼接事件
这个真的更加简单了 … 比 input 还简单啊,都不用获取值,只是直接赋值为 radio 的值
name=”1″,1 是你设置给 radio 的 value 值
你还能知道 radio 绑定的是 change 事件
看下下面的 radio 的渲染函数你就懂了
with(this) {
return _c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (name),
expression: "name"
}],
attrs: {
"type": "radio",
"value": "1"
},
domProps: {
// _q 方法,作用是,判断两个参数是否相等
"checked": _q(name, "1")
},
on: {"change": function($event) {name = "1";}
}
})
}
双向更新
在 render 执行的时候,绑定值 收集到 本组件的 watcher
1、内部变化,通知更新 watcher,render 重新执行,获取新的 name,更新 radio 元素属性 checked
2、外部变化,直接赋值 更新 绑定值 name 等于 radio 元素属性 value