v-model 部分源码:
export const vModelText: ModelDirective<
HTMLInputElement | HTMLTextAreaElement
> = {
// el dom节点对象; binding 对象; vnode
created(el, { modifiers: { lazy, trim, number } }, vnode) {
// 获取props中的modelValue属性对应的函数
el[assignKey] = getModelAssigner(vnode)
// number修饰符
const castToNumber =
number || (vnode.props && vnode.props.type === 'number')
// change 值改变且光标离开焦点时触发;input 一直触发
addEventListener(el, lazy ? 'change' : 'input', e => {
if ((e.target as any).composing) return
let domValue: string | number = el.value
if (trim) {
// 如果 trim 则手动调用 trim
domValue = domValue.trim()
}
if (castToNumber) {
// 如果是数字,则调用toNumber转为数字
domValue = looseToNumber(domValue)
}
el[assignKey](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"
// 赋值 value, 目前只是单向流动
mounted(el, { value }) {
el.value = value == null ? '' : value
},
beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
el[assignKey] = getModelAssigner(vnode)
// avoid clearing unresolved text. #2302
if ((el as any).composing) return
if (document.activeElement === el && el.type !== 'range') {
if (lazy) {
return
}
if (trim && el.value.trim() === value) {
return
}
if (
(number || el.type === 'number') &&
looseToNumber(el.value) === value
) {
return
}
}
const newValue = value == null ? '' : value
if (el.value !== newValue) {
el.value = newValue
}
}
}
这段代码定义了一个名为 vModelText
的 Vue.js 自定义指令,用于处理文本输入框和文本区域的双向数据绑定(two-way data binding)。让我详细解释一下这段代码的各个部分:
vModelText
的定义:
vModelText
是一个自定义指令,用于处理
和
元素的双向数据绑定。created
、mounted
和 beforeUpdate
。created
钩子:
created
钩子在元素被创建时触发。v-model
指令关联的数据属性的更新函数,并将其存储在元素的 assignKey
属性中。modifiers
)和元素的属性,决定是否进行数字类型转换。如果有 number
修饰符或元素的类型为 ‘number’,则将输入值转换为数字。lazy
修饰符,监听 change
或 input
事件。这些事件用于响应用户输入的变化。compositionstart
和 compositionend
事件来检测),以避免不必要的输入事件。mounted
钩子:
mounted
钩子在元素被挂载到 DOM 后触发。v-model
指令关联的数据属性的值,以确保初始值正确显示在输入框中。beforeUpdate
钩子:
beforeUpdate
钩子在组件更新之前触发。v-model
指令关联的数据属性的更新函数,并将其存储在元素的 assignKey
属性中。总的来说,这段代码实现了文本输入框和文本区域的双向数据绑定功能,确保用户输入能够同步到 Vue.js 数据属性,并且在数据属性变化时能够正确地显示在输入框中。它考虑了一些特殊情况,例如中文输入法的处理、数字类型的转换和修饰符的影响。这是 Vue.js 中用于处理 v-model
的部分代码。
emit 部分源码:
export function emit(
instance: ComponentInternalInstance,
event: string,
...rawArgs: any[]
) {
if (instance.isUnmounted) return
const props = instance.vnode.props || EMPTY_OBJ
if (__DEV__) {
const {
emitsOptions,
propsOptions: [propsOptions]
} = instance
if (emitsOptions) {
if (
!(event in emitsOptions) &&
!(
__COMPAT__ &&
(event.startsWith('hook:') ||
event.startsWith(compatModelEventPrefix))
)
) {
if (!propsOptions || !(toHandlerKey(event) in propsOptions)) {
warn(
`Component emitted event "${event}" but it is neither declared in ` +
`the emits option nor as an "${toHandlerKey(event)}" prop.`
)
}
} else {
const validator = emitsOptions[event]
if (isFunction(validator)) {
const isValid = validator(...rawArgs)
if (!isValid) {
warn(
`Invalid event arguments: event validation failed for event "${event}".`
)
}
}
}
}
}
let args = rawArgs
// 判断是不是 update: 开头的,如果是则将这个开头截断,获取后面部分 modelArg
const isModelListener = event.startsWith('update:')
// for v-model update:xxx events, apply modifiers on args
const modelArg = isModelListener && event.slice(7)
if (modelArg && modelArg in props) {
const modifiersKey = `${
modelArg === 'modelValue' ? 'model' : modelArg
}Modifiers`
const { number, trim } = props[modifiersKey] || EMPTY_OBJ
if (trim) {
args = rawArgs.map(a => (isString(a) ? a.trim() : a))
}
if (number) {
args = rawArgs.map(looseToNumber)
}
}
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsComponentEmit(instance, event, args)
}
if (__DEV__) {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && props[toHandlerKey(lowerCaseEvent)]) {
warn(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(
instance,
instance.type
)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
// handlerName = on + 事件名称
let handlerName
let handler =
props[(handlerName = toHandlerKey(event))] ||
// also try camelCase event handler (#2249)
props[(handlerName = toHandlerKey(camelize(event)))]
// for v-model update:xxx events, also trigger kebab-case equivalent
// for props passed via kebab-case
if (!handler && isModelListener) {
handler = props[(handlerName = toHandlerKey(hyphenate(event)))]
}
if (handler) {
callWithAsyncErrorHandling(
handler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
const onceHandler = props[handlerName + `Once`]
if (onceHandler) {
if (!instance.emitted) {
instance.emitted = {}
} else if (instance.emitted[handlerName]) {
return
}
instance.emitted[handlerName] = true
callWithAsyncErrorHandling(
onceHandler,
instance,
ErrorCodes.COMPONENT_EVENT_HANDLER,
args
)
}
if (__COMPAT__) {
compatModelEmit(instance, event, args)
return compatInstanceEmit(instance, event, args)
}
}
这段代码是 Vue.js 中的 emit
函数的源码,用于在组件实例中触发自定义事件。让我详细解释一下这段代码的功能和工作原理:
emit
函数签名:
export function emit(
instance: ComponentInternalInstance,
event: string,
...rawArgs: any[]
)
instance
:组件内部实例,即当前组件的实例。event
:要触发的自定义事件的名称。rawArgs
:事件参数,可以是任意数量的参数。首先,代码检查组件实例是否已卸载(isUnmounted
属性)。如果组件已卸载,就不执行任何操作,以避免触发已销毁的组件的事件。
获取组件实例的 props
对象,以便后续判断事件是否在 emits
选项中声明。
在开发环境中(__DEV__
为 true
),进行事件验证:
emits
选项,代码会检查触发的事件是否在 emits
选项中声明。如果事件没有在 emits
中声明,且不是以 'hook:'
或 compatModelEventPrefix
开头的事件,会发出警告。emits
选项中声明,还会检查事件的参数是否有效,即是否符合 emits
选项中定义的验证函数。如果验证失败,会发出警告。如果触发的事件以 'update:'
开头,代码会进行一些特殊处理。它会尝试根据事件名称提取 modelArg
,然后检查是否存在与之对应的修饰符(number
和 trim
)。如果修饰符存在,会对事件参数进行相应的处理。
在开发环境下,如果启用了 Devtools 或 Devtools 检查,代码会调用 devtoolsComponentEmit
函数,用于在开发者工具中跟踪组件事件的触发。
进行事件名称的大小写检查,以确保事件名称不会因为大小写不一致而导致问题。如果事件名称在 HTML 属性中以小写形式存在,但在组件中以不同的大小写形式注册了处理程序,会发出警告。
然后,代码会尝试获取与事件名称匹配的事件处理程序(处理函数)。它首先尝试使用事件名称作为键来查找处理程序,如果找不到,则尝试使用事件名称的驼峰形式(camelize
后的形式)来查找处理程序。对于 update:xxx
类型的事件,还会尝试使用事件名称的短横线形式(hyphenate
后的形式)来查找处理程序。
如果找到了事件处理程序,代码会调用 callWithAsyncErrorHandling
函数来执行事件处理程序,并传递事件参数。这个函数用于处理异步错误,并在出错时提供有关错误的详细信息。
如果组件中定义了 handlerName + 'Once'
形式的事件处理程序,代码会检查是否已经触发过该事件。如果已经触发过,将不再执行。这是为了确保一次性事件只触发一次。
最后,在兼容模式下(__COMPAT__
为 true
),代码会调用与兼容性相关的函数 compatModelEmit
和 compatInstanceEmit
来处理事件触发,以确保与旧版 Vue.js 兼容性。
总之,emit
函数是 Vue.js 中用于触发自定义事件的核心函数。它处理了事件的验证、参数处理、错误处理以及兼容性支持等功能,确保组件能够正确地触发和处理自定义事件。