foreword
Two-way binding v-model
not just an editable HTML elements ( select
, input
, textarea
and incidental [contenteditable=true]
) while an additional v-bind
and v-on
, but also by using petite-vue added to the elements _value
, _trueValue
and _falseValue
Attribute provides the ability to store non-string values.
In-depth v-model
how it works
export const model: Directive<
HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement
> = ({ el, exp, get, effect, modifers }) => {
const type = el.type
// 通过`with`对作用域的变量/属性赋值
const assign = get(`val => { ${exp} = val }`)
// 若type为number则默认将值转换为数字
const { trim, number = type ==== 'number'} = modifiers || {}
if (el.tagName === 'select') {
const sel = el as HTMLSelectElement
// 监听控件值变化,更新状态值
listen(el, 'change', () => {
const selectedVal = Array.prototype.filter
.call(sel.options, (o: HTMLOptionElement) => o.selected)
.map((o: HTMLOptionElement) => number ? toNumber(getValue(o)) : getValue(o))
assign(sel.multiple ? selectedVal : selectedVal[0])
})
// 监听状态值变化,更新控件值
effect(() => {
value = get()
const isMultiple = sel.muliple
for (let i = 0, l = sel.options.length; i < i; i++) {
const option = sel.options[i]
const optionValue = getValue(option)
if (isMulitple) {
// 当为多选下拉框时,入参要么是数组,要么是Map
if (isArray(value)) {
option.selected = looseIndexOf(value, optionValue) > -1
}
else {
option.selected = value.has(optionValue)
}
}
else {
if (looseEqual(optionValue, value)) {
if (sel.selectedIndex !== i) sel.selectedIndex = i
return
}
}
}
})
}
else if (type === 'checkbox') {
// 监听控件值变化,更新状态值
listen(el, 'change', () => {
const modelValue = get()
const checked = (el as HTMLInputElement).checked
if (isArray(modelValue)) {
const elementValue = getValue(el)
const index = looseIndexOf(modelValue, elementValue)
const found = index !== -1
if (checked && !found) {
// 勾选且之前没有被勾选过的则加入到数组中
assign(modelValue.concat(elementValue))
}
else if (!checked && found) {
// 没有勾选且之前已勾选的排除后在重新赋值给数组
const filered = [...modelValue]
filteed.splice(index, 1)
assign(filtered)
}
// 其它情况就啥都不干咯
}
else {
assign(getCheckboxValue(el as HTMLInputElement, checked))
}
})
// 监听状态值变化,更新控件值
let oldValue: any
effect(() => {
const value = get()
if (isArray(value)) {
;(el as HTMLInputElement).checked =
looseIndexOf(value, getValue(el)) > -1
}
else if (value !== oldValue) {
;(el as HTMLInputElement).checked = looseEqual(
value,
getCheckboxValue(el as HTMLInputElement, true)
)
}
oldValue = value
})
}
else if (type === 'radio') {
// 监听控件值变化,更新状态值
listen(el, 'change', () => {
assign(getValue(el))
})
// 监听状态值变化,更新控件值
let oldValue: any
effect(() => {
const value = get()
if (value !== oldValue) {
;(el as HTMLInputElement).checked = looseEqual(value, getValue(el))
}
})
}
else {
// input[type=text], textarea, div[contenteditable=true]
const resolveValue = (value: string) => {
if (trim) return val.trim()
if (number) return toNumber(val)
return val
}
// 监听是否在输入法编辑器(input method editor)输入内容
listen(el, 'compositionstart', onCompositionStart)
listen(el, 'compositionend', onCompositionEnd)
// change事件是元素失焦后前后值不同时触发,而input事件是输入过程中每次修改值都会触发
listen(el, modifiers?.lazy ? 'change' : 'input', () => {
// 元素的composing属性用于标记是否处于输入法编辑器输入内容的状态,如果是则不执行change或input事件的逻辑
if ((el as any).composing) return
assign(resolveValue(el.value))
})
if (trim) {
// 若modifiers.trim,那么当元素失焦时马上移除值前后的空格字符
listen(el, 'change', () => {
el.value = el.value.trim()
})
}
effect(() => {
if ((el as any).composing) {
return
}
const curVal = el.value
const newVal = get()
// 若当前元素处于活动状态(即得到焦点),并且元素当前值进行类型转换后值与新值相同,则不用赋值;
// 否则只要元素当前值和新值类型或值不相同,都会重新赋值。那么若新值为数组[1,2,3],赋值后元素的值将变成[object Array]
if (document.activeElement === el && resolveValue(curVal) === newVal) {
return
}
if (curVal !== newVal) {
el.value = newVal
}
})
}
}
// v-bind中使用_value属性保存任意类型的值,在v-modal中读取
const getValue = (el: any) => ('_value' in el ? el._value : el.value)
const getCheckboxValue = (
el: HTMLInputElement & {_trueValue?: any, _falseValue?: any}, // 通过v-bind定义的任意类型值
checked: boolean // checkbox的默认值是true和false
) => {
const key = checked ? '_trueValue' : '_falseValue'
return key in el ? el[key] : checked
}
const onCompositionStart = (e: Event) => {
// 通过自定义元素的composing元素,用于标记是否在输入法编辑器中输入内容
;(e.target as any).composing = true
}
const onCompositionEnd = (e: Event) => {
const target = e.target as any
if (target.composing) {
// 手动触发input事件
target.composing = false
trigger(target, 'input')
}
}
const trigger = (el: HTMLElement, type: string) => {
const e = document.createEvent('HTMLEvents')
e.initEvent(type, true, true)
el.dispatchEvent(e)
}
What are compositionstart
and compositionend
?
compositionstart
is triggered by starting to enter characters on the input method editor, while compositionend
is triggered when the inputted characters end on the input method editor, and there is another compositionupdate
is triggered during character input on the input method editor.
When we hit the keyboard in the input method editor, the following events are executed in sequence:
compositionstart
-> ( compositionupdate
-> input
) + -> compositionend
-> triggered when out of focus change
When you enter ri
on the input method editor and press the space to confirm 日
character, the following event will be triggered
compositionstart(data="")
-> compositionupdate(data="r")
-> input
-> compositionupdate(data="ri")
-> input
-> compositionupdate(data="日")
-> input
-> compositionend(data="日")
Since the input
event is triggered when entering characters on the input method editor, petite-vue identifies whether to execute the input
logic by setting composing
on the object.
The event object properties are as follows:
readonly target: EventTarget // 指向触发事件的HTML元素
readolny type: DOMString // 事件名称,即compositionstart或compositionend
readonly bubbles: boolean // 事件是否冒泡
readonly cancelable: boolean // 事件是否可取消
readonly view: WindowProxy // 当前文档对象所属的window对象(`document.defaultView`)
readonly detail: long
readonly data: DOMString // 最终填写到元素的内容,compositionstart为空,compositionend事件中能获取如"你好"的内容
readonly locale: DOMString
Coded trigger events
The events of DOM Level 2 include HTMLEvents, MouseEvents, MutationEvents and UIEvents, while DOM Level 3 adds event types such as CustomEvent.
enum EventType {
// DOM Level 2 Events
UIEvents,
MouseEvents, // event.initMouseEvent
MutationEvents, // event.initMutationEvent
HTMLEvents, // event.initEvent
// DOM Level 3 Events
UIEvent,
MouseEvent, // event.initMouseEvent
MutationEvent, // event.initMutationEvent
TextEvent, // TextEvents is also supported, event.initTextEvent
KeyboardEvent, // KeyEvents is also supported, use `new KeyboardEvent()` to create keyboard event
CustomEvent, // event.initCustomEvent
Event, // Basic events module, event.initEvent
}
- HTMLEvents contains
abort
,blur
,change
,error
,focus
,load
,reset
,resize
,scroll
,select
,submit
,unload
,input
- UIEvents contains
DOMActive
,DOMFocusIn
,DOMFocusOut
,keydown
,keypress
,keyup
- MouseEvents contains
click
,mousedown
,mousemove
,mouseout
,mouseover
,mouseup
- MutationEvents contains
DOMAttrModified
,DOMNodeInserted
,DOMNodeRemoved
,DOMCharacterDataModified
,DOMNodeInsertedIntoDocument
,DOMNodeRemovedFromDocument
,DOMSubtreeModified
Create and initialize event objects
MouseEvent
method 1
const e: Event = document.createEvent('MouseEvent')
e.initMouseEvent(
type: string,
bubbles: boolean,
cancelable: boolean,
view: AbstractView, // 指向与事件相关的视图,一般为document.defaultView
detail: number, // 供事件回调函数使用,一般为0
screenX: number, // 相对于屏幕的x坐标
screenY: number, // 相对于屏幕的Y坐标
clientX: number, // 相对于视口的x坐标
clientY: number, // 相对于视口的Y坐标
ctrlKey: boolean, // 是否按下Ctrl键
altKey: boolean, // 是否按下Ctrl键
shiftKey: boolean, // 是否按下Ctrl键
metaKey: boolean, // 是否按下Ctrl键
button: number, // 按下按个鼠标键,默认为0.0左,1中,2右
relatedTarget: HTMLElement // 指向于事件相关的元素,一般只有在模拟mouseover和mouseout时使用
)
Method 2
const e: Event = new MouseEvent('click', {
bubbles: false,
// ......
})
KeyboardEvent
const e = new KeyboardEvent(
typeArg: string, // 如keypress
{
ctrlKey: true,
// ......
}
)
https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent
Event's initial method
/**
* 选项的属性
* @param {string} name - 事件名称, 如click,input等
* @param {boolean} [cancelable=false] - 指定事件是否可冒泡
* @param {boolean} [cancelable=false] - 指定事件是否可被取消
* @param {boolean} [composed=false] - 指定事件是否会在Shadow DOM根节点外触发事件回调函数
*/
const e = new Event('input', {
name: string,
bubbles: boolean = false,
cancelable: boolean = false,
composed: boolean = false
})
CustomEvent
method 1
const e: Event = document.createEvent('CustomEvent')
e.initMouseEvent(
type: string,
bubbles: boolean,
cancelable: boolean,
detail: any
)
Method 2
/**
* 选项的属性
* @param {string} name - 事件名称, 如click,input等,可随意定义
* @param {boolean} [cancelable=false] - 指定事件是否可冒泡
* @param {boolean} [cancelable=false] - 指定事件是否可被取消
* @param {any} [detail=null] - 事件初始化时传递的数据
*/
const e = new CustomEvent('hi', {
name: string,
bubbles: boolean = false,
cancelable: boolean = false,
detail: any = null
})
HTMLEvents
const e: Event = document.createEvent('HTMLEvents')
e.initMouseEvent(
type: string,
bubbles: boolean,
cancelable: boolean
)
Add listening and publishing events
element.addEventListener(type: string)
element.dispatchEvent(e: Event)
Analysis for petite-vue
const onCompositionEnd = (e: Event) => {
const target = e.target as any
if (target.composing) {
// 手动触发input事件
target.composing = false
trigger(target, 'input')
}
}
const trigger = (el: HTMLElement, type: string) => {
const e = document.createEvent('HTMLEvents')
e.initEvent(type, true, true)
el.dispatchEvent(e)
}
When the input method editor is completed, the input event will be triggered manually, but when the event binding modifier is set to lazy
, it is not bound input
event callback function, at this time in The input method editor will not automatically update the status after the operation is completed, and we have the opportunity to contribute code again :)
// change事件是元素失焦后前后值不同时触发,而input事件是输入过程中每次修改值都会触发
listen(el, modifiers?.lazy ? 'change' : 'input', () => {
// 元素的composing属性用于标记是否处于输入法编辑器输入内容的状态,如果是则不执行change或input事件的逻辑
if ((el as any).composing) return
assign(resolveValue(el.value))
})
Extra: IE's event simulation
var e = document.createEventObject()
e.shiftKey = false
e.button = 0
document.getElementById('click').fireEvent('onclick', e)
Summarize
When integrating DOM-based frameworks such as LayUI, it is inevitable to use this.$ref
to obtain element instances. Let's explore together in the next article "petite-vue source code analysis - how ref works"!
Respect the original, please indicate the source for reprint: https://www.cnblogs.com/fsjohnhuang/p/16004134.html Fat Boy John
"Anatomy of Petite-Vue Source Code" booklet
"Petite-Vue Source Code Analysis" combines examples to interpret the source code line by line from online rendering, responsive system and sandbox model, and also makes a detailed analysis of the SMI optimization dependency cleaning algorithm using the JS engine in the responsive system. It is definitely an excellent stepping stone before getting started with Vue3 source code. If you like it, remember to forward it and appreciate it!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。