【若川视野 x 源码共读】第28期 | vue react 小程序 message 组件(Toast组件)

325 阅读3分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

工作中比较常用移动端组件,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
  }
}

总结

卸载组件

  1. unmountComponentAtNode 会卸载 react组件的 state 和 事件,卸载成功后会返回true
  2. 然后从父亲身上卸载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 属性