Skip to content

react-animation-source-code-read

Posted on:November 19, 2018 at 02:21 PM

React 动画之社区实现

react 动画实现由TransitionCSSTransitionTransitionGroup组成,三个组件分别实现了动画状态管理,动画 css 管理,和列表动画元素状态管理。

Transition

transition组件是实现动画状态监管的核心组件,它本身并不实现任何动画逻辑,只是保存了当前组件的动画状态,和状态切换时的钩子函数调用。

export const UNMOUNTED = "unmounted"; // 未渲染
export const EXITED = "exited"; // 退场的
export const ENTERING = "entering"; // 进入中
export const ENTERED = "entered"; // 进入的
export const EXITING = "exiting"; // 退出的

以上是源码中定义的组件状态。

constuctor


  constructor(props, context) {
    super(props, context)

    let parentGroup = context.transitionGroup
    // In the context of a TransitionGroup all enters are really appears
    let appear =
      parentGroup && !parentGroup.isMounting ? props.enter : props.appear

    let initialStatus

    this.appearStatus = null

    if (props.in) {
      if (appear) {
        initialStatus = EXITED
        this.appearStatus = ENTERING
      } else {
        initialStatus = ENTERED
      }
    } else {
      if (props.unmountOnExit || props.mountOnEnter) {
        initialStatus = UNMOUNTED
      } else {
        initialStatus = EXITED
      }
    }

    this.state = { status: initialStatus }

    this.nextCallback = null
  }

构造器函数中,首先根据传入的 Props 来确认组件的初始状态。

getDerivedStateFromProps

 static getDerivedStateFromProps({ in: nextIn }, prevState) {
    if (nextIn && prevState.status === UNMOUNTED) {
      return { status: EXITED }
    }
    return null
  }

react 在最近的 16.4 中将该生命周期修改为static,并将函数的调用机制修改为贯穿创建和更新。具体可以查看官方提供的示意图。 react 生命周期 这段代码的的作用是如果下一次组件的状态是准备进入但是之前的状态是未渲染的,这直接将状态替换为退出的

首先确认下UNMOUTED的状态是如何产生的,只有在组件上添加了unmountOnExit或者mountOnEnter,且初始化状态infalse时,会在初始化阶段定义为该状态。

render

render() {
    const status = this.state.status
    if (status === UNMOUNTED) {
      return null
    }

    const { children, ...childProps } = this.props
    // filter props for Transtition
    delete childProps.in
    delete childProps.mountOnEnter
    delete childProps.unmountOnExit
    delete childProps.appear
    delete childProps.enter
    delete childProps.exit
    delete childProps.timeout
    delete childProps.addEndListener
    delete childProps.onEnter
    delete childProps.onEntering
    delete childProps.onEntered
    delete childProps.onExit
    delete childProps.onExiting
    delete childProps.onExited

    if (typeof children === 'function') {
      return children(status, childProps)
    }

    const child = React.Children.only(children)
    return React.cloneElement(child, childProps)
  }

接上面的,当状态为UNMOUNTED时,是不会渲染内容的,所以如果要进入是需要从UNMOUNTED => EXITED => ENTERED,当然,如果定义了unmountOnExit,当infalse的时候,组件的状态会退回为UNMOUNTED

render 函数支持两种渲染方式,函数和 react element,所以需要将内部使用的 prop 删除掉再传递给子组件。

didMount & didUpdate

组件在渲染完成之后,有了初始的状态,为了动画开始,需要对组件的状态进行修改。

updateStatus(mounting = false, nextStatus) {
    if (nextStatus !== null) {
      // nextStatus will always be ENTERING or EXITING.
      this.cancelNextCallback()
      const node = ReactDOM.findDOMNode(this)

      if (nextStatus === ENTERING) {
        this.performEnter(node, mounting)
      } else {
        this.performExit(node)
      }
    } else if (this.props.unmountOnExit && this.state.status === EXITED) {
      this.setState({ status: UNMOUNTED })
    }
  }

渲染完成和更新的钩子会根据状态分别执行performEnterperformExit

