前言

双向绑定v-model不仅仅是对可编辑HTML元素(select, input, textarea和附带[contenteditable=true])同时附加v-bindv-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)}

compositionstartcompositionend是什么?

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或compositionendreadonly bubbles: boolean // 事件是否冒泡readonly cancelable: boolean // 事件是否可勾销readonly view: WindowProxy // 以后文档对象所属的window对象(`document.defaultView`)readonly detail: longreadonly 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 = falsee.button = 0document.getElementById('click').fireEvent('onclick', e)

总结

整合LayUI等DOM-based框架时免不了应用this.$ref获取元素实例,下一篇《petite-vue源码分析-ref的工作原理》咱们一起来摸索吧!