rc component notification

简单记录一下 Notifications 里面使用到的 CSSMotionList 的一段代码。

先看下面代码片段,Notification.add()当,notice 的数量超过最大数量时,Antd 是把第一个的 key 借给最新插入的这个 notice 使用,做法是保留 key,赋值给新插入的 notice,它自己的 key 变成 userPassKey,然后,notices 数组里面删除老的第一个新的插入末尾,简单说就是1,2,3,4-> 2,3,4,1, 用了shift() 方法,代码如下。

add = (originNotice: NoticeContent, holderCallback?: HolderReadyCallback) => {
  const key = originNotice.key || getUuid();
  const notice: NoticeContent & { key: React.Key; userPassKey?: React.Key } = {
    ...originNotice,
    key,
  };
  const { maxCount } = this.props;
  this.setState((previousState: NotificationState) => {
    const { notices } = previousState;
    const noticeIndex = notices.map((v) => v.notice.key).indexOf(key);
    const updatedNotices = notices.concat();
    if (noticeIndex !== -1) {
      updatedNotices.splice(noticeIndex, 1, { notice, holderCallback });
    } else {
      if (maxCount && notices.length >= maxCount) {
        // XXX, use key of first item to update new added (let React to move exsiting
        // instead of remove and mount). Same key was used before for both a) external
        // manual control and b) internal react 'key' prop , which is not that good.
        // eslint-disable-next-line no-param-reassign

        // zombieJ: Not know why use `updateKey`. This makes Notice infinite loop in jest.
        // Change to `updateMark` for compare instead.
        // https://github.com/react-component/notification/commit/32299e6be396f94040bfa82517eea940db947ece
        notice.key = updatedNotices[0].notice.key as React.Key;
        notice.updateMark = getUuid();

        // zombieJ: That's why. User may close by key directly.
        // We need record this but not re-render to avoid upper issue
        // https://github.com/react-component/notification/issues/129
        notice.userPassKey = key;

        updatedNotices.shift();
      }
      updatedNotices.push({ notice, holderCallback });
    }
    return {
      notices: updatedNotices,
    };
  });
};

而在CSSMotionList里面是如何体现这一变化的呢。它里面注意是采用diff算法来发现改变,它的 diff 算法的时间复杂度是O(N),跟Reactdiff算法一样。它是有如下假设的,列表里面的数据注意是用来添加删除的,基本上可以看出是无须的,举个简单的例子。1,2,3 变成了1,3,4 算法会翻译成1:keep,2:Delete,3:Keep,4:Add。对于特例,1,2,3 变成2,3,1,它会先翻译成2:Add,3:Add,1:Keep,2:Delete,3:Delete, 然后进一步修正成 2:Add,3:Add,1:Keep,然后再次修正成2:Keep,3:Keep,1:Keep。所以对于调换顺序它的做法有点不那么自然,看如下代码。

export function diffKeys(
  prevKeys: KeyObject[] = [],
  currentKeys: KeyObject[] = []
) {
  let list: KeyObject[] = [];
  let currentIndex = 0;
  const currentLen = currentKeys.length;

  const prevKeyObjects = parseKeys(prevKeys);
  const currentKeyObjects = parseKeys(currentKeys);

  // Check prev keys to insert or keep
  prevKeyObjects.forEach((keyObj) => {
    let hit = false;

    for (let i = currentIndex; i < currentLen; i += 1) {
      const currentKeyObj = currentKeyObjects[i];
      if (currentKeyObj.key === keyObj.key) {
        // New added keys should add before current key
        if (currentIndex < i) {
          list = list.concat(
            currentKeyObjects
              .slice(currentIndex, i)
              .map((obj) => ({ ...obj, status: STATUS_ADD }))
          );
          currentIndex = i;
        }
        list.push({
          ...currentKeyObj,
          status: STATUS_KEEP,
        });
        currentIndex += 1;

        hit = true;
        break;
      }
    }

    // If not hit, it means key is removed
    if (!hit) {
      list.push({
        ...keyObj,
        status: STATUS_REMOVE,
      });
    }
  });

  // Add rest to the list
  if (currentIndex < currentLen) {
    list = list.concat(
      currentKeyObjects
        .slice(currentIndex)
        .map((obj) => ({ ...obj, status: STATUS_ADD }))
    );
  }

  /**
   * Merge same key when it remove and add again:
   *    [1 - add, 2 - keep, 1 - remove] -> [1 - keep, 2 - keep]
   */
  const keys = {};
  list.forEach(({ key }) => {
    keys[key] = (keys[key] || 0) + 1;
  });
  const duplicatedKeys = Object.keys(keys).filter((key) => keys[key] > 1);
  duplicatedKeys.forEach((matchKey) => {
    // Remove `STATUS_REMOVE` node.
    list = list.filter(
      ({ key, status }) => key !== matchKey || status !== STATUS_REMOVE
    );

    // Update `STATUS_ADD` to `STATUS_KEEP`
    list.forEach((node) => {
      if (node.key === matchKey) {
        // eslint-disable-next-line no-param-reassign
        node.status = STATUS_KEEP;
      }
    });
  });

  return list;
}

