[源码分析] Antd-RC-Notification

Notification

全局通知提示框,和Message相比,Notification主要用于展示较为完整的通知内容,且允许带上交互。这里引用Antd官网的使用说明:

  • 较为复杂的通知内容。
  • 带有交互的通知,给出用户下一步的行动点。
  • 系统主动推送。

Rc-Notification

Antd的Notification和Message组件的底层都是rc-notification, 它的执行流程如下:

核心在于维护一个task queue,里面存储着所有要渲染的通知框配置,组件会根据不同配置的placement,将配置分别派发至对应的渲染队列渲染通知框。

Notifications

当我们传入配置时,会在Notifications中将其写入configList state,而后根据它的Placement将其派发至不同的渲染队列,并调用createPortal将组件挂载到指定的container。

向外暴露了open, close, destroy方法,分别用于打开,关闭一个task,以及销毁整个队列的task。

typescript 复制代码
// ========================= Refs =========================
  React.useImperativeHandle(ref, () => ({
    // Create
    open: (config) => {
      setConfigList((list) => {
        let clone = [...list];

        // Replace if exist
        const index = clone.findIndex((item) => item.key === config.key);
        const innerConfig: InnerOpenConfig = { ...config };
        if (index >= 0) {
          innerConfig.times = ((list[index] as InnerOpenConfig)?.times || 0) + 1;
          clone[index] = innerConfig;
        } else {
          innerConfig.times = 0;
          clone.push(innerConfig);
        }

        if (maxCount > 0 && clone.length > maxCount) {
          clone = clone.slice(-maxCount);
        }

        return clone;
      });
    },
    // Close a task
    close: (key) => {
      onNoticeClose(key);
    },
    // Destroy all tasks
    destroy: () => {
      setConfigList([]);
    },
  }));

NoticeList

渲染分发好的Task, 根据placement生成对应的css,根据传入的stack,计算出通知框的位移量,调用CSSMotionList为组件添加对应的移入移出动画。

Calculate the stack

typescript 复制代码
// If dataIndex is -1, that means this notice has been removed in data, but still in dom
// Should minus (motionIndex - 1) to get the correct index because keys.length is not the same as dom length
const stackStyle: CSSProperties = {};
if (stack) {
  const index = keys.length - 1 - (dataIndex > -1 ? dataIndex : motionIndex - 1);
  const transformX = placement === "top" || placement === "bottom" ? "-50%" : "0";
  if (index > 0) {
    stackStyle.height = expanded
      ? dictRef.current[strKey]?.offsetHeight
      : latestNotice?.offsetHeight;

    // Transform
    let verticalOffset = 0;
    for (let i = 0; i < index; i++) {
      verticalOffset += dictRef.current[keys[keys.length - 1 - i].key]?.offsetHeight + gap;
    }

    const transformY = (expanded ? verticalOffset : index * offset) *
      (placement.startsWith("top") ? 1 : -1);
    const scaleX = !expanded && latestNotice?.offsetWidth && dictRef.current[strKey]?.offsetWidth
        ? (latestNotice?.offsetWidth - offset * 2 * (index < 3 ? index : 3)) /
          dictRef.current[strKey]?.offsetWidth
        : 1;
    stackStyle.transform = `translate3d(${transformX}, ${transformY}px, 0) scaleX(${scaleX})`;
  } else {
    stackStyle.transform = `translate3d(${transformX}, 0, 0)`;
  }
}

Notice

渲染我们实际看到的通知框内容,以及实现duration倒计时,Pause On Hover等功能

倒计时

当duration大于0,且鼠标不在通知框上时,会开启倒计时。这里维护一个用于记录已经过时间的状态值: spentTime,当鼠标移到通知框上时,会停止倒计时,并记录下已经经过的时间值,下次倒计时的时间将会是duration - spentTime后的时间。

typescript 复制代码
 const [spentTime, setSpentTime] = React.useState(0);
 
