前言
尝试编写 vModelText 指令对象
提醒:如果你看懂官网的那2个例子,且对render函数有点了解,那下面这个例子可以先看下.
例子展示
这个例子就是通过实现一个自定的vModelTex指令对象来达到双向绑定 可以狠狠的点我去看看
const vModelText = (() => {
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')
return {
created(el, binding, vnode) {
const { number, trim, lazy } = binding.modifiers
on(el, lazy ? 'change' : 'input', el._handleEvt = (e) => {
let target = e.target, domValue = target.value
if (trim) {
domValue = domValue.trim()
} else if (number || el.type == 'number') {
domValue = isNaN(parseFloat(domValue)) ? domValue : parseFloat(domValue)
}
const fn = vnode['props'] && vnode['props']['onUpdate:modelValue']
if (fn) fn(domValue)
})
},
beforeUpdate(el, binding) {
const { number, trim, lazy } = binding.modifiers
if (el.value !== binding.value) {
el.value = binding.value
}
},
beforeUnmount(el, { modifiers: { lazy } }) {
off(el, lazy ? 'change' : 'input', el._handleEvt)
}
}
})()
例子实现讲解
<input v-model="value" />
- 双向绑定的实现原理(下面只是input[type为text和number和range]、textarea的实现原理):
- 在指令created钩子中, 监听input元素的input事件;
- 用户输入时当值发生变化,触发input事件,重新赋值给响应式value变量;
- 当响应式值value改变时,调用指令的beforeUpdate钩子,这个钩子函数中把响应式value的值赋值给input这个dom元素;
- 上面例子的代码就不细讲(举这个例子就是想让大家养成看源码前,先学会思考,如果是你来实现vModelText,你会怎么想),瞅瞅就好,下面才开始进入正题.
小栗子
- 本次举的小例子, 不是源码中的单侧,而是我编写的小例子,为了让大家更直观的去理解.
<div id="app"></div>
const { render, defineComponent, h, withDirectives, vModelText } = Vue
const root = document.querySelector('#app')
const component = defineComponent({
data() {
return { value: null }
},
template: `
<div>
<input v-model="value" />
<p>{{ value }}</p>
</div>
`
})
render(h(component), root)
这个例子很简单,就是在input上使用了v−model然后把value的值实时的展示在p元素上.
function render() {
const _this = this
return h('div', [
withDirectives(h('input', {
'onUpdate:modelValue'($event) {
_this.value = $event
}
}), [[vModelText , this.value ]]),
h('p', this.value)
])
}
上面的模板其实可以写成这样的render函数,可以看到本质还是调用了withDirectives方法.
function render(_ctx, _cache) {
with (_ctx) {
const { vModelText: _vModelText, createVNode: _createVNode, withDirectives: _withDirectives, toDisplayString: _toDisplayString, openBlock: _openBlock, createBlock: _createBlock } = _Vue
return (_openBlock(), _createBlock("div", null, [
_withDirectives(_createVNode("input", {
"onUpdate:modelValue": $event => (value = $event)
}, null, 8 /* PROPS */, ["onUpdate:modelValue"]), [
[_vModelText, value]
]),
_createVNode("p", null, _toDisplayString(value), 1 /* TEXT */)
]))
}
}
- 这个render函数是运行时根据上面的模板通过Vue.compile编译出来的,可以看到模板编译的render函数和上面写的render函数类似
- 从我写的render函数或者通过Vue.compile编译出来的render函数来看
<input v-model="value" />
被编译成了
withDirectives(h('input', {
'onUpdate:modelValue'($event) {
_this.value = $event
}
}), [[vModelText , this.value ]])
- 当input元素触发input事件时,会调用onUpdate:modelValue函数从而对value进行赋值更新
- 当响应式value更改触发指令beforeUpdate钩子时,会把响应式value的值赋值给input元素
- 其实就是这么简单,那接下来看看尤大是如何实现的
vModelText内部实现
vModelText源码 runtime-dom/src/directives/vModel.ts
const getModelAssigner = (vnode: VNode): AssignerFn => {
const fn = vnode.props!['onUpdate:modelValue']
return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
}
function onCompositionStart(e: Event) {
;(e.target as any).composing = true
}
function onCompositionEnd(e: Event) {
const target = e.target as any
if (target.composing) {
target.composing = false
trigger(target, 'input')
}
}
function trigger(el: HTMLElement, type: string) {
const e = document.createEvent('HTMLEvents')
e.initEvent(type, true, true)
el.dispatchEvent(e)
}
// We are exporting the v-model runtime directly as vnode hooks so that it can
// be tree-shaken in case v-model is never used.
export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
created(el, { modifiers: { lazy, trim, number } }, vnode) {
el._assign = getModelAssigner(vnode)
const castToNumber = number || el.type === 'number'
addEventListener(el, lazy ? 'change' : 'input', e => {
if ((e.target as any).composing) return
let domValue: string | number = el.value
if (trim) {
domValue = domValue.trim()
} else if (castToNumber) {
domValue = toNumber(domValue)
}
el._assign(domValue)
})
if (trim) {
addEventListener(el, 'change', () => {
el.value = el.value.trim()
})
}
if (!lazy) {
addEventListener(el, 'compositionstart', onCompositionStart)
addEventListener(el, 'compositionend', onCompositionEnd)
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
addEventListener(el, 'change', onCompositionEnd)
}
},
// set value on mounted so it's after min/max for type="range"
mounted(el, { value }) {
el.value = value == null ? '' : value
},
beforeUpdate(el, { value, modifiers: { trim, number } }, vnode) {
el._assign = getModelAssigner(vnode)
// avoid clearing unresolved text. #2302
if ((el as any).composing) return
if (document.activeElement === el) {
if (trim && el.value.trim() === value) {
return
}
if ((number || el.type === 'number') && toNumber(el.value) === value) {
return
}
}
const newValue = value == null ? '' : value
if (el.value !== newValue) {
el.value = newValue
}
}
}
-
从实现来看,比上面尝试写的vModelText的实现,要多好多其他情况的处理
-
当执行created钩子时:
- 拿到需要执行的函数: 通过getModelAssigner方法,从vnode的props上提取 onUpdate:modelValue的函数,对于本次的小栗子其实就是
"onUpdate:modelValue": $event => (value = $event)
后面的那个函数
- 根据 lazy 来给 input 元素注册 change 或者 input 事件
- 根据 trim 来给 input 元素再次注册一个change事件
- 根据 lazy 来给 input 元素分别注册 compositionstart compositionend change 事件为了修复Safari < 10.2的bug
-
当执行mounted钩子时:
- 为 type="range" 的 input元素 做特殊处理
-
当执行beforeUpdate钩子时:
- 拿到需要执行的函数
- 避免清除未解析的文本
- 给input.value赋值
-
上面代码小总结:
- 调用created钩子时注册相关事件,当input值发生改变后,调用onUpdate:modelValue函数给value赋值
- 当value值改变时,调用beforeUpdate钩子,把value值赋值给input.value
- 这就达到了双向绑定(话说,我觉得最好加个beforeUnmount钩子,把created钩子中注册的事件移除掉)
总结
一个优秀的库真不容易,要处理那么多特殊情况
下篇: Vue3疑问系列(5) — v-model(vModelCheckbox)指令是如何工作的?
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!