1、结构
由下图可见,其实输入框组成部分其实若干个插槽配合input所使用的。本文input的类型一共分为两种,一种是input一种是textarea。但是他们都离不开这个结构。prepend、append、prefix-icon、suffix-icon是分别为 prepend slot、prefix slot、 suffix slot、append slot的插槽
总体结构
总体来看,整个组件也是非常好理解的,第一个div盒子就是包裹全部, 属性大致是一些命名空间,继承样式,以及鼠标事件...,里面有两个template,分别用来装input和textarea情况的
input类型结构
我们来逐一对应代码
textarea类型结构
2、Input-非textarea情况
hovering
hovering:hover状态,用于判断是否显示清空图标,状态修改的事件,一般多为handleMouseLeave,handleMouseEnter 给大家介绍另外一个状态 showClear:显示清空图标按钮
判断是否传递了clearable
是否被禁用了
是否只读
是否聚焦或者处于hover状态
关于计算图标偏移问题
关于图标是如何恰巧的放在input的左侧和右侧的,在这里我们可以看到详细的结构代码
prefix-icon,suffix-icon 的位置受prepend 、 append 的影响,请看下面代码。
focus
focus 状态,用于判断 清空图标的显示、密码查看图标。状态修改的事件 为 focus,blur
isComposing
isComposing:正在输入状态,用于中文输入时,正在输入,但文本未确认时。此时也触发input事件,但是此时不应该修改v-model值
状态改变的事件 input:
compositionstart 文字输入之前触发
compositionupdate 输入过程中每次敲击键盘触发
compositionend 选择字词完成时触发
注册这三个事件的原因在于实现中文输入法下,仅在选词后触发 input 事件。
由于在输入拼音的时输入框不是立即获得输入的值,而是要确实后才能获取到
触发compositionstart时,文本框会填入待确认文本,同时触发 input 事件;如果不想触发 input ,需设置一个变量来控制。
value值的更新
纠正一下,我们都会遇到的一道面试题是el-input的值是如何更新的,很多小伙伴都会说是v-model。但是这个答案吧,深层的阅读源码后,我答案是不是。在源码里面是通过:value + @input事件 来进行值更新 在源码里面当value发生变化的时,也需要及时地去调用setNativeInputValue以达到更新当前组件地value
// 动态计算值
const _ref = computed(() => input.value || textarea.value)
// 更新值调用的函数
const setNativeInputValue = () => {
const input = _ref.value
if (!input || input.value === nativeInputValue.value) return
input.value = nativeInputValue.value
}
// 监听nativeInputValue 如果变化则调用更新函数
watch(nativeInputValue, () => setNativeInputValue())
// when change between <input> and <textarea>,
// update DOM dependent value and styles
// https://github.com/ElemeFE/element/issues/14857
watch(
() => props.type,
async () => {
await nextTick()
setNativeInputValue()
resizeTextarea()
updateIconOffset()
}
)
onMounted(() => {
if (!props.formatter && props.parser) {
debugWarn(
'ElInput',
'If you set the parser, you also need to set the formatter.'
)
}
setNativeInputValue()
updateIconOffset()
nextTick(resizeTextarea)
})
//输入触发方法handleInput()
const handleInput = async (event: Event) => {
recordCursor()
let { value } = event.target as TargetElement
if (props.formatter) {
value = props.parser ? props.parser(value) : value
value = props.formatter(value)
}
// should not emit input during composition
// see: https://github.com/ElemeFE/element/issues/10516
if (isComposing.value) return
// hack for https://github.com/ElemeFE/element/issues/8548
// should remove the following line when we don't support IE
if (value === nativeInputValue.value) {
setNativeInputValue()
return
}
emit(UPDATE_MODEL_EVENT, value)
emit('input', value)
// ensure native input value is controlled
// see: https://github.com/ElemeFE/element/issues/12850
await nextTick()
setNativeInputValue()
setCursor()
}
根据我们前面的了解到在input选择字词完成时触发compositionend()方法是会调用到handleInput() 方法的 而当value发生变化时,handleInput()会及时地调用setNativeInputValue() 以达到更新当前组件的value 但事实上除了在handleInput()时调用setNativeInputValue()是远远不够的 所以我们需要通过监听以及onMounted生命周期去调用setNativeInputValue()去更新值
show-password
show-password 显示密码,可省略type=‘password’的用法,先看文档上所介绍的
其中切换的两种形态
我记得我的老师曾经跟我说过一句话:遇到不会的先去看官方文档,文档这里说了这是一个布尔值,默认是false,如果你给了这个属性,就是true
值得注意的是,show-password只有在type=password才生效的,用例上也有说明,证明是搭配使用的,我们看看下方代码截图, 这里:type是用三元作了一层判断,这个passwordVisible就是我们等一下需要探讨的东西
// 是否显示密码,用于input type(password类型)状态切换,事件触发click密码查看图标
const passwordVisible = ref(false)
const handlePasswordVisible = () => {
//密码显示与隐藏
passwordVisible.value = !passwordVisible.value
//当切换时 光标位置置于最后的方法
focus()
}
const focus = async () => {
// see: https://github.com/ElemeFE/element/issues/18573
await nextTick()
//密码显示与隐藏的切换 使光标位置置后,使用 nextTick()可等页面更新后再进行聚焦(显示密码后再聚焦,焦点在文字后面)
_ref.value?.focus()
}
$attrs与useAttrs()运用
包含了父作用域中不作为组件 props 或 自定义事件。当一个组件没有声明任何 prop 时,这里会包含所有父级作用域的绑定,并且可以通过 v-bind="$attrs" 传入内部组件——在创建高阶的组件时非常有用。
这里可以插播一条面试题诶
vue中常见的组件通信方式:
父子通信(部分可用来作为爷孙通信)
1、父传子props,子传父用$emit
2、通过$parent/$children。//但是这个方法其实在我接触到真实项目之中是不推荐使用的,因为很容易出bug
3、provide/inject
4、$attrs/$listeners
兄弟通信
1、$parent/$children
2、vuex
3、中央事件总线eventBus
$attrs 是在vue的2.4版本以上添加的,项目中如果有多层组件传参可以使用$attrs,可以使代码更加美观,更简洁,维护更方便。如果使用传统的父子传参prop和$emit传参,$on会很繁琐;如果使用vuex会大材小用,只是在这几个组件里面使用就没有必要使用vuex,使用eventBus,如果使用不恰当就可能会引起出现多次事件执行.
作用:$attrs可父子孙一脉相传的组件间的通信
eg:
父子通信:如果父子组件之间通讯 在父组件传给子组件的数据,可用props来进行接收。如果不用props接收,父组件传过来的数据会在$attrs里面
父子孙通信:父组件的传给子组件的数据,子组件可以用props来接收,props没有接收完的数据会在$attrs中,孙组件要想接收这个数据可以在子组件加入v-bind='$attrs',来传给孙组件,这样孙子组件就可以接收它爷爷传过来的数据了。
$attrs的方便之处在于数据是一层一层传下去,不用每一层都用props接收之后再传给后面的组件,可以直接加一个v-bind:'$attrs'
在我们vue3里面在原先,可以通过该 API 来获取组件的上下文信息,包含了 attrs 、slots 、emit、expose 等父子组件通信数据和方法,该 API 将在 3.2 版本之后删除,context 里面的数据,会用新的 useSlots 和 useAttrs API 来代替。 useAttrs 可以是用来获取 attrs 数据的(也就是非 props 的属性值)。如果当前组件里没有将某个属性指定为 props,那么父组件绑定下来的属性值,都会进入到 attrs 里,通过这个新 API 来拿到。
<script lang="ts" setup>
import { useAttrs as useRawAttrs} from 'vue',//有删减哦,源码里不止引用那么点
import { useAttrs } from '@element-plus/hooks'//有删减哦,源码里不止引用那么点
// useAttrs props穿透
const rawAttrs = useRawAttrs()
// 目的是返回除了class style 及其他事件的属性
const containerAttrs = computed(() => {
const comboBoxAttrs: Record<string, unknown> = {}
if (props.containerRole === 'combobox') {
comboBoxAttrs['aria-haspopup'] = rawAttrs['aria-haspopup']
comboBoxAttrs['aria-owns'] = rawAttrs['aria-owns']
comboBoxAttrs['aria-expanded'] = rawAttrs['aria-expanded']
}
return comboBoxAttrs
})
const attrs = useAttrs({
excludeKeys: computed<string[]>(() => {
return Object.keys(containerAttrs.value)
}),
})
</script>
interface Params {
excludeListeners?: boolean
excludeKeys?: ComputedRef<string[]>
}
// 默认排除的key值
const DEFAULT_EXCLUDE_KEYS = ['class', 'style']
//侦听器前缀
const LISTENER_PREFIX = /^on[A-Z]/
export const useAttrs = (
params: Params = {}
): ComputedRef<Record<string, unknown>> => {
const { excludeListeners = false, excludeKeys } = params
const allExcludeKeys = computed<string[]>(() => {
return (excludeKeys?.value || []).concat(DEFAULT_EXCLUDE_KEYS)
})
// .concat是为 排除key的数组
// 以下是 使其变成响应式,可监听变化 ↓↓
const instance = getCurrentInstance()
if (!instance) {
debugWarn(
'use-attrs',
'getCurrentInstance() returned null. useAttrs() must be called at the top of a setup function'
)
return computed(() => ({}))
}
// 返回
return computed(() =>
fromPairs(
// 这里返回一个 二维数组 [{key,value},{key,value}]根据变化修改attrs
Object.entries(instance.proxy?.$attrs!).filter(
([key]) =>
!allExcludeKeys.value.includes(key) &&
!(excludeListeners && LISTENER_PREFIX.test(key))
)
)
)
}
这里顺便还想讲讲debugWarn的原理,也就是我们日常看到在控制台报错的信息里面的源码信息
import { isString } from './types'
class ElementPlusError extends Error {
constructor(m: string) {
super(m)
this.name = 'ElementPlusError'
}
}
export function throwError(scope: string, m: string): never {
throw new ElementPlusError(`[${scope}] ${m}`)
}
export function debugWarn(err: Error): void
export function debugWarn(scope: string, message: string): void
export function debugWarn(scope: string | Error, message?: string): void {
if (process.env.NODE_ENV !== 'production') {
const error: Error = isString(scope)
? new ElementPlusError(`[${scope}] ${message}`)
: scope
// eslint-disable-next-line no-console
// 最后在这里标红console出来
console.warn(error)
}
}
- Textarea 自适应高度
textarea类型的自适应高度无非跟autosize有关,整理翻出了一些源码解释一下原理给大家看
import { calcTextareaHeight } from './utils'
...
// onMounted的时候调用
onMounted(() => {
// 此处有删减一些跟章节无关调用的方法
nextTick(resizeTextarea)
})
// 监听
watch(
() => props.type,
async () => {
resizeTextarea()
}
)
watch(
() => props.modelValue,
() => {
nextTick(() => resizeTextarea()
}
)
// resizeTextarea
const resizeTextarea = () => {
const { type, autosize } = props
if (!isClient || type !== 'textarea') return
if (autosize) {
// 传入autosize设置的值的情况
const minRows = isObject(autosize) ? autosize.minRows : undefined
const maxRows = isObject(autosize) ? autosize.maxRows : undefined
textareaCalcStyle.value = {
...calcTextareaHeight(textarea.value!, minRows, maxRows),
}
} else {
// 不设置内容自动调整高度的情况(让其自动撑开)
textareaCalcStyle.value = {
minHeight: calcTextareaHeight(textarea.value!).minHeight,
}
}
}
// utils.ts内calcTextareaHeight()函数
export function calcTextareaHeight(
targetElement: HTMLTextAreaElement,
minRows = 1,
maxRows?: number
): TextAreaHeight {
if (!hiddenTextarea) {
hiddenTextarea = document.createElement('textarea')
document.body.appendChild(hiddenTextarea)
}
// targetElement 内容,minRow:最小行数(默认值为1), maxRows? 可传可不传的最大行数(数字型)
const { paddingSize, borderSize, boxSizing, contextStyle } =
calculateNodeStyling(targetElement)
// 创建一个隐藏样式的textarea 并添加到body上 并设置行内样式以及隐藏样式
hiddenTextarea.setAttribute('style', `${contextStyle};${HIDDEN_STYLE}`)
hiddenTextarea.value = targetElement.value || targetElement.placeholder || ''
// 获取真实高度 => 获取此隐藏textarea的scrollHeight
let height = hiddenTextarea.scrollHeight
const result = {} as TextAreaHeight
//这里是一些boxsizing是border-box还是content-box来决定高度是如何计算的(获取真是height)
if (boxSizing === 'border-box') {
height = height + borderSize
} else if (boxSizing === 'content-box') {
height = height - paddingSize
}
// 计算单行高度 => 通过清空内容 获取单行高度
hiddenTextarea.value = ''
const singleRowHeight = hiddenTextarea.scrollHeight - paddingSize
// isNumber() 校验输入的是否为数字型
//通过 minRows和maxRows去得到minHeight和height
if (isNumber(minRows)) {
let minHeight = singleRowHeight * minRows
if (boxSizing === 'border-box') {
minHeight = minHeight + paddingSize + borderSize
}
height = Math.max(minHeight, height)
result.minHeight = `${minHeight}px`
}
if (isNumber(maxRows)) {
let maxHeight = singleRowHeight * maxRows
if (boxSizing === 'border-box') {
maxHeight = maxHeight + paddingSize + borderSize
}
height = Math.min(maxHeight, height)
}
result.height = `${height}px`
// 移除 hiddenTextarea并重置
hiddenTextarea.parentNode?.removeChild(hiddenTextarea)
hiddenTextarea = undefined
return result
}
以上是input框的相关内容-后续也会更新哒!!!大家如果有什么问题可以评论区提问我会回答哒,关于inputNumber类型请看另外一篇文档。多谢大家支持,谢谢谢谢谢谢