v-model
本质是 prop 和 事件监听 的语法糖。
通过 prop 实现
数据 -> 视图
的单向数据流;通过监听 change 或 input 事件实现视图 -> 数据
的单向数据流。
js
/**
* v-model 的实现
* 注册了 created 和 beforeUpdate 两个钩子函数
*/
const VModelText = {
/**
* created 钩子函数
* @param {Object} el - 节点对象
* @param {Object} binding - binding 对象
* @param {Object} vnode - 虚拟节点对象
* created 主要做了 3 件事:
* 1、将 js 对象的 value 赋值为元素的 value 属性
* 2、通过 getModelAssigner 方法获取 props 中的 onUpdate:modelValue 属性赋值给元素的 _assign 属性
* 3、通过 addEventListener 监听 input 标签的事件
*/
created(el, { value, modifiers: { lazy, trim, number } }, vnode) {
// 1、将 js 对象的 value 赋值为元素的 value 属性(数据 -> 视图的单向数据流)
el.value = value == null ? '' : value
// 2、通过 getModelAssigner 方法获取 props 中的 onUpdate:modelValue 属性赋值给元素的 _assign 属性
el._assign = getModelAssigner(vnode)
// 判断是否配置了 number 或 元素的 type 为 number
const castToNumber = number || el.type === 'number'
// 3、通过 addEventListener 监听 input 标签的事件
// 根据 lazy 决定监听元素的 change 或 input 事件(change | input)
addEventListener(el, lazy ? 'change' : 'input', e => {
if (e.target.composing) return
// 获取元素的新值
let domValue = el.value
// 如果配置了 trim,则调用 String.trim() 清除首尾空格
if (trim) {
domValue = domValue.trim()
}
// 如果配置了 number 或元素的 type 为 number,则转换成 number 后再赋值给元素
else if (castToNumber) {
domValue = toNumber(domValue)
}
// 更新数据
el._assign(domValue)
})
// 如果配置了 trim,则调用 String.trim() 清除首尾空格
if (trim) {
addEventListener(el, 'change', () => {
el.value = el.value.trim()
})
}
// 如果没有配置 lazy,则添加两个事件对中文输入法输入进行监听
if (!lazy) {
addEventListener(el, 'compositionstart', onCompositionStart)
addEventListener(el, 'compositionend', onCompositionEnd)
}
},
/**
* beforeUpdata 钩子函数
* @param {Object} el - 节点对象
* @param {Object} binding - binding 对象
* @param {Object} vnode - 虚拟节点对象
*/
beforeUpdate(el, { value, modifiers: { trim, number } }, vnode) {
el._assign = getModelAssigner(vnode)
if (document.activeElement === el) {
// 如果配置了 trim,则将元素的值通过 String.trim() 清除首尾空格和再赋值给数据
if (trim && el.value.trim() === value) return
// 如果配置了 number 或 元素的 type 为 number,则将元素的值转换为 number 再赋值给数据
if ((number || el.type === 'number') && toNumber(el.value) === value) return
}
const newValue = value == null ? '' : value
// 更新前判断新旧值是否相同,如果不同,则把数据更新到 DOM 上
if (el.value !== newValue) {
el.value = newValue
}
}
}
/**
* 获取 model 分配器
*/
const getModelAssigner = (vnode) => {
const fn = vnode.props['onUpdate:modelValue']
return isArray(fn) ? value => invokeArrayFns(fn, value) : fn
}
/**
* 中文输入法触发的事件
*/
function onCompositionStart(e) {
e.target.composing = true
}
/**
* 中文输入法中确定输入的数据后触发的事件
*/
function onCompositionEnd(e) {
const target = e.target
if (target.composing) {
target.composing = false
// 手动触发 input 事件进行赋值
trigger(target, 'input')
}
}