过去一年,React 给整个前端界带来了一种新的开发方式,咱们抛弃了无所不能的 DOM 操做。对于 React 实现动画这个命题,DOM 操做已是一条死路,而 CSS3 动画又只能实现一些最简单的功能。这时候 ReactCSSTransitionGroup Addon,无疑是一枚强心剂,可以帮助咱们以最低的成本实现例如节点初次渲染、节点被删除时添加动效的需求。本文将会深刻实现原理来玩转 ReactCSSTransitionGroup。html
在介绍 ReactCSSTransitionGroup 的用法前,先来实现一个常规 transition 动画,要实现的是删除某个节点的时候,让该节点的透明度不断的变大。前端
handleRemove(item) { const { items } = this.state; const len = items.length; this.setState({ items: items.reduce((result, entry) => { return entry.id === item.id ? [...result, { ...item, isRemoving: true }] : [...result, item]; }, []) }, () => { setTimeout(() => { this.setState({ items: items.reduce((result, entry) => { return entry.id === item.id ? result : [...result, item]; }, []) }); }, 500); }); }, render() { const items = this.state.items.map((item, i) => { return ( <div key={item.id} onClick={this.handleRemove.bind(this, item)} className={item.isRemoving ? 'removing-item' : ''}> {item.name} </div> ); }); return ( <div> <button onClick={this.handleAdd}>Add Item</button> <div> {items} </div> </div> ); }
同时咱们在 CSS 中须要提供以下的样式react
.removing-item { opacity: 0.01; transition: opacity .5s ease-in; }
相同的需求,使用 ReactCSSTransitionGroup 建立动画会是怎么的呢?git
handleRemove(i) { const { items } = this.state; const len = items.length; this.setState({ items: [...items.slice(0, i), ...item.slice(i + 1, len - 1)] }); }, render() { const items = this.state.items.map((item, i) => { return ( <div key={item} onClick={this.handleRemove.bind(this, i)}> {item} </div> ); }); return ( <div> <button onClick={this.handleAdd}>Add Item</button> <ReactCSSTransitionGroup transitionName="example"> {items} </ReactCSSTransitionGroup> </div> ); }
在这个例子中,当新的节点从 ReactCSSTransitionGroup 中删除时,这个节点会被加上 example-leave
的 class,在下一帧中这个节点还会被加上 example-leave-active
的 class,经过添加如下 CSS 代码,被删除的节点就会有动画的效果。github
.example-leave { opacity: 1; transition: opacity .5s ease-in; } .example-leave.example-leave-active { opacity: 0.01; }
从这个例子,咱们能够看到 ReactCSSTransition 能够把开发者从一大堆动画相关的 state 中解放出来,只须要关心数据的变化,以及 CSS 的 transition 动画逻辑。浏览器
后面将会仔细分析 ReactCSSTransitionGroup 的源码实现。在看代码以前,你们能够先看 官网的文档,对 ReactCSSTransitionGroup 的用法进一步了解。看完之中,能够想一想两个问题:app
appear 动画和 enter 动画有什么区别?ide
ReactCSSTransitionGroup 子元素的生命周期是怎样的?函数
ReactCSSTransitionGroup 的源码分为5个模块,咱们先看看这5个模块之间的关系:工具
咱们来整理一下这几个模块的分工与职责:
ReactTransitionEvents 提供了对各类前缀的 transitionend、animationend 事件的绑定和解绑工具
ReactTransitionChildMapping 提供了对 ReactTransitionGroup 这个 component 的 children 进行格式化的工具
ReactCSSTransitionGroup 会调用 ReactCSSTransitionGroupChild 对 children 中的每一个元素进行包装,而后将包装后的 children 做为 ReactTransitionGroup 的 children 。
从这个关系图里面能够看到,ReactTransitionGroup 和 ReactCSSTransitionGroupChild 才是实现动画的关键部分,所以,本文会从 ReactTransitionGroup 开始解读,而后从 ReactCSSTransitionGroupChild 中解读怎么实现具体的动画逻辑。
下面咱们按照 React 生命周期来解读 ReactTransitionGroup。
在初始化 state 的时候,将 this.props.children
转化为对象,其中对象的 key 就是 component key,这个 key 与 children 中的元素一一对应,而后将该对象设置为 this.state.children;
在初次 render 的时候,将 this.state.children
中每个普通的 child component 经过指定的 childFactory
包裹成一个新的 component,并渲染成指定类型的 component 的子元素。在下面的源码中也能够看到,咱们在建立过程当中给每一个 child 设置的 key 也会做为 ref,方便后续索引。
render: function() { var childrenToRender = []; for (var key in this.state.children) { var child = this.state.children[key]; if (child) { childrenToRender.push(React.cloneElement( this.props.childFactory(child), {ref: key, key: key} )); } } return React.createElement( this.props.component, this.props, childrenToRender ); }
初次 mount 后,遍历 this.state.children
中的每一个元素,依次执行 appear 动画的逻辑。
当接收到新的 props 后,先将 nextProps.children
和 this.props.children
合并,而后转化为对象,并更新到 this.state.children
。计算在 nextProps
中即将 leave 的 child,若是该元素当前没有正在运行的动画,将该元素的 key 保存在 keysToLeave。
对于 nextProps 中新的 child,若是该元素没有正在运行的动画的话(也许会疑惑,一个刚进入的元素怎么会有动画正在运行呢?下文将会解释),将该元素的 key 保存在 keysToEnter。从这里也能看出来,原本在 nextProps 中即将 leave 的 child 会被保留下来以达到动画效果,等动画效果结束后才会被 remove。
component 更新完成后,对 keysToEnter
中的每一个元素执行 enter 动画的逻辑,对 keysToLeave
中的每一个元素执行 leave 动画的逻辑。因为 enter 动画的逻辑和 appear 动画的逻辑几乎如出一辙,无非是变成执行 child 的componentWillEnter
和 componentDidEnter
方法。
leave 动画稍有不一样,看下面源码能够看到,在 leave 动画结束后,若是发现该元素从新 enter,这里会再次执行 enter 动画,不然的话经过更新 state
中的 children 来删除相应的节点。这里也能够回答,为何对刚 enter 的元素,也要判断该元素是否正在进行动画,由于若是该元素上一次 leave 的动画尚未结束,那么这个节点还一直保留在页面中运行动画。
另外,你们有没有注意到一个问题,若是 leave 动画的回调函数没有被调用,那么这个节点将永远不会被移除。
if (currentChildMapping && currentChildMapping.hasOwnProperty(key)) { // This entered again before it fully left. Add it again. this.performEnter(key); } else { this.setState(function(state) { var newChildren = assign({}, state.children); delete newChildren[key]; return {children: newChildren}; }); }
至此,咱们看到 ReactTransitionGroup 没有实现任何具体的动画逻辑。
搞清楚 ReactTransitionGroup 的原理之后,ReactCSSTransitionGroup 作的事情就很简单了。简单地说, ReactCSSTransitionGroup 调用了 ReactTransitionGroup ,提供了本身的 childFactory 方法,而这个 childFactory 则是调用了 ReactCSSTRansitionGroupChild 。
_wrapChild: function(child) { // We need to provide this childFactory so that // ReactCSSTransitionGroupChild can receive updates to name, enter, and // leave while it is leaving. return React.createElement( ReactCSSTransitionGroupChild, { name: this.props.transitionName, appear: this.props.transitionAppear, enter: this.props.transitionEnter, leave: this.props.transitionLeave, appearTimeout: this.props.transitionAppearTimeout, enterTimeout: this.props.transitionEnterTimeout, leaveTimeout: this.props.transitionLeaveTimeout, }, child ); }
下面来看 ReactCSSTransitionGroupChild 是怎么实现节点的动画的。以 appear 动画为例,在 child.componentWillAppear
被调用的时候,给该节点加上 xxx-appear 的 className ,而且在一帧(React 里是写死的17ms)后,给该节点加上 xxx-appear-active 的 className ,最后在动画结束后删除 xxx-appear 以及 xxx-appear-active 的 className。
enter、leave 动画的实现相似。到这里源码就解读完了,其中,还有一些细节要去注意的。
key
里的秘密在源码解读的过程当中,咱们发现 ReactTransitionGroup 会将 children 转化为对象,而后经过 for...in...
遍历。对于这一过程,会不会感到有所疑虑,ReactTransitionGroup 怎么保证子节点渲染的顺序。
对于这个问题,React 的处理过程能够简化为下面的代码,测试结果显示,当 key 为字符串类型时,for...in...
遍历的顺序和 children 的顺序可以保持一致;可是当 key 为数值类型时,for...in...
遍历的顺序和 children 的顺序就不必定可以保持一致,你们能够用下面这段简单的代码测试一下。
function test (o) { var result = {}; for (var i = 0, len = o.length; i < len; i++) { result[o[i].key] = o[i]; } for (var key in result) { if (result[key]) { console.log(key, result[key]); } } }
所以,咱们知道 ReactCSSTransitionGroup 全部子 component 的 key 千万不要设置成纯数字,必定要是字符串类型的。
在 React 0.14 版本中,React 已经表示将在将来的版本中废弃监听 transitionend、 animationend 事件,而是经过设置动画的 timeout 来达到结束动画的目的,有没有想过 React 为何要放弃原生事件,而改用 setTimeout
。
事实上,缘由很简单,transitontend 事件在某些状况是不会被触发。在 transitionend 的 MDN文档 中有这么几行文字:
In the case where a transition is removed before completion, such as if the transition-property is removed, then the event will not fire. The event will also not fire if the animated element becomes display: none before the transition fully completes.
当动画元素的 transition 属性在动画完成前被移除了,transitionend 事件不会被触发
当动画元素在动画完成前,display 样式被设置成 "none",这种状况 transitionend 事件不会被触发
当动画还没完成,当前浏览器标签页失焦很长的时间(大于动画时间),transitionend 事件不会被触发,直到该标签页从新聚焦后 transitionend 事件才会触发
正是因为 transitionend
不会触发,会致使隐形 bug,能够看其中一个 bug。
appear 动画是 ReactCSSTransitionGroup 组件初次 mount 后,才会被添加到 ReactCSSTransitionGroup 的全部子元素上。
enter 动画是 ReactCSSTransitionGroup 组件更新后,被添加到新增的子元素上。
ReactCSSTransitionGroup 提供建立 CSS 动画最简单的方法,对于更加个性化的动画,你们能够经过调用 ReactTransitionGroup 自定义动画。