简单介绍一下,Notification 调用的过程中,发生了什么,什么顺序.

  • 首先调用 Notification.newInstance(props,callback),这个时候在整个 React VirtualDom 创建了一个树,这颗树跟我们的 App 的树地位一样,它们应该与 App 树放在 React 内部同一个数组里面. props 里面可以配置 notification 容器大小,以及,getContainer来指定容器挂载在 Dom 的位置等。
  • callback.notice(noticeProps)来打开一个 noticie. 它会往容器的 states 里面插入一条新的 notice,可以参考上面的代码。
  • Notification 会被 React 触发检查更新。CSSMotionList 会发现更新后的 notices list。然后新添加的 notice 会变成一个新的 Motion,而它里面套着正在的 notice。notice 的状态是 ADD(diff) 或者 Keep 的 Motion 对应的 props 的 visible=true,动画开始.
  • notice 会被 CSSMotion 通过动画(或者保持不变) render 出来,然后 notice 里面会有个 timeout,到时候自动关闭,关闭时通过调用 notification 里面定义的 onClose 方法,onClose 里面会调用 remove()(这个是更新 state 里面的 notices[]),然后调用 notice.onClose()(notice 的回调)。 remove() 方法就是把节点从 state 里面的 notices 数组里面对应的节点移除(notification 里面有两个数组,一个是这个存放在 state 里面,另一个是由这个计算出来,传递给 CSSMotionList 的),然后 CSSMotionList 会发现差异(在检查更新里面),它会把移除的节点会被标记成 REMOVE,它的 visible 会变成 false,传递给 CSSMotion, CSSMotion 会走关闭的动画。CSSMotion 会调用 CSSMotionList.onVisibleChanged 方法(动画结束后),这个方法里面除了向上抛方法,还会调用自身的 removeKey 方法,将节点 keyEntities 里面的节点标记成 Removed 状态。在 notification 里面的 onVisibleChanged 方法里面把 key 对应的 map 清除掉。在 CSSMotionList getDerivedStateFromProps 里面会被清除掉,(不知道为啥要这样写,直接清除掉不香吗?知道原因了,是因为动画, 进来,出去,变化都是要动画的) 然后对于的 Motion Node 就被清除了。

介绍一下使用 Notification 遇到的一个问题

Warning: Render methods should be a pure function of props and state; triggering nested component updates from render is not allowed. If necessary, trigger nested updates in componentDidUpdate.

为什么会出现一个问题?看下面代码示意,一开始的想法是,我如何提供 NotificationInstance,受制于 angular DI 的影响,我一开始的想法是通过 Context。我可以将 NotificationInstance 放到 Context 里面,这样子孙后代就可以拿到了。所以,我在 Context 里面调用了Notification.newInstance(),而,这个 ContextProvider 是作为 App 主树的一个分支,React 不允许在 render 方法里面去构建一个新的树,所以报错了上面的错误。因为Notification.newInstance() 里面会调用 ReactDOM.render(<Notification {...props} ref={ref} />, div);
然后我想到了两个方法,结果调到了另外两个坑里,一个是我只要setTimeout 或者React.useEffect 来封装Notification.newInstance(), 不就可以骗过 React。但是还是不行。如下RCDemo里面会报Hook函数调用顺序不一致的错误,因为第一次rcNotificationContext是空,所以下一行的 hook 函数没有值,不会执行,而异步后,rcNotificationContext有值了,hook 函数执行,所以报错了。

<RcNotificationContextProvider>
  <RcDemo />
</RcNotificationContextProvider>;
//RcNotificationContextProvider 定义
export const RcNotificationContextProvider: React.FC = ({ children }) => {
  const [instance, setInstance] = useState<NotificationInstance>();

  if (instance === undefined) {
    //正是下面的这个调用报错了
    Notification.newInstance(
      {
        maxCount: 5,
      },
      (instance) => {
        setInstance(instance);
      }
    );
  }
  return (
    <RcNotificationContext.Consumer>
      {(originalInstance) => (
        <RcNotificationContext.Provider value={notificationInstance}>
          {children}
        </RcNotificationContext.Provider>
      )}
    </RcNotificationContext.Consumer>
  );
  //RcDemo 一开始的代码片段。
  const rcNotificationContext = React.useContext(RcNotificationContext);
  const [notice, elements] = rcNotificationContext?.useNotification() || [];
};

正确的使用方式就是,就在某个文件里面放置如下代码, 然后直接导入 notificationInstance 使用就行了。为什么这样可以,而且 newInstance 方法只执行一次。这个就要讲到 es6 里面的模块加载了。es6 里面模块加载首先 export/import 的是地址引用,不是值拷贝(es5), notificationInstance, 它的值要等到后面才会有, 然后,模块本身确保了只执行一次的特性。

let notificationInstance: NotificationInstance;
Notification.newInstance(
  {
    maxCount: 5,
  },
  (instance) => {
    notificationInstance = instance;
  }
);
export default notificationInstance;

//RCDemo
//这个import 会导致执行上面的文件
import notificationInstance from "../../../RcNotification.context";

const NotificationDemo: React.FC = () => {
  const [notice, elements] = notificationInstance.useNotification() || [];

  //xxx
};

总结一下,React 里面不推荐使用 Context,官方也不推荐,因为,官方说会使得调用栈变得很复杂,不可控。而 Angular 里面的 DI 是它最强大的武器,这一点跟 React 的差别很大。

原文地址:https://www.cnblogs.com/kongshu-612/p/14940782.html