[源码分析] 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];
相关推荐
卸任9 小时前
Vibe coding工具使用心得
前端·ai编程
brien99 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance10 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M10 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写10 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李10 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易10 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马10 小时前
前沿视界
前端
kyriewen10 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易11 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员