前言
本文来聊聊 “
<select v-model="variable"></select>
” 其内部又是如何工作的?看这篇文章前,一定要把官网中的 表单输入绑定[选择框 (Select)例子看懂] 官网教程
尝试编写 vModelSelect 指令对象
根据前面几篇文章的讲解,其实双向绑定原理的套路都一样,加上上一篇也讲解了looseEqual.ts
的实现.
所以这次就不编写 vModelSelect
指令对象了, 因为比较简单.
小栗子
如果你已经看完官网的例子,那我建议你看下我写的7个小例子
- should work with single select
- multiple select (model is Array)
- v-model.number should work with select tag
- v-model.number should work with select tag
- multiple select (model is Array, option value is object)
- multiple select (model is Set)
- multiple select (model is Set, option value is object)
这7个小例子其实是vue-next单测中的,怕有的同学看不懂,所以改写成大家一眼就能看明白的例子。
你可以狠狠的点我去看看
vModelSelect 内部实现
vModelSelect源码 runtime-dom/src/directives/vModel.ts
export const vModelSelect: ModelDirective<HTMLSelectElement> = {
created(el, { value, modifiers: { number } }, vnode) { // eg: <select multiple v-model="value"></select>`
const isSetModel = isSet(value) // value 是不是 set实例
addEventListener(el, 'change', () => { // el 监听 change 事件
const selectedVal = Array.prototype.filter
.call(el.options, (o: HTMLOptionElement) => o.selected) // 拿到选中的 option dom
.map(
(o: HTMLOptionElement) =>
number ? toNumber(getValue(o)) : getValue(o) // 获取到 option dom 上绑定的value值
)
el._assign( // 调用 onUpdate:modelValue函数 给 value 赋值
el.multiple
? isSetModel
? new Set(selectedVal)
: selectedVal
: selectedVal[0]
)
})
el._assign = getModelAssigner(vnode) // 拿到 onUpdate:modelValue函数
},
// set value in mounted & updated because <select> relies on its children
// <option>s.
mounted(el, { value }) { // 元素安装都是先子后父,所以需要在 mounted钩子中
setSelected(el, value) // 反选
},
beforeUpdate(el, _binding, vnode) {
el._assign = getModelAssigner(vnode) // 更新前 重新获取 onUpdate:modelValue函数
},
updated(el, { value }) {
setSelected(el, value) // 反选
}
}
function setSelected(el: HTMLSelectElement, value: any) {
const isMultiple = el.multiple // select dom 是否含有 multiple 属性
if (isMultiple && !isArray(value) && !isSet(value)) { // 有的话绑定的值只能是数组或者set实例
__DEV__ &&
warn(
`<select multiple v-model> expects an Array or Set value for its binding, ` +
`but got ${Object.prototype.toString.call(value).slice(8, -1)}.`
)
return
}
for (let i = 0, l = el.options.length; i < l; i++) { // 遍历select下的所有option dom
const option = el.options[i] // 当前的 option dom
const optionValue = getValue(option) // 拿到 当前的 option dom 上绑定的值
if (isMultiple) { // 多选
if (isArray(value)) { // 数组 eg: v-model="value" value是array
option.selected = looseIndexOf(value, optionValue) > -1 //找出索引判断结果
} else { // set实例 eg: v-model="value" value是new Set()实例
option.selected = value.has(optionValue) // 判断当前option的value值是否在value中,进行反选
}
} else {
if (looseEqual(getValue(option), value)) { // 不是多选的话,直接根据比较结果给select.selectedIndex赋值(比如select 下有2个option,当 select.selectedIndex = 1时,第二个option dom就会被选中)
el.selectedIndex = i
return
}
}
}
if (!isMultiple) { // 单选时 且没有选中任何options则把select.selectedIndex置为-1(就是select没选中)
el.selectedIndex = -1
}
}
// retrieve raw value set via :value bindings
function getValue(el: HTMLOptionElement | HTMLInputElement) {
return '_value' in el ? (el as any)._value : el.value
}
-
逐行解释都写在代码中,不是考虑到有的同学看不懂,我真不想写注解
看源码其实只要明白其思路即可,这样自己就能根据思路去实现,哪天源码因修改bug而改变了点,那难道就慌了? -
vModelSelec 实现原理
- 模板编译
<select v-model="variable"></select> 会编译成如下: (function anonymous() { const _Vue = Vue return function render(_ctx, _cache) { with(_ctx) { const { vModelSelect: _vModelSelect, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue return _withDirectives((_openBlock(), _createBlock("select", { "onUpdate:modelValue": $event => (variable = $event) }, null, 8 /* PROPS */ , ["onUpdate:modelValue"])), [ [_vModelSelect, variable] ]) } } })
<select multiple v-model="variable"></select> 会编译成如下: (function anonymous() { const _Vue = Vue return function render(_ctx, _cache) { with(_ctx) { const { vModelSelect: _vModelSelect, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue return _withDirectives((_openBlock(), _createBlock("select", { multiple: "", "onUpdate:modelValue": $event => (variable = $event) }, null, 8 /* PROPS */ , ["onUpdate:modelValue"])), [ [_vModelSelect, variable] ]) } } })
上面的结果有共同的特点,就是指令最终会编译成使用
withDirectives
调用,其次v-model
编译后,还多一个"onUpdate:modelValue": $event => (variable = $event)
,其实看到这里,就算不看vModelSelect源码,我自己也能实现vModelSelect指令对象了.-
为了更好的理解原理,我们先来约定几个
关键词
,不然,我解释的时候,可能听不懂我在说啥eg: <select v-model="variable"> <option value="foo">foo</option> <option value="bar">bar</option> </select>`
a.
绑定值
: 响应式变量 variableb.
onUpdate:modelValue
: 模板编译生成的,它是一个函数$event => (variable = $event)
c.
select dom
: select 标签元素d.
option doms
: select 标签元素下所有的option标签元素e.
option dom
: select 标签元素下的某个option标签元素f.
option dom value
:比如<option value="bar">bar</option>他的value就是 bar值
-
先思考下什么是双向绑定?(下面是我针对
<select v-model="variable"></select>
使用的解释)a. 当用户选择
option dom
触发change事件时,会把option dom value
赋值给绑定值
b.
绑定值
数据发生变化后,会对option doms
进行反选c. 我就是这么理解的
-
看看上面尤大写的vModelSelect是不是这个思路:
a: created钩子中给
select dom
注册了change事件,当用户选择时会触发回调。回调中会根据option doms
遍历筛选出选中option dom
的option dom value
,然后调用onUpdate:modelValue
函数给绑定值
赋值。b. mounted和updated钩子中调用setSelected方法进行反选。setSelected方法中主要是遍历
option doms
,遍历过程中如果是多选则根据option dom value
是否在绑定值
中,然后给option dom
的selected赋值结果,从而达到多选;如果不是多选,则调用looseEqual(option dom value, 绑定值)
得出结果,然后给select dom
的selectedIndex赋值索引,从而达到单选。c.尤大的实现和上面思考的什么是双向绑定,其实是差不多的。
总结
其实vModelText
vModelCheckbox
vModelRadio
vModelSelect
的实现思路都一样,只是实现的时候注册的事件和el 反选的属性不太一样,反选的条件都是调用looseEqual.ts
中的宽松比较方法。
下篇: Vue3疑问系列(8) — 组件上使用v-model, 是如何工作的?