Ant Design 源码分析之Notification

1. 使用场景

​ 全局提示通知提醒信息

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

2. Notification API

  • notification.success(config)
  • notification.error(config)
  • notification.info(config)
  • notification.warning(config)
  • notification.warn(config)
  • notification.open(config)
  • notification.close(key: String)
  • notification.destroy()

3. 源码分析

3.1 设计思路

1.png

使用 notification class 中的方法生成对应的位置的 notification component,
使用notification component 渲染 notice components

  • notificationInstance

来进行对创建 notification component实例的缓存,然后在销毁时将缓存的实例删除

const notificationInstance = {}; 

// 销毁实例
destroy() {
  Object.keys(notificationInstance).forEach(cacheKey => {
    notificationInstance[cacheKey].destroy()
    delete notificationInstance[cacheKey]
  })
}
// 缓存实例 单例模式 防止实例重复生成
getNotificationInstance(
  {
      //.......省略部分代码........
  },
  callback: (n: any) => void,
) {
  const cacheKey = `${prefixCls}-${placement}`;
  if (notificationInstance[cacheKey]) {
    callback(notificationInstance[cacheKey]);
    return;
  }
  (Notification as any).newInstance(
    {
      //.......省略部分代码........
    },
    (notification: any) => {
      // 缓存实例
      notificationInstance[cacheKey] = notification;
      callback(notification);
    },
  );
}
复制代码

2.png

