学习React的过渡管理 React Transition Group(二)SwitchTransition篇

453 阅读5分钟

前面一篇文章介绍了React Transition Group中用于单个组件过渡管理的两个组件TransitionCSSTransition。而React Transition Group还有两个用于管理多个组件过渡管理的组件SwitchTransitionTransitionGroup。下面介绍两个组件的用法以及我对其源码的一些研究。

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接受单个TransitionCSSTransition组件作为子组件,并且有一个属性mode接受string类型值,用于指定切换效果。

props可选解释
mode可选有两个值可供选择,"out-in"(先展示离开过渡效果,再展示进入过渡效果)、 "in-out"(展示进入过渡效果,再展示离开过渡效果),默认值是"out-in"

SwitchTransition的子组件TransitionCSSTransition的属性、用法并没有改变,需要注意的有两点:

  1. TransitionCSSTransition在作为 SwitchTransition 子组件时,不需要定义属性 in。这是因为SwitchTransition会接管子组件的in属性。
  2. TransitionCSSTransition在作为 SwitchTransition 子组件时,需要定义属性key。这是因为SwitchTransition会通过key属性在渲染前后是否一致,来展示当前的过渡效果。

2. SwitchTransition源码解析

让我们结合源码跑一遍吧

SwitchTransition 中有三种状态(status): ENTERED, ENTERINGEXITING

首先,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方法中可以看到,当statusEXITING时,渲染的子组件由leaveRenders[mode](data)决定。记住,我们当前的mode为out-inchildren值发生了变更,而state.currrent值则是缓存的children值。

可以看到,leaveRenders将返回current,即缓存的子组件, 并且子组件的in属性值为false,而初始化时current的in属性值为true,因此这里会展示缓存子组件的离开过渡效果,并且current组件定义的onExited钩子,将会在过渡效果消失时,修改SwitchTransitionstatus值和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方法时,当statusENTERING时,渲染的子组件由enterRenders[mode](data)决定。

可以看到,enterRenders将返回children,即当前子组件,并且子组件的in属性值设为ture。将会展示进入过渡效果,同时也绑定了事件onEnteredstatus重新回到ENTERING。至此,我们完成一次切换效果。

Q&A

Q1:由于子组件的key值变更了,当前子组件会重新挂载,然而我们没定义appear属性和appear样式,难道看不到进入过渡效果了吗 ?

还记得SwitchTransition中的appeared吗? 在这里派上用场了,在SwitchTransition组件挂载之后this.appeared值为true,传递给子组件Transition时就会起到appear的属性。而且因为此时Transitionappear属性仍为false,会直接使用enter样式,无需定义appear样式。

Q2: 为什么在前面说SwitchTransition是管理多个组件过渡的组件?

这是因为React Transition Group在实现SwitchTransition进入/离开的切换效果时,其实同时用到了两个TransitionCSSTransition来实现这个效果。比如实现out-in效果时,会先让之前缓存的子组件展示离开效果,在过渡结束时,让当前的子组件展示进入效果。

Q3:SwitchTransition组件有哪些值得学习的地方?

有很多方面,就比如作者在整体设计思路上,巧妙地运用React.cloneElement来进行子组件缓存、更新,减少了用户接触学习的成本。