react动画之react-transition-group原理剖析

1,560 阅读3分钟

第一个组件    Transition  详细文档

是一个很底层的组件,其他组件的实现都是基于它
用法:
'entering''entered''exiting''exited'
第一种:传一个回调函数
当in为true state 变为 entering, 500ms 后变为 entered
当in为false state 变为 exiting, 500ms 后变为 exited
同时可以监听每个阶段的回调函数
也可以启用或者禁用 enter={false} 入场动画
function App() {
  const [inProp, setInProp] = useState(false);
  return (
    <div>
      <Transition in={inProp} timeout={500}>
        {state => (
          // ...
        )}
      </Transition>
      <button onClick={() => setInProp(true)}>
        Click to Enter
      </button>
    </div>
  );
}
内部结构有点复杂,判断情况也很多,这里仅仅给出核心代码
核心源码:
performEnter(mounting) {  const { enter } = this.props  const appearing = this.context ? this.context.isMounting : mounting  const [maybeNode, maybeAppearing] = this.props.nodeRef    ? [appearing]    : [ReactDOM.findDOMNode(this), appearing]  const timeouts = this.getTimeouts()  const enterTimeout = appearing ? timeouts.appear : timeouts.enter  // no enter animation skip right to ENTERED  // if we are mounting and running this it means appear _must_ be set  if ((!mounting && !enter) || config.disabled) {    this.safeSetState({ status: ENTERED }, () => {      this.props.onEntered(maybeNode)    })    return  }  this.props.onEnter(maybeNode, maybeAppearing)//   先把state 设置为 entering, 然后 timeout(500ms)后添加 entered   this.safeSetState({ status: ENTERING }, () => {    this.props.onEntering(maybeNode, maybeAppearing)    this.onTransitionEnd(enterTimeout, () => {      this.safeSetState({ status: ENTERED }, () => {        this.props.onEntered(maybeNode, maybeAppearing)      })    })  })}
第二种作为底层组件,    

CSSTransition
[详细文档](http://reactcommunity.org/react-transition-group/css-transition)
用法
function App() {
  const [inProp, setInProp] = useState(false);
  return (
    <div>
      <CSSTransition in={inProp} timeout={200} classNames="my-node">
        <div>
          {"I'll receive my-node-* classes"}
        </div>
      </CSSTransition>
      <button type="button" onClick={() => setInProp(true)}>
        Click to Enter
      </button>
    </div>
  );
}
用法比 transition 更加舒服,更加像一个真正的,能用的组件
同样是通过 in 来控制入场和出场,增加了 classNames="my-node" 前缀
每个阶段会替子元素添加 这些css class
.my-node-enter {
  opacity: 0;
}
.my-node-enter-active {
  opacity: 1;
  transition: opacity 200ms;
}
.my-node-exit {
  opacity: 1;
}
.my-node-exit-active {
  opacity: 0;
  transition: opacity 200ms;
}
原理分析
CSSTransition.js
render() {  const { classNames: _, ...props } = this.props;  return (    <Transition      {...props}      onEnter={this.onEnter}// 重点      onEntered={this.onEntered}// 重点      onEntering={this.onEntering}// 重点      onExit={this.onExit}// 重点      onExiting={this.onExiting}// 重点      onExited={this.onExited}// 重点    />  );}

Transition.jsreturn (  // allows for nested Transitions  <TransitionGroupContext.Provider value={null}>    {typeof children === 'function'      ? children(status, childProps)      : React.cloneElement(React.Children.only(children), childProps)}// 重点  </TransitionGroupContext.Provider>)
通过监听底层组件 transition 的各个阶段回调函数,通过dom操作来添加class类

例如

onEnter = (maybeNode, maybeAppearing) => {  const [node, appearing] = this.resolveArguments(maybeNode, maybeAppearing)  this.removeClasses(node, 'exit');  this.addClass(node, appearing ? 'appear' : 'enter', 'base');  if (this.props.onEnter) {    this.props.onEnter(maybeNode, maybeAppearing)  }}
TransitionGroup

用法

<TransitionGroup className="todo-list">  {items.map(({ id, text }) => (    <CSSTransition      key={id}      timeout={500}      classNames="item"    >      <ListGroup.Item>        <Button          className="remove-btn"          variant="danger"          size="sm"          onClick={() =>            setItems(items =>              items.filter(item => item.id !== id)            )          }        >          &times;        </Button>        {text}      </ListGroup.Item>    </CSSTransition>  ))}</TransitionGroup>

.item-enter {.item-enter {opacity: 0;}.item-enter-active {opacity: 1;transition: opacity 500ms ease-in;}.item-exit {opacity: 1;}.item-exit-active {opacity: 0;transition: opacity 500ms ease-in;}
用法更加简单,只需要,给子节点包一个 CSSTransition 就可以
核心原理解析
比如删除一个子结点,会等出场动画结束后,才会删除这个元素
这里用到了 保存react结点的方法
唯一有点黑科技的地方是,比如删除一个子结点,会等出场动画结束后,才会删除这个元素
这是一个非常常见的需求,
TransitionGroup 的做法是
把 children 放在 state里面维护
上一次的 children 和当前children进行合并,找出当前删除的item项,用上一个补过来
pre          current
item1      item1
item2     item2
item3     XXXX
例如 item3已经删除了,那么就先用 上一次的 item3补过来,然后给 item3 设置 in={false}
然后 放一个 handleExited,监听动画结束后删除该 item3, 结束
static getDerivedStateFromProps(  nextProps,  { children: prevChildMapping, handleExited, firstRender }) {  return {    children: firstRender      ? getInitialChildMapping(nextProps, handleExited)      : getNextChildMapping(nextProps, prevChildMapping, handleExited),    firstRender: false,  }}

export function getNextChildMapping(nextProps, prevChildMapping, onExited) {  let nextChildMapping = getChildMapping(nextProps.children)  let children = mergeChildMappings(prevChildMapping, nextChildMapping)  Object.keys(children).forEach(key => {    let child = children[key]    if (!isValidElement(child)) return    const hasPrev = key in prevChildMapping    const hasNext = key in nextChildMapping    const prevChild = prevChildMapping[key]    const isLeaving = isValidElement(prevChild) && !prevChild.props.in    // item is new (entering)    if (hasNext && (!hasPrev || isLeaving)) {      // console.log('entering', key)// 新入场的元素      children[key] = cloneElement(child, {        onExited: onExited.bind(null, child),        in: true,        exit: getProp(child, 'exit', nextProps),        enter: getProp(child, 'enter', nextProps),      })    } else if (!hasNext && hasPrev && !isLeaving) {      // item is old (exiting)      // console.log('leaving', key)// 要删除的这个元素,   给它最后的挣扎,结束后,就会执行  onExited 函数      children[key] = cloneElement(child, { in: false })    } else if (hasNext && hasPrev && isValidElement(prevChild)) {      // item hasn't changed transition states      // copy over the last transition props;      // console.log('unchanged', key)      children[key] = cloneElement(child, {        onExited: onExited.bind(null, child),        in: prevChild.props.in,        exit: getProp(child, 'exit', nextProps),        enter: getProp(child, 'enter', nextProps),      })    }  })  return children}
删除元素后执行的方法,
// node is `undefined` when user provided `nodeRef` prophandleExited(child, node) {  let currentChildMapping = getChildMapping(this.props.children)  if (child.key in currentChildMapping) return  if (child.props.onExited) {    child.props.onExited(node)  }  if (this.mounted) {    this.setState(state => {      let children = { ...state.children }      delete children[child.key]      return { children }    })  }}

未完待续