四、事件处理
1. Event 事件类型
在开发中我们会经常在事件处理函数中使用event事件对象,比如在input框输入时实时获取输入的值;使用鼠标事件时,通过 clientX、clientY 获取当前指针的坐标等等。
我们知道,Event是一个对象,并且有很多属性,这时很多人就会把 event 类型定义为any,这样的话TypeScript就失去了它的意义,并不会对event事件进行静态检查,如果一个键盘事件触发了下面的方法,也不会报错:
const handleEvent = (e: any) => { console.log(e.clientX, e.clientY) } 复制代码
由于Event事件对象中有很多的属性,所以我们也不方便把所有属性及其类型定义在一个interface中,所以React在声明文件中给我们提供了Event事件对象的类型声明。
常见的Event 事件对象如下:
- 剪切板事件对象:ClipboardEvent<T = Element>
- 拖拽事件对象:DragEvent<T = Element>
- 焦点事件对象:FocusEvent<T = Element>
- 表单事件对象:FormEvent<T = Element>
- Change事件对象:ChangeEvent<T = Element>
- 键盘事件对象:KeyboardEvent<T = Element>
- 鼠标事件对象:MouseEvent<T = Element, E = NativeMouseEvent>
- 触摸事件对象:TouchEvent<T = Element>
- 滚轮事件对象:WheelEvent<T = Element>
- 动画事件对象:AnimationEvent<T = Element>
- 过渡事件对象:TransitionEvent<T = Element>
可以看到,这些Event事件对象的泛型中都会接收一个Element元素的类型,这个类型就是我们绑定这个事件的标签元素的类型,标签元素类型将在下面的第五部分介绍。
来看一个简单的例子:
type State = { text: string; }; const App: React.FC = () => { const [text, setText] = useState<string>("") const onChange = (e: React.FormEvent<HTMLInputElement>): void => { setText(e.currentTarget.value); }; return ( <div> <input type="text" value={text} onChange={onChange} /> </div> ); } 复制代码
这里就给onChange方法的事件对象定义为了FormEvent类型,并且作用的对象时一个HTMLInputElement类型的标签(input标签)
可以来看下MouseEvent事件对象和ChangeEvent事件对象的类型声明,其他事件对象的声明形似也类似:
interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> { altKey: boolean; button: number; buttons: number; clientX: number; clientY: number; ctrlKey: boolean; /** * See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method. */ getModifierState(key: string): boolean; metaKey: boolean; movementX: number; movementY: number; pageX: number; pageY: number; relatedTarget: EventTarget | null; screenX: number; screenY: number; shiftKey: boolean; } interface ChangeEvent<T = Element> extends SyntheticEvent<T> { target: EventTarget & T; } 复制代码
在很多事件对象的声明文件中都可以看到 EventTarget 的身影。这是因为,DOM的事件操作(监听和触发),都定义在EventTarget接口上。EventTarget 的类型声明如下:
interface EventTarget { addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void; dispatchEvent(evt: Event): boolean; removeEventListener(type: string, listener?: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void; } 复制代码
比如在change事件中,会使用的e.target来获取当前的值,它的的类型就是EventTarget。来看下面的例子:
<input onChange={e => onSourceChange(e)} placeholder="最多30个字" /> const onSourceChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.value.length > 30) { message.error('请长度不能超过30个字,请重新输入'); return; } setSourceInput(e.target.value); }; 复制代码
这里定义了一个input输入框,当触发onChange事件时,会调用onSourceChange方法,该方法的参数e的类型就是:React.ChangeEvent,而e.target的类型就是EventTarget:
在来看一个例子:
questionList.map(item => ( <div key={item.id} role="button" onClick={e => handleChangeCurrent(item, e)} > // 组件内容... </div> ) const handleChangeCurrent = (item: IData, e: React.MouseEvent<HTMLDivElement>) => { e.stopPropagation(); setCurrent(item); }; 复制代码
这点代码中,点击某个盒子,就将它设置为当前的盒子,方便执行其他操作。当鼠标点击盒子时,会触发handleChangeCurren方法,该方法有两个参数,第二个参数是event对象,在方法中执行了e.stopPropagation();是为了阻止冒泡事件,这里的stopPropagation()实际上并不是鼠标事件MouseEvent的属性,它是合成事件上的属性,来看看声明文件中的定义:
interface MouseEvent<T = Element, E = NativeMouseEvent> extends UIEvent<T, E> { //... } interface UIEvent<T = Element, E = NativeUIEvent> extends SyntheticEvent<T, E> { //... } interface SyntheticEvent<T = Element, E = Event> extends BaseSyntheticEvent<E, EventTarget & T, EventTarget> {} interface BaseSyntheticEvent<E = object, C = any, T = any> { nativeEvent: E; currentTarget: C; target: T; bubbles: boolean; cancelable: boolean; defaultPrevented: boolean; eventPhase: number; isTrusted: boolean; preventDefault(): void; isDefaultPrevented(): boolean; stopPropagation(): void; isPropagationStopped(): boolean; persist(): void; timeStamp: number; type: string; } 复制代码
可以看到,这里的stopPropagation()是一层层的继承来的,最终来自于BaseSyntheticEvent合成事件类型。原生的事件集合SyntheticEvent就是继承自合成时间类型。SyntheticEvent<T = Element, E = Event>泛型接口接收当前的元素类型和事件类型,如果不介意这两个参数的类型,完全可以这样写:
<input onChange={(e: SyntheticEvent<Element, Event>)=>{ //... }} /> 复制代码
2. 事件处理函数类型
说完事件对象类型,再来看看事件处理函数的类型。React也为我们提供了贴心的提供了事件处理函数的类型声明,来看看所有的事件处理函数的类型声明:
type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void }["bivarianceHack"]; type ReactEventHandler<T = Element> = EventHandler<SyntheticEvent<T>>; // 剪切板事件处理函数 type ClipboardEventHandler<T = Element> = EventHandler<ClipboardEvent<T>>; // 复合事件处理函数 type CompositionEventHandler<T = Element> = EventHandler<CompositionEvent<T>>; // 拖拽事件处理函数 type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>; // 焦点事件处理函数 type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>; // 表单事件处理函数 type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>; // Change事件处理函数 type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>; // 键盘事件处理函数 type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>; // 鼠标事件处理函数 type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>; // 触屏事件处理函数 type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>; // 指针事件处理函数 type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>; // 界面事件处理函数 type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>; // 滚轮事件处理函数 type WheelEventHandler<T = Element> = EventHandler<WheelEvent<T>>; // 动画事件处理函数 type AnimationEventHandler<T = Element> = EventHandler<AnimationEvent<T>>; // 过渡事件处理函数 type TransitionEventHandler<T = Element> = EventHandler<TransitionEvent<T>>; 复制代码
这里面的T的类型也都是Element,指的是触发该事件的HTML标签元素的类型,下面第五部分会介绍。
EventHandler会接收一个E,它表示事件处理函数中 Event 对象的类型。bivarianceHack 是事件处理函数的类型定义,函数接收一个 Event 对象,并且其类型为接收到的泛型变量 E 的类型, 返回值为 void。
还看上面的那个例子:
type State = { text: string; }; const App: React.FC = () => { const [text, setText] = useState<string>("") const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { setText(e.currentTarget.value); }; return ( <div> <input type="text" value={text} onChange={onChange} /> </div> ); } 复制代码
这里给onChange方法定义了方法的类型,它是一个ChangeEventHandler的类型,并且作用的对象时一个HTMLImnputElement类型的标签(input标签)。
五、HTML标签类型
1. 常见标签类型
在项目的依赖文件中可以找到HTML标签相关的类型声明文件:
所有的HTML标签的类型都被定义在 intrinsicElements 接口中,常见的标签及其类型如下:
a: HTMLAnchorElement; body: HTMLBodyElement; br: HTMLBRElement; button: HTMLButtonElement; div: HTMLDivElement; h1: HTMLHeadingElement; h2: HTMLHeadingElement; h3: HTMLHeadingElement; html: HTMLHtmlElement; img: HTMLImageElement; input: HTMLInputElement; ul: HTMLUListElement; li: HTMLLIElement; link: HTMLLinkElement; p: HTMLParagraphElement; span: HTMLSpanElement; style: HTMLStyleElement; table: HTMLTableElement; tbody: HTMLTableSectionElement; video: HTMLVideoElement; audio: HTMLAudioElement; meta: HTMLMetaElement; form: HTMLFormElement; 复制代码
那什么时候会使用到标签类型呢,上面第四部分的Event事件类型和事件处理函数类型中都使用到了标签的类型。上面的很多的类型都需要传入一个ELement类型的泛型参数,这个泛型参数就是对应的标签类型值,可以根据标签来选择对应的标签类型。这些类型都继承自HTMLElement类型,如果使用时对类型类型要求不高,可以直接写HTMLELement。比如下面的例子:
<Button type="text" onClick={(e: React.MouseEvent<HTMLElement>) => { handleOperate(); e.stopPropagation(); }} > <img src={cancelChangeIcon} alt="" /> 取消修改 </Button> 复制代码
其实,在直接操作DOM时也会用到标签类型,虽然我们现在通常会使用框架来开发,但是有时候也避免不了直接操作DOM。比如我在工作中,项目中的某一部分组件是通过npm来引入的其他组的组件,而在很多时候,我有需要动态的去个性化这个组件的样式,最直接的办法就是通过原生JavaScript获取到DOM元素,来进行样式的修改,这时候就会用到标签类型。
来看下面的例子:
document.querySelectorAll('.paper').forEach(item => { const firstPageHasAddEle = (item.firstChild as HTMLDivElement).classList.contains('add-ele'); if (firstPageHasAddEle) { item.removeChild(item.firstChild as ChildNode); } }) 复制代码
这是我最近写的一段代码(略微删改),在第一页有个add-ele元素的时候就删除它。这里我们将item.firstChild断言成了HTMLDivElement类型,如果不断言,item.firstChild的类型就是ChildNode,而ChildNode类型中是不存在classList属性的,所以就就会报错,当我们把他断言成HTMLDivElement类型时,就不会报错了。很多时候,标签类型可以和断言(as)一起使用。
后面在removeChild时又使用了as断言,为什么呢?item.firstChild不是已经自动识别为ChildNode类型了吗?因为TS会认为,我们可能不能获取到类名为paper的元素,所以item.firstChild的类型就被推断为ChildNode | null,我们有时候比TS更懂我们定义的元素,知道页面一定存在paper 元素,所以可以直接将item.firstChild断言成ChildNode类型。
2. 标签属性类型
众所周知,每个HTML标签都有自己的属性,比如Input框就有value、width、placeholder、max-length等属性,下面是Input框的属性类型定义:
interface InputHTMLAttributes<T> extends HTMLAttributes<T> { accept?: string | undefined; alt?: string | undefined; autoComplete?: string | undefined; autoFocus?: boolean | undefined; capture?: boolean | string | undefined; checked?: boolean | undefined; crossOrigin?: string | undefined; disabled?: boolean | undefined; enterKeyHint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send' | undefined; form?: string | undefined; formAction?: string | undefined; formEncType?: string | undefined; formMethod?: string | undefined; formNoValidate?: boolean | undefined; formTarget?: string | undefined; height?: number | string | undefined; list?: string | undefined; max?: number | string | undefined; maxLength?: number | undefined; min?: number | string | undefined; minLength?: number | undefined; multiple?: boolean | undefined; name?: string | undefined; pattern?: string | undefined; placeholder?: string | undefined; readOnly?: boolean | undefined; required?: boolean | undefined; size?: number | undefined; src?: string | undefined; step?: number | string | undefined; type?: string | undefined; value?: string | ReadonlyArray<string> | number | undefined; width?: number | string | undefined; onChange?: ChangeEventHandler<T> | undefined; } 复制代码
如果我们需要直接操作DOM,就可能会用到元素属性类型,常见的元素属性类型如下:
- HTML属性类型:HTMLAttributes
- 按钮属性类型:ButtonHTMLAttributes
- 表单属性类型:FormHTMLAttributes
- 图片属性类型:ImgHTMLAttributes
- 输入框属性类型:InputHTMLAttributes
- 链接属性类型:LinkHTMLAttributes
- meta属性类型:MetaHTMLAttributes
- 选择框属性类型:SelectHTMLAttributes
- 表格属性类型:TableHTMLAttributes
- 输入区属性类型:TextareaHTMLAttributes
- 视频属性类型:VideoHTMLAttributes
- SVG属性类型:SVGAttributes
- WebView属性类型:WebViewHTMLAttributes
一般情况下,我们是很少需要在项目中显式的去定义标签属性的类型。如果子级去封装组件库的话,这些属性就能发挥它们的作用了。来看例子(来源于网络,仅供学习):
import React from 'react'; import classNames from 'classnames' export enum ButtonSize { Large = 'lg', Small = 'sm' } export enum ButtonType { Primary = 'primary', Default = 'default', Danger = 'danger', Link = 'link' } interface BaseButtonProps { className?: string; disabled?: boolean; size?: ButtonSize; btnType?: ButtonType; children: React.ReactNode; href?: string; } type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement> // 使用 交叉类型(&) 获得我们自己定义的属性和原生 button 的属性 type AnchorButtonProps = BaseButtonProps & React.AnchorHTMLAttributes<HTMLAnchorElement> // 使用 交叉类型(&) 获得我们自己定义的属性和原生 a标签 的属性 export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps> //使用 Partial<> 使两种属性可选 const Button: React.FC<ButtonProps> = (props) => { const { disabled, className, size, btnType, children, href, ...restProps } = props; const classes = classNames('btn', className, { [`btn-${btnType}`]: btnType, [`btn-${size}`]: size, 'disabled': (btnType === ButtonType.Link) && disabled // 只有 a 标签才有 disabled 类名,button没有 }) if(btnType === ButtonType.Link && href) { return ( <a className={classes} href={href} {...restProps} > {children} </a> ) } else { return ( <button className={classes} disabled={disabled} // button元素默认有disabled属性,所以即便没给他设置样式也会和普通button有一定区别 {...restProps} > {children} </button> ) } } Button.defaultProps = { disabled: false, btnType: ButtonType.Default } export default Button; 复制代码
这段代码就是用来封装一个buttom按钮,在button的基础上添加了一些自定义属性,比如上面将button的类型使用交叉类型(&)获得自定义属性和原生 button 属性 :
type NativeButtonProps = BaseButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement> 复制代码
可以看到,标签属性类型在封装组件库时还是很有用的,更多用途可以自己探索~