前面一篇文章介绍了
React Transition Group中用于单个组件过渡管理的两个组件Transition和CSSTransition。而React Transition Group还有两个用于管理多个组件过渡管理的组件SwitchTransition、TransitionGroup。下面介绍两个组件的用法以及我对其源码的一些研究。
SwitchTransition
SwitchTransition用于管理过渡组件的切换效果。这里的切换指的过渡组件 进入/离开 屏幕之间的切换。
1. SwitchTransition用法
用官方文档的例子说明:
<SwitchTransition mode={mode}>
<CSSTransition
key={state}
nodeRef={nodeRef}
addEndListener={(done) => {
nodeRef.current.addEventListener("transitionend", done, false);
}}
classNames="fade"
>
<div ref={nodeRef} className="button-container">
<Button onClick={() => setState((state) => !state)}>
{state ? "Hello, world!" : "Goodbye, world!"}
</Button>
</div>
</CSSTransition>
</SwitchTransition>
可以看到,SwitchTransition接受单个Transition 或 CSSTransition组件作为子组件,并且有一个属性mode接受string类型值,用于指定切换效果。
| props | 可选 | 解释 |
|---|---|---|
| mode | 可选 | 有两个值可供选择,"out-in"(先展示离开过渡效果,再展示进入过渡效果)、 "in-out"(展示进入过渡效果,再展示离开过渡效果),默认值是"out-in" |
SwitchTransition的子组件Transition 或 CSSTransition的属性、用法并没有改变,需要注意的有两点:
Transition或CSSTransition在作为SwitchTransition子组件时,不需要定义属性in。这是因为SwitchTransition会接管子组件的in属性。Transition或CSSTransition在作为SwitchTransition子组件时,需要定义属性key。这是因为SwitchTransition会通过key属性在渲染前后是否一致,来展示当前的过渡效果。
2. SwitchTransition源码解析
让我们结合源码跑一遍吧
SwitchTransition 中有三种状态(status): ENTERED, ENTERING 和 EXITING。
首先,SwitchTransition的初始状态为ENTERED。记住这里的appeared,之后会用到😀。
class SwitchTransition extends React.Component {
state = {
status: ENTERED,
current: null,
};
// 这里的appeared将会传递给子组件使用,用于指示Transition组件是否展示appear效果(首次进入过渡)
appeared = false;
componentDidMount() {
this.appeared = true;
}
//...
}
开始执行getDerivedStateFromProps方法,这里state.current便是缓存的子组件,初始化后current为空值,status为ENTERED,在这一步进行子组件缓存,in属性值为true。
static getDerivedStateFromProps(props, state) {
if (props.children == null) {
return {
current: null,
};
}
if (state.status === ENTERING && props.mode === modes.in) {
return {
status: ENTERING,
};
}
// 这里如果子组件key属性值发生变更,则status改变,开始切换效果
if (state.current && areChildrenDifferent(state.current, props.children)) {
return {
status: EXITING,
};
}
return {
// 如果 status为ENTERED,则进行子组件缓存
current: React.cloneElement(props.children, {
in: true,
}),
};
}
然后执行render方法,可以看到最终的渲染的子组件是根据status决定的,而我们的初始状态为ENTERED,因此渲染的子组件就是current,因为刚刚才缓存的,所以也就是当前子组件。
render() {
const {
props: { children, mode },
state: { status, current },
} = this;
const data = { children, current, changeState: this.changeState, status };
let component;
// 这里根据status的不同,所渲染的子组件也不同。
switch (status) {
case ENTERING:
component = enterRenders[mode](data);
break;
case EXITING:
component = leaveRenders[mode](data);
break;
case ENTERED:
component = current;
}
return (
<TransitionGroupContext.Provider value={{ isMounting: !this.appeared }}>
{component}
</TransitionGroupContext.Provider>
);
}
假设SwitchTransition的mode属性值为out-in,此时如果我们改变SwitchTransition子组件的key值,将开始切换过渡效果。这是因为在getDerivedStateFromProps中使用areChildrenDifferent方法来判断children是否发生变更。
function areChildrenDifferent(oldChildren, newChildren) {
if (oldChildren === newChildren) return false;
if (
React.isValidElement(oldChildren) &&
React.isValidElement(newChildren) &&
oldChildren.key != null &&
oldChildren.key === newChildren.key
) {
return false;
}
return true;
}
在执行getDerivedStateFromProps方法后,当前的status变为EXITING。在render方法中可以看到,当status为EXITING时,渲染的子组件由leaveRenders[mode](data)决定。记住,我们当前的mode为out-in,children值发生了变更,而state.currrent值则是缓存的children值。
可以看到,leaveRenders将返回current,即缓存的子组件, 并且子组件的in属性值为false,而初始化时current的in属性值为true,因此这里会展示缓存子组件的离开过渡效果,并且current组件定义的onExited钩子,将会在过渡效果消失时,修改SwitchTransition的status值和current值。
const leaveRenders = {
// 当mode为"out-in", status为EXITING时,执行下面方法。
[modes.out]: ({ current, changeState }) =>
React.cloneElement(current, {
in: false,
onExited: callHook(current, 'onExited', () => {
// changeState修改SwitchTransition的state
changeState(ENTERING, null);
}),
}),
[modes.in]: ({ current, changeState, children }) => [
current,
React.cloneElement(children, {
in: true,
onEntered: callHook(children, 'onEntered', () => {
changeState(ENTERING);
}),
}),
],
};
const enterRenders = {
// 当mode为"out-in", status为ENTERING时,执行下面方法。
[modes.out]: ({ children, changeState }) =>
React.cloneElement(children, {
in: true,
onEntered: callHook(children, 'onEntered', () => {
changeState(ENTERED, React.cloneElement(children, { in: true }));
}),
}),
[modes.in]: ({ current, children, changeState }) => [
React.cloneElement(current, {
in: false,
onExited: callHook(current, 'onExited', () => {
changeState(ENTERED, React.cloneElement(children, { in: true }));
}),
}),
React.cloneElement(children, {
in: true,
}),
],
};
过渡效果结束后,SwitchTransition的状态改为ENTERING,current值改为空值。进行再一次渲染,执行getDerivedStateFromProps方法后,current值变为当前children组件的缓存。执行render方法时,当status为ENTERING时,渲染的子组件由enterRenders[mode](data)决定。
可以看到,enterRenders将返回children,即当前子组件,并且子组件的in属性值设为ture。将会展示进入过渡效果,同时也绑定了事件onEntered,status重新回到ENTERING。至此,我们完成一次切换效果。
Q&A
Q1:由于子组件的
key值变更了,当前子组件会重新挂载,然而我们没定义appear属性和appear样式,难道看不到进入过渡效果了吗 ?
还记得SwitchTransition中的appeared吗? 在这里派上用场了,在SwitchTransition组件挂载之后this.appeared值为true,传递给子组件Transition时就会起到appear的属性。而且因为此时Transition的appear属性仍为false,会直接使用enter样式,无需定义appear样式。
Q2: 为什么在前面说
SwitchTransition是管理多个组件过渡的组件?
这是因为React Transition Group在实现SwitchTransition的进入/离开的切换效果时,其实同时用到了两个Transition 或 CSSTransition来实现这个效果。比如实现out-in效果时,会先让之前缓存的子组件展示离开效果,在过渡结束时,让当前的子组件展示进入效果。
Q3:SwitchTransition组件有哪些值得学习的地方?
有很多方面,就比如作者在整体设计思路上,巧妙地运用React.cloneElement来进行子组件缓存、更新,减少了用户接触学习的成本。