前言
雙向繫結v-model
不僅僅是對可編輯HTML元素(select
, input
, textarea
和附帶[contenteditable=true]
)同時附加v-bind
和v-on
,而且還能利用通過petite-vue附加給元素的_value
、_trueValue
和_falseValue
屬性提供儲存非字串值的能力。
深入v-model
工作原理
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)
}
compositionstart
和compositionend
是什麼?
compositionstart
是開始在輸入法編輯器上輸入字元觸發,而compositionend
則是在輸入法編輯器上輸入字元結束時觸發,另外還有一個compositionupdate
是在輸入法編輯器上輸入字元過程中觸發。
當我們在輸入法編輯器敲擊鍵盤時會按順序執行如下事件:compositionstart
-> (compositionupdate
-> input
)+ -> compositionend
-> 當失焦時觸發change
當在輸入法編輯器上輸入ri
後按空格確認日
字元,則觸發如下事件compositionstart(data="")
-> compositionupdate(data="r")
-> input
-> compositionupdate(data="ri")
-> input
-> compositionupdate(data="日")
-> input
-> compositionend(data="日")
由於在輸入法編輯器上輸入字元時會觸發input
事件,所以petite-vue中通過在物件上設定composing
標識是否執行input
邏輯。
事件物件屬性如下:
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
編碼方式觸發事件
DOM Level2的事件中包含HTMLEvents, MouseEvents、MutationEvents和UIEvents,而DOM Level3則增加如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包含
abort
,blur
,change
,error
,focus
,load
,reset
,resize
,scroll
,select
,submit
,unload
,input
- UIEvents包含
DOMActive
,DOMFocusIn
,DOMFocusOut
,keydown
,keypress
,keyup
- MouseEvents包含
click
,mousedown
,mousemove
,mouseout
,mouseover
,mouseup
- MutationEvents包含
DOMAttrModified
,DOMNodeInserted
,DOMNodeRemoved
,DOMCharacterDataModified
,DOMNodeInsertedIntoDocument
,DOMNodeRemovedFromDocument
,DOMSubtreeModified
建立和初始化事件物件
MouseEvent
方法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時使用
)
方法2
const e: Event = new MouseEvent('click', {
bubbles: false,
// ......
})
KeyboardEvent
const e = new KeyboardEvent(
typeArg: string, // 如keypress
{
ctrlKey: true,
// ......
}
)
https://developer.mozilla.org...
Event的初始方法
/**
* 選項的屬性
* @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
方法1
const e: Event = document.createEvent('CustomEvent')
e.initMouseEvent(
type: string,
bubbles: boolean,
cancelable: boolean,
detail: any
)
方法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
)
新增監聽和釋出事件
element.addEventListener(type: string)
element.dispatchEvent(e: Event)
針對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)
}
當在輸入法編輯器操作完畢後會手動觸發input事件,但當事件繫結修飾符設定為lazy
後並沒有繫結input
事件回撥函式,此時在輸入法編輯器操作完畢後並不會自動更新狀態,我們又有機會可以貢獻程式碼了:)
// change事件是元素失焦後前後值不同時觸發,而input事件是輸入過程中每次修改值都會觸發
listen(el, modifiers?.lazy ? 'change' : 'input', () => {
// 元素的composing屬性用於標記是否處於輸入法編輯器輸入內容的狀態,如果是則不執行change或input事件的邏輯
if ((el as any).composing) return
assign(resolveValue(el.value))
})
外番:IE的事件模擬
var e = document.createEventObject()
e.shiftKey = false
e.button = 0
document.getElementById('click').fireEvent('onclick', e)
總結
整合LayUI等DOM-based框架時免不了使用this.$ref
獲取元素例項,下一篇《petite-vue原始碼剖析-ref的工作原理》我們一起來探索吧!
尊重原創,轉載請註明來自:https://www.cnblogs.com/fsjoh... 肥仔John