简单记录一下 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),跟React的diff
算法一样。它是有如下假设的,列表里面的数据注意是用来添加删除的,基本上可以看出是无须的,举个简单的例子。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 就被清除了。