[源码分析] 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];
相关推荐
轻口味1 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王1 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发2 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀2 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef4 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6414 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻5 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云5 小时前
npm淘宝镜像
前端·npm·node.js