前言
尝试编写 vModelCheckbox 指令对象
思考:如果你看懂官网的的例子和我写的那4个小例子,在介绍vModelCheckbox实现之前,思考下,如果让你来实现一个vModelCheckbox对象,你会怎么写?
例子展示
这个例子就是通过实现一个自定义的vModelCheckbox指令对象来达到双向绑定 可以狠狠的点我去看看
const vModelCheckbox = (() => {
const listener = (type = 'on') => {
return (el, evt, handler, useCapture = false) => {
if (el && evt && handler) {
el[type == 'on' ? 'addEventListener' : 'removeEventListener'](evt, handler, useCapture)
}
}
}
const on = listener('on')
const off = listener('off')
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasCustomValue = (vnode) => {
const props = vnode.props || {}
return hasOwnProperty.call(props, 'true-value') && hasOwnProperty.call(props, 'false-value')
}
const getCheckboxValue = (el, vnode) => {
const props = vnode.props || {}
if (hasCustomValue(vnode)) {
const trueValue = props['true-value'], falseValue = props['false-value']
return el.checked ? trueValue : falseValue
} else {
return el.checked
}
}
const setMapValue = (el, vnode) => {
const props = vnode['props'] || {}
el._mapData = new Map([[
props['true-value'] || true, true
], [
props['false-value'] || false, false
]])
}
const getMapValue = (el, key) => {
return el._mapData.get(key)
}
return {
created(el, binding, vnode, preVnode) {
const { value } = binding
on(el, 'change', el._handleEvt = (evt) => {
const fn = vnode['props'] && vnode['props']['onUpdate:modelValue']
const domValue = vnode['props']['value']
const checked = evt.target.checked
if (Array.isArray(value)) {
if (checked && !value.includes(domValue)) {
value.push(domValue)
fn && fn(value)
} else if (!checked && value.includes(domValue)) {
const i = value.findIndex(m => m === domValue)
if (i !== -1) {
value.splice(i, 1)
fn && fn(value)
}
}
} else if (value instanceof Set) {
if (checked) {
fn && fn(value.add(domValue))
} else if (!checked && value.delete(domValue)) {
fn && fn(value)
}
} else {
fn && fn(getCheckboxValue(el, vnode))
}
})
},
mounted(el, binding, vnode, preVnode) {
setMapValue(el, vnode)
},
beforeUpdate(el, binding, vnode, preVnode) {
setMapValue(el, vnode)
const { value, oldValue } = binding
const props = vnode['props']
const fn = props && props['onUpdate:modelValue']
if (Array.isArray(value)) {
el.checked = value.includes(props['value'])
} else if (value instanceof Set) {
el.checked = value.has(props['value'])
} else if (value !== oldValue) {
el.checked = getMapValue(el, value)
}
},
beforeUnmount(el) {
el._mapData = null
off(el, 'change', el._handleEvt)
}
}
})()
Vue.vModelCheckbox = vModelCheckbox
-
上面的代码不考虑代码写的咋样, 功能起码完成了。注意,引用类型的数据,回显时必须使用同一个引用数据,因为我内部使用了[].includes和set.has方法来判断的.
-
比起Vue3 v-molde="数组", 当数组的值是引用类型的数据时,Vue3 回显是不需要同一个引用对象,就能进行回显,但是使用 Vue3 v-molde="set实例",如果set实例的成员是引用数据时,回显时set实例的成员必须是同一个引用类型数据。这样Vue3 v-molde="set实例"和Vue3 v-molde="数组"的回显的使用姿势不保持一致了.
-
上面代码实现的思路
- 在created钩子中注册change事件,每次复选框选中或者不选中时,触发该事件,v-model="绑定值", 根据绑定值的类型求出value值然后赋值给绑定值
- 在beforeUpdate钩子中, 根据绑定值的类型,和vnode[props][value]的值求出是否被选中,然后赋值给el.checked
- 这样就达到了双向绑定
小栗子
小栗子就不贴代码了,但是想要理解源码,最好要看下,不然下面的实现,可能会看不懂.
关于本次源码讲解的的4个小例子
使用姿势都会了,那接下来看看尤大是如何实现的(看看那个使用姿势的问题到底是哪行代码引起的)
vModelCheckbox内部实现
vModelCheckbox源码 runtime-dom/src/directives/vModel.ts
export const vModelCheckbox: ModelDirective<HTMLInputElement> = {
created(el, _, vnode) {
el._assign = getModelAssigner(vnode) // 拿到 onUpdate:modelValue 函数
addEventListener(el, 'change', () => {
const modelValue = (el as any)._modelValue // 获取绑定的值eg: v-model="arr" 这里的 modelValue就是 arr
const elementValue = getValue(el) // 获取el的的value eg: <input v-model="arr" :value="{ name: 'xzw' }" /> 这里的elementValue就是 { name: 'xzw' }
const checked = el.checked // input checkbox 的选中状态
const assign = el._assign
if (isArray(modelValue)) { // v-model="arr"
const index = looseIndexOf(modelValue, elementValue) // 使用比较宽松的方式找出elementValue在modelValue的索引(这样,不是同一个引用对象,也能进行回显)
const found = index !== -1
if (checked && !found) { // 选中且没有找到
assign(modelValue.concat(elementValue)) // 合并elementValue到modelValue上,然后给arr赋值
} else if (!checked && found) { // 未选中且找到了
const filtered = [...modelValue]
filtered.splice(index, 1) // 删除已存在且未选中的
assign(filtered) // 给arr赋值
}
} else if (isSet(modelValue)) { // v-model="set实例"
const cloned = new Set(modelValue)
if (checked) { // 选中则添加(set实例不会重复添加)
cloned.add(elementValue)
} else { // 否则未选中则删除
cloned.delete(elementValue)
}
assign(cloned) // 给 set实例赋值
} else { // 处理原始值类型
assign(getCheckboxValue(el, checked)) // getCheckboxValue获取真实的value值后给绑定值赋值
}
})
},
// set initial checked on mount to wait for true-value/false-value
mounted: setChecked, // 回显
beforeUpdate(el, binding, vnode) {
el._assign = getModelAssigner(vnode) // 获取onUpdate:modelValue函数(为了每次更新都使用最新的绑定函数)
setChecked(el, binding, vnode) // 回显
}
}
function setChecked(
el: HTMLInputElement,
{ value, oldValue }: DirectiveBinding,
vnode: VNode
) {
// store the v-model value on the element so it can be accessed by the
// change listener.
;(el as any)._modelValue = value // 给el添加_modelValue属性 : eg: v-model="arr" 这里的 value 就是 arr
if (isArray(value)) { // 数组
el.checked = looseIndexOf(value, vnode.props!.value) > -1 // eg: <input v-model="arr" :value="{ name: 'xzw' }" /> 这里的vnode.props!.value就是 { name: 'xzw' }, 根据vnode.props!.value的值是否在value中,来进行回显
} else if (isSet(value)) { // set实例
el.checked = value.has(vnode.props!.value) // 根据set.has来判断(如果set的成员是引用对象,回显时必须同同一个引用对象才能回显,这就和上面数组的实现有差异了。)
} else if (value !== oldValue) { // 原始值类型
el.checked = looseEqual(value, getCheckboxValue(el, true)) // 根据a,b的判断是否相等再赋值el.checked
}
}
// retrieve raw value for true-value and false-value set via :true-value or :false-value bindings
function getCheckboxValue(
el: HTMLInputElement & { _trueValue?: any; _falseValue?: any },
checked: boolean
) {
const key = checked ? '_trueValue' : '_falseValue'
return key in el ? el[key] : checked
}
// retrieve raw value set via :value bindings
function getValue(el: HTMLOptionElement | HTMLInputElement) {
return '_value' in el ? (el as any)._value : el.value
}
-
上面的实现不难理解,和 '尝试编写 vModelCheckbox 指令对象' 的思路差不多。
-
注解都写在代码中了,所以不重复解释了,这里主要说下他和 '尝试编写 vModelCheckbox 指令对象' 实现的差异:
- getCheckboxValue 方法中的
_trueValue 和 _falseValue属性
,这2个属性何时给el添加上去的?
我们知道安装元素vnode的时候,会把vnode上的属性,class,style和事件都添加到创建好的el上.
[后面会单独写一篇关于vnode如何变成el,属性和事件如何添加上去的,这里不做过多介绍]
patchProp 源码地址
export const patchProp: DOMRendererOptions['patchProp'] = (
el,
key,
prevValue,
nextValue,
isSVG = false,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
) => {
switch (key) {
// special
case 'class':
patchClass(el, nextValue, isSVG)
break
case 'style':
patchStyle(el, prevValue, nextValue)
break
default:
if (isOn(key)) {
// ignore v-model listeners
if (!isModelListener(key)) {
patchEvent(el, key, prevValue, nextValue, parentComponent)
}
} else if (shouldSetAsProp(el, key, nextValue, isSVG)) {
patchDOMProp(
el,
key,
nextValue,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
)
} else {
// special case for <input v-model type="checkbox"> with
// :true-value & :false-value
// store value as dom properties since non-string values will be
// stringified.
if (key === 'true-value') {
;(el as any)._trueValue = nextValue
} else if (key === 'false-value') {
;(el as any)._falseValue = nextValue
}
patchAttr(el, key, nextValue, isSVG)
}
break
}
}
原来如此,难怪能拿到我们绑定的引用数据,而在'尝试编写 vModelCheckbox 指令对象'中,我是通过vnode['props']['value']来获取的,似乎也没有问题.
- getValue方法中的 _value属性又是何时添加上去的呢?
在上面的 patchProp 方法内部会调用 patchDOMProp 方法
patchDOMProp 源码
export function patchDOMProp(
el: any,
key: string,
value: any,
// the following args are passed only due to potential innerHTML/textContent
// overriding existing VNodes, in which case the old tree must be properly
// unmounted.
prevChildren: any,
parentComponent: any,
parentSuspense: any,
unmountChildren: any
) {
// ...
if (key === 'value' && el.tagName !== 'PROGRESS') {
// store value as _value as well since
// non-string values will be stringified.
el._value = value
const newValue = value == null ? '' : value
if (el.value !== newValue) {
el.value = newValue
}
return
}
// ...
}
原来还是在patchProp时添加的,这样就很方便拿到原始值或者true-value|false-value的值,而不必像'尝试编写 vModelCheckbox 指令对象'实现中通过setMapValue设值
和通过getMapValue来取值
.
-
在beforeUpdate钩子中,他的实现采用宽松的比较方式去比较的,有点类似鸭式辨型的思想,像鸭子一样嘎嘎叫且用2条腿行走的动物就认为它是鸭子.
而在'尝试编写 vModelCheckbox 指令对象'采用[].includes 和 set.has
方法来判断的宽松的比较代码,比较简单,就不黏贴了,可以点我去查看
总结
使用: v-model作用在 <input type="checkbox" v-mode="绑定值" />
绑定值可以是原始值变量
或者数组
或者set实例
实现:
- 在created钩子中注册change事件,事件触发后根据绑定值的类型求出value值然后赋值给绑定值
- 在beforeUpdate钩子中根据绑定值的类型和
vnode[props][value]
的值求出是否被选中然后赋值给el.checked - 从而达到双向绑定
下篇: Vue3疑问系列(5) — v-model(vModelRadio)指令是如何工作的?
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!