本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
工作中比较常用移动端组件,message没怎么用过。所以选择了移动端的toast,相对antd,antd-mobile的代码简单多了
antd-mobile文档:mobile.ant.design/zh/componen…
源码
这个是导出Toast组件的文件,可以看到主要方法都是从methods文件中导出的。所以主要看这个文件。
import './toast.less'
import { clear, show, config } from './methods'
export type { ToastShowProps, ToastHandler } from './methods'
// 导出这三个方法
const Toast = {
show,
clear,
config,
}
export default Toast
import React, {
createRef,
forwardRef,
useEffect,
useImperativeHandle,
useState,
} from 'react'
import { resolveContainer } from '../../utils/get-container'
import ReactDOM from 'react-dom'
import { InternalToast, ToastProps } from './toast'
import { mergeProps } from '../../utils/with-default-props'
const containers = [] as HTMLDivElement[]
function unmount(container: HTMLDivElement) {
// https://zh-hans.reactjs.org/docs/react-dom.html#unmountcomponentatnode
// 在 React 18 中,unmountComponentAtNode 已被 root.unmount() 取代
const unmountResult = ReactDOM.unmountComponentAtNode(container) // 从react上卸载组件。
if (unmountResult && container.parentNode) {
container.parentNode.removeChild(container) // 从dom上卸载组件
}
}
export type ToastShowProps = Omit<ToastProps, 'visible'>
const defaultProps = {
duration: 2000, // 提示持续时间,若为 0 则不会自动关闭
position: 'center', // 垂直方向显示位置
maskClickable: true, // 是否允许背景点击
}
export type ToastHandler = {
close: () => void
}
type ToastShowRef = ToastHandler
export function show(p: ToastShowProps | string) {
// 合并props
const props = mergeProps(
defaultProps,
typeof p === 'string' ? { content: p } : p // 如果只传入了一个string类型,就做toast的content
)
let timer = 0
const { getContainer = () => document.body } = props // toast信息的提示的容器
const container = document.createElement('div') // 创建div
const bodyContainer = resolveContainer(getContainer) // 看看容器是 用户传入的还是默认的
bodyContainer.appendChild(container) // 把创建的div 追加到 容器上
clear() // 创建新的把之前的删掉。理论上,创建一个删一个,所以数组里应该只有一个。如果是异步可能会排队?
containers.push(container) // 把新的放到数组里
// forwardRef 传递函数组件的ref
const TempToast = forwardRef<ToastShowRef>((_, ref) => {
const [visible, setVisible] = useState(true) // 控制开关状态
useEffect(() => {
return () => {
props.afterClose?.() // Toast 完全关闭后的回调
}
}, [])
useEffect(() => {
if (props.duration === 0) { // 如果duration设置了0,直接停止逻辑 就不会关闭
return
}
timer = window.setTimeout(() => { // 然后看传入的时间,几秒后关闭toast
setVisible(false)
}, props.duration)
return () => {
window.clearTimeout(timer)
}
}, [])
// https://zh-hans.reactjs.org/docs/hooks-reference.html#useimperativehandle
// 配合 forwardRef 使用,父组件只能通过 ref 获取 close 这一个方法
useImperativeHandle(ref, () => ({
close: () => setVisible(false),
}))
return (
<InternalToast
{...props}
getContainer={() => container}
visible={visible}
afterClose={() => {
unmount(container)
}}
/>
)
})
const ref = createRef<ToastShowRef>()
ReactDOM.render(<TempToast ref={ref} />, container)
return {
close: () => { // show的返回值
ref.current?.close()
},
} as ToastHandler
}
// 会直接卸载组件
export function clear() {
while (true) { // 一直取
const container = containers.pop() // 从数组里面取出来
if (!container) break // 如果取没了,就不循环了
unmount(container) // 取出来最后一个卸载
}
}
// 设置全局的默认值
export function config(
val: Pick<ToastProps, 'duration' | 'position' | 'maskClickable'>
) {
if (val.duration !== undefined) {
defaultProps.duration = val.duration
}
if (val.position !== undefined) {
defaultProps.position = val.position
}
if (val.maskClickable !== undefined) {
defaultProps.maskClickable = val.maskClickable
}
}
总结
卸载组件
unmountComponentAtNode会卸载 react组件的 state 和 事件,卸载成功后会返回true- 然后从父亲身上卸载
dom
function unmount(container: HTMLDivElement) {
// https://zh-hans.reactjs.org/docs/react-dom.html#unmountcomponentatnode
// 在 React 18 中,unmountComponentAtNode 已被 root.unmount() 取代
const unmountResult = ReactDOM.unmountComponentAtNode(container) // 从react上卸载组件。
if (unmountResult && container.parentNode) {
container.parentNode.removeChild(container) // 从dom上卸载组件
}
}
afterClose关闭后执行
利用 useEffect 组件销毁前执行函数返回值
useEffect(() => {
return () => {
props.afterClose?.() // Toast 完全关闭后的回调
}
}, [])
useImperativeHandle
下面是react官网提供的例子,这样父组件只能在ref中获取子组件的focus方法,保护子组件的dom,开发业务的时候可能用到的不多。在组件开发的时候,这个api还是可以很好防止其他人滥用dom上的方法。
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
Omit(ts类型)
忽略 ToastProps 中的 visible 属性
export type ToastShowProps = Omit<ToastProps, 'visible'> // 忽略ToastProps 中的 visible 属性