3.png

  • noticeInstance

    class Notification extends Component<NotificationProps, NotificationState> {
      state: NotificationState = {
        notices: [], // notice实例缓存
      }
      add = (notice) => {
        const key = notice.key = notice.key || getUuid();
        this.setState(previousState => {
        const notices = previousState.notices;
        if (!notices.filter(v => v.key === key).length) {
          return {
            notices: notices.concat(notice),
          };
        }
      });
       // 销毁实例
      remove = (key) => {
        this.setState(previousState => {
          return {
            notices: previousState.notices.filter(notice => notice.key !== key),
          };
        });
      }
      render() {
        const { notices } = this.state;
        const noticeNodes = notices.map((notice, index) => {
          //.......省略部分代码........
          return (
            <Notice
              prefixCls={prefixCls}
              closeIcon={closeIcon}
              {...notice}
              key={key}
              update={update}
              onClose={onClose}
              onClick={notice.onClick}
            >
              {notice.content}
            </Notice>
          );
        });
        return (
          <>
            {noticeNodes}
          </>
        );
      }
    }
    
    复制代码

    3.2 实现细节

    分析代码之前我们先来看下 notification 组件的结构,主要分成三层

4.png

  • NotificationApi
    调用堆栈如图

5.png

notificationApi 组件的封装,提供了类似notification.success notification.error notification.open notification.destroy 等api接口

const api: any = {
  open: notice,
  close(key: string) {
    Object.keys(notificationInstance).forEach(cacheKey =>
      notificationInstance[cacheKey].removeNotice(key),
    );
  },
  config: setNotificationConfig,
  destroy() {
    Object.keys(notificationInstance).forEach(cacheKey => {
      notificationInstance[cacheKey].destroy();
      delete notificationInstance[cacheKey];
    });
  },
};

['success', 'info', 'warning', 'error'].forEach(type => {
  api[type] = (args: ArgsProps) =>
    api.open({
      ...args,
      type,
    });
});
复制代码

看上图NotificationApi 暴露了几个接口,大部分是通过类似open的一个工厂函数实现的,open方法又指向了notice,来看下notice

function notice(args: ArgsProps) {
  const outerPrefixCls = args.prefixCls || 'ant-notification';
  //.......省略部分代码........
  const { placement, top, bottom, getContainer } = args;
  // 生成实例方法 调用实例暴露出来的接口
  getNotificationInstance(
    {
      //.......省略部分代码........
    },
    (notification: any) => {
      notification.notice({
        content: (),
        duration,
      	......
      });
    },
  );
}
复制代码

getNotificationInstance 命名方式是典型的单例模式,使用单例模式不仅节省了内存空间,而且单例延迟执行的特性也保证了在没有通知的情况下不会生成notification组件,保证了性能

 // 单例模式 防止实例重复生成
function getNotificationInstance({
    prefixCls,
    placement,
    getContainer,
  }, callback: (n: any) => void) {
  const cacheKey = `${prefixCls}-${placement}`;
  if (notificationInstance[cacheKey]) {
    callback(notificationInstance[cacheKey]);
    return;
  }
  // 实例化Notification组件
  (Notification as any).newInstance(
    {
     //.......配置信息省略........
    },
    (notification: any) => {
      // 缓存实例
      notificationInstance[cacheKey] = notification;
      callback(notification);
    },
  );
}

复制代码

getNotificationInstance 第一个参数是配置信息,比如css前缀,弹出位置,通知栏的容器等。根据前两个条件自由组合生成cacheKey,也就是不同的弹出位置/css前缀 会生成多个的 notification实例, 其他情况不管调用多少次都只会生成一个notification 实例,notificationInstance做缓存,调用notification.destroy会用到

  • Notification

    antd 强依赖 react-component组件库,notificationApi 依赖 rc-notification, 该组件主要是暴露 notification实例以及实例方法,通过ReactDOM.render将Notification组件渲染到页面上,可以选择渲染到传入的container或者body中。通过ref将notification实例传入callback回调函数。

    Notification.newInstance = function newNotificationInstance(properties, callback) {
      const { getContainer, ...props } = properties || {};
      const div = document.createElement('div');
      if (getContainer) {
        const root = getContainer();
        root.appendChild(div);
      } else {
        document.body.appendChild(div);
      }
      let called = false;
      // 对外暴露实例的方法 
      function ref(notification: Notification) {
        if (called) {
          return;
        }
        called = true;
        callback({
          notice(noticeProps) {
            notification.add(noticeProps);
          },
          removeNotice(key) {
            notification.remove(key);
          },
          component: notification,
          destroy() {
            // 销毁notification实例容器
            ReactDOM.unmountComponentAtNode(div);
            div.parentNode.removeChild(div);
          },
        });
      }
      // React版本升级到16.x以后 不支持 ReactDOM.render方法生成实例,antd 3.x版本进行修复,使用ref代替
      // 2.x版本解决方案 我也有输出在第4部分内容里
      ReactDOM.render(<Notification {...props} ref={ref} />, div);
    };
    复制代码

    Notification组件主要是维护 notice生成,notice移除,notification实例渲染的 wrapper,noticelist渲染等

    class Notification extends Component<NotificationProps, NotificationState> {
      // 维护了notices列表
      state: NotificationState = {
        notices: [], 
      };
      add = (notice: NoticeContent) => {
       // notificationApi对外暴露了key key是区分生成notice的唯一性 如果没有设置 每次都生成唯一key
        notice.key = notice.key || getUuid();
        const { key } = notice;
        const { maxCount } = this.props;
        // maxCount是对notice生成实例个数限制 rc-notification内部是暴露了maxCount。
        // antd组件内部并没有用到maxCount也没有对外暴露
        this.setState(previousState => {
          const { notices } = previousState;
          const noticeIndex = notices.map(v => v.key).indexOf(key);
          const updatedNotices = notices.concat();
          if (noticeIndex !== -1) {
            updatedNotices.splice(noticeIndex, 1, notice);
          } else {
            if (maxCount && notices.length >= maxCount) {
              notice.updateKey = updatedNotices[0].updateKey || updatedNotices[0].key;
              updatedNotices.shift();
            }
            updatedNotices.push(notice);
          }
          return {
            notices: updatedNotices,
          };
        });
      };
      // 移除notice
      remove = (key: React.Key) => {
        this.setState(previousState => ({
          notices: previousState.notices.filter(notice => notice.key !== key),
        }));
      };
    
      render() {
        const { notices } = this.state;
        const { prefixCls, className, closeIcon, style } = this.props;
        const noticeNodes = notices.map((notice, index) => {
          //.......省略一大串代码........
          const onClose = createChainedFunction(
            this.remove.bind(this, notice.key),
            notice.onClose,
          ) as any;
          return (
            <Notice
              prefixCls={prefixCls}
              closeIcon={closeIcon}
              {...notice}
              key={key}
              update={update}
              onClose={onClose}
              onClick={notice.onClick}
            >
              {notice.content}
            </Notice>
          );
        });
        return (
          <>
            {noticeNodes}
          </>
        );
      }
    }
    
    复制代码

    createChainedFunction

    • 能够删除当前的notification的缓存值
    • 执行外部传入的关闭回调函数
     // rc-utils 按照顺序链式调用函数去执行
     export default function createChainedFunction() {
        const args = [].slice.call(arguments, 0);
        if (args.length === 1) {
          return args[0];
        }
    
        return function chainedFunction() {
          for (let i = 0; i < args.length; i++) {
            if (args[i] && args[i].apply) {
              args[i].apply(this, arguments);
            }
          }
        };
      }
    
    复制代码
  • Notice

    export default class Notice extends Component<NoticeProps> {
      componentDidUpdate(prevProps: NoticeProps) {
        if (this.props.duration !== prevProps.duration || this.props.update) {
          this.restartCloseTimer();
        }
      }
      close = (e?: React.MouseEvent<HTMLAnchorElement>) => {
        if (e) {
          e.stopPropagation();
        }
        this.clearCloseTimer();
        this.props.onClose();
      };
    
      startCloseTimer = () => {
        if (this.props.duration) {
          this.closeTimer = window.setTimeout(() => {
            this.close();
          }, this.props.duration * 1000);
        }
      };
    
      clearCloseTimer = () => {
        if (this.closeTimer) {
          clearTimeout(this.closeTimer);
          this.closeTimer = null;
        }
      };
    
      restartCloseTimer() {
        this.clearCloseTimer();
        this.startCloseTimer();
      }
    
      render() {
        return (
          <></>
        );
      }
    复制代码

4. 参考资料

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享