// ======================== Effect ========================
  React.useEffect(() => {
    if (!mergedHovering && duration > 0) {
      // 记录开始时间(减去已经经过的时间的差值)
      const start = Date.now() - spentTime;
      const timeout = setTimeout(
        () => {
          onInternalClose();
        },
        duration * 1000 - spentTime,
      );

      return () => {
        if (pauseOnHover) {
          clearTimeout(timeout);
        }
        // 记录结束时当前时间和开始时间的差值
        setSpentTime(Date.now() - start);
      };
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [duration, mergedHovering, times]);

useNotification

调用Notifications,使用ref获取该组件暴露出来的方法:

typescript 复制代码
 const contextHolder = (
    <Notifications
      container={container}
      ref={notificationsRef}
      prefixCls={prefixCls}
      motion={motion}
      maxCount={maxCount}
      className={className}
      style={style}
      onAllRemoved={onAllRemoved}
      stack={stack}
      renderNotifications={renderNotifications}
    />
 );

维护一组api,传入配置时,会打上对应的type:

typescript 复制代码
// ========================= Refs =========================
const api = React.useMemo<NotificationAPI>(() => {
  return {
    open: (config) => {
      const mergedConfig = mergeConfig(shareConfig, config);
      if (mergedConfig.key === null || mergedConfig.key === undefined) {
        mergedConfig.key = `rc-notification-${uniqueKey}`;
        uniqueKey += 1;
      }
      setTaskQueue((queue) => [
        ...queue,
        { type: "open", config: mergedConfig },
      ]);
    },
    close: (key) => {
      setTaskQueue((queue) => [...queue, { type: "close", key }]);
    },
    destroy: () => {
      setTaskQueue((queue) => [...queue, { type: "destroy" }]);
    },
  };
}, []);

随后遍历这个队列,根据不同的type,调用Notifications中暴露出的方法:

typescript 复制代码
// ======================== Effect ========================
React.useEffect(() => {
  // Flush task when node ready
  if (notificationsRef.current && taskQueue.length) {
    taskQueue.forEach((task) => {
      switch (task.type) {
        case "open":
          notificationsRef.current.open(task.config);
          break;

        case "close":
          notificationsRef.current.close(task.key);
          break;

        case "destroy":
          notificationsRef.current.destroy();
          break;
      }
    });
    // React 17 will mix order of effect & setState in async
    // - open: setState[0]
    // - effect[0]
    // - open: setState[1]
    // - effect setState([]) * here will clean up [0, 1] in React 17
    setTaskQueue((oriQueue) =>
      oriQueue.filter((task) => !taskQueue.includes(task))
    );
  }
}, [taskQueue]);

最后返回api和contextHolder:

typescript 复制代码
// ======================== Return ========================
return [api, contextHolder];
相关推荐
Canace2 小时前
AI 生成到 90% 突然断了:你的解决方案是?
前端·人工智能
TinssonTai2 小时前
Vite 8 版 Chrome 插件全家桶,popup/options/sidepanel 一次集齐
前端·vue.js
玉鸯2 小时前
让 Agent 面向用户:AG-UI 协议构建 Agent 前端
前端·python·agent
程序员黑豆3 小时前
鸿蒙应用开发 @Extend 装饰器使用教程
前端·harmonyos
杨先生哦3 小时前
【2026热端攻防系列 10/12】前端凭据安全深度攻防:Cookie/Storage劫持、会话固定、凭据泄露与浏览器最新加固方案
前端·笔记·安全·web安全
莫石3 小时前
坦克打无人机模拟(three-tile 地形)
前端
hunterandroid3 小时前
[鸿蒙从零到一] HarmonyOS 任务调度与并发模型实战:taskpool、Worker 与可取消任务
前端
颜进强3 小时前
前端看后端 14:什么是 CORS?
前端·后端
sugar__salt4 小时前
Vue3 自定义指令与插槽(Slot)技术详解
前端·javascript·vue.js·前端框架·vue
Csvn4 小时前
🎯 Flex 布局的 `min-width: auto` 陷阱:为什么内容总是撑破容器?
前端