解析 React Transition Group ①

Transition Group 组件

Transition Group 组件用于管理一个 Transition 组件列表,即使 Transition 不声明状态 Transition Group 也会自动为其维护一个内部状态。

  • 自动为子组件列表添加状态并记录
  • 子组件添加或删除时不会被直接渲染,而是被 Transition Group 拦截,当执行完动画逻辑后,在内部状态中删除,并重新渲染。

首次渲染时可以记录子组件并吸收为内部状态,需要注意以下细节:

  • 子组件可能不合法,或没有 key
  • Transition Group 的属性优先级需要高于子组件相同属性的优先级
  • 需要实现清除逻辑给组件被删除时使用,从内部状态中清除被删除的组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
const TransitionGroup = (props) => {
// 是否首次渲染
const firstRender = useRef(true);
useEffect(() => {
firstRender.current = false;
}, []);

const [, rerender] = useState([]);

const latestChildrenRef = useRef(props.children);
latestChildrenRef.current = props.children;

// handleOnExit 是一个组件内的方法
// 需要注意的是它通过 latestChildrenRef 保证 children 永远是最新的
// 因为组件可能被重新渲染,而 handleOnExit 方法可能已经被绑定在了组件上
// 因此,在它真正执行的上下文中无法获取到最近的 children 属性

const handleOnExit = (child, node) => {
const currentMap = getChildrenMapping(latestChildrenRef.current);

// onExit 执行的时候,这个元素可能已经没有了,在外部通过map重新渲染了新列表,所以已经计算的currentChildrenMap 不可靠
if (currentMap.has(child.key)) return;

if (child.props.onExited) {
child.props.onExited(node);
}

preChildrenMap.current.delete(child.key);
rerender([]);
};

const mappedChildren = firstRender.current
? getInitialChildMapping(props, handleOnExit)
: getNextChildrenMap(props, preChildrenMap.current, handleOnExit); // 更新时的逻辑
};

const getInitialChildMapping = (props, handleExit) => {
return getChildrenMapping(props.children, (child) => {
return React.cloneElement(child, {
// 在子组件退出的方法中处理删除内部状态的逻辑
onExited: handleExit(child),

// 初始化状态
in: true,

// Transition Group 的属性优先级需要高于子组件
appear: getProp(child, "appear", props),
enter: getProp(child, "enter", props),
exit: getProp(child, "exit", props),
});
});
};

const getChildrenMapping = (children, fn) => {
const childrenMap = new Map();
const mapper = (child) => {
return fn && React.isValidElement(child) ? fn(child) : child;
};

if (children) {
// map 函数可以自动添加key
React.Children.map(children, (child) => child).forEach((child) => {
childrenMap.set(child.key, mapper(child));
});
}
return childrenMap;
};

更新阶段, 新列表并不一定只会删除其中一个元素,而是可以添加或删除多个元素,而删除操作并不是立即执行的,而是当动画结束后,从内部状态中删除。

这需要一种策略合并两个列表。让旧组件尽量保持在原来的位置上,再将附近的新元素插入到就元素的前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
const mergeMappingChildren = (preChildrenMap, currentChildrenMap) => {
const pre = preChildrenMap || new Map();
const next = currentChildrenMap || new Map();

let pendingQueue = [];
let prePendingMap = new Map();

// 如果新列表中有当前这个旧元素
// 记录这个旧元素之前的元素,放进列表
pre.forEach((preChild) => {
if (next.has(preChild.key)) {
if (pendingQueue.length) {
prePendingMap.set(preChild.key, pendingQueue);
pendingQueue = [];
}
} else {
pendingQueue.push(preChild.key);
}
});

const mergedChildrenMap = new Map();
next.forEach((nextChild) => {
// 新元素排在旧元素的前面
// 旧元素尽可能保持在原有的位置。
if (prePendingMap.has(nextChild.key)) {
const pendingQueue = prePendingMap.get(nextChild.key);
pendingQueue.forEach((key) => {
mergedChildrenMap.set(key, pre.get(key));
});
}
mergedChildrenMap.set(nextChild.key, next.get(nextChild.key));
});

if (pendingQueue.length) {
pendingQueue.forEach((preChild) => {
mergedChildrenMap.set(preChild.key, next.get(preChild.key));
});
}

return mergedChildrenMap;
};

const getNextChildrenMap = (props, preChildrenMap, handleExit) => {
const nextChildrenMap = getChildrenMapping(props.children);
const mergedChildrenMap = mergeMappingChildren(
preChildrenMap,
nextChildrenMap
);

const childrenMap = new Map();
mergedChildrenMap.forEach((child) => {
const preHas = preChildrenMap.has(child.key);
const nextHas = nextChildrenMap.has(child.key);
const isExiting = preHas && !preChildrenMap.get(child.key).props.in;

// 本次更新中被删除的元素
if (preHas && !nextHas && !isExiting) {
childrenMap.set(
child.key,
React.cloneElement(child, {
in: false,
})
);
// 新增的元素,包括正在退出中的元素
} else if ((!preHas || isExiting) && nextHas) {
childrenMap.set(
child.key,
React.cloneElement(child, {
onExited: () => handleExit(child),
in: true,
enter: getProp(child, "enter", props),
exit: getProp(child, "exit", props),
})
);
// 没有改变的元素
} else if (preHas && nextHas && isValidElement(prevChild)) {
childrenMap.set(
child.key,
React.cloneElement(child, {
onExited: () => handleExit(child),
in: preChildrenMap.get(child.key).props.in,
enter: getProp(child, "enter", props),
exit: getProp(child, "exit", props),
})
);
}
});
return childrenMap;
};
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信