performEnter(node, mounting) {
    const { enter } = this.props
    const appearing = this.context.transitionGroup
      ? this.context.transitionGroup.isMounting
      : mounting

    const timeouts = this.getTimeouts()

    // no enter animation skip right to ENTERED
    // if we are mounting and running this it means appear _must_ be set
    if (!mounting && !enter) {
      this.safeSetState({ status: ENTERED }, () => {
        this.props.onEntered(node)
      })
      return
    }

    this.props.onEnter(node, appearing)

    this.safeSetState({ status: ENTERING }, () => {
      this.props.onEntering(node, appearing)

      // FIXME: appear timeout?
      this.onTransitionEnd(node, timeouts.enter, () => {
        this.safeSetState({ status: ENTERED }, () => {
          this.props.onEntered(node, appearing)
        })
      })
    })
  }

可以看到,通过初始状态,逐步调用onEnter,onEntering,onEntered并且更替状态 ENTERING => ENTERED。 而我们需要的做的就是根据 Transition 组件返回的状态来实现代码逻辑。

CSSTransition

CSSTransition 的实现就比较简单了,可以直接看 render 就可以了

render() {
    const props = { ...this.props };

    delete props.classNames;

    return (
      <Transition
        {...props}
        onEnter={this.onEnter}
        onEntered={this.onEntered}
        onEntering={this.onEntering}
        onExit={this.onExit}
        onExiting={this.onExiting}
        onExited={this.onExited}
      />
    );
  }

实际上就是对 Trsition 的一个扩展,作用就是在状态变更的时候加上相应的 class 名称,如果提前在环境中定义了相应的 css 定义,则直接可以实现效果,减少了代码。 用过 vue transition 组件的同学应该很熟悉。

TransitionGroup

TransitionGroup 相当于是在 CSSTransition 的基础上再进一步的封装。 为包裹的子组件提供相应的状态。

static getDerivedStateFromProps(
    nextProps,
    { children: prevChildMapping, handleExited, firstRender }
  ) {
    return {
      children: firstRender
        ? getInitialChildMapping(nextProps, handleExited)
        : getNextChildMapping(nextProps, prevChildMapping, handleExited),
      firstRender: false,
    }
  }

可以看到,组件并不是直接进行渲染 children,而是进行了一层封装。

export function getInitialChildMapping(props, onExited) {
  return getChildMapping(props.children, child => {
    return cloneElement(child, {
      onExited: onExited.bind(null, child),
      in: true,
      appear: getProp(child, "appear", props),
      enter: getProp(child, "enter", props),
      exit: getProp(child, "exit", props),
    });
  });
}

export function getChildMapping(children, mapFn) {
  let mapper = child => (mapFn && isValidElement(child) ? mapFn(child) : child);

  let result = Object.create(null);
  if (children)
    Children.map(children, c => c).forEach(child => {
      // run the map function here instead so that the key is the computed one
      result[child.key] = mapper(child);
    });
  return result;
}

可以看到,封装之后的子组件已经不是之前的了,而且经过克隆的组件会添加默认属性,而且如果之前定义过,则会覆盖。

 render() {
    const { component: Component, childFactory, ...props } = this.props
    const children = values(this.state.children).map(childFactory)

    delete props.appear
    delete props.enter
    delete props.exit

    if (Component === null) {
      return children
    }
    return <Component {...props}>{children}</Component>
  }

render 和之前的实现很相似。

以上是组件进入时候的初始化,但是当组件中子组件数量发生变化的时候又是如何处理的呢?

getNextChildMapping

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)
      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;
}

第一步,clone 新的 children, 第二步,合并之前的 children,这里的合并逻辑是将子组件已相对合理的顺序进行整合。然后根据状态的不同,分别进入和离开。 第三部,根据状态的不同,clone 新的子对象,并添加相应的属性,依旧会覆盖子组件的定义。 第四部,render

以上就是社区实现的动画组件,了解过其实现原理后,再写一些简单的过渡动画,那就再棒不过了。