Animated动画库的使用
使用可看这篇文章Animated动画库的使用
Animated实现原理:
在上面的文章中,我们已经基本掌握了 RN Animated 的各种常用 API,接下来我们来了解一下这些 API 是如何设计出来的。
以下内容参考自 Animated 原作者的分享视频
首先,从 React 的生命周期来编程的话,一个动画大概是这样子写:
getInitialState() {
return {left: 0};
}
render(){
return (
<div style={{left: this.state.left}}>
<Child />
</div>
);
}
onChange(value) {
this.setState({left: value});
}
只需要通过 requestAnimationFrame 调用 onChange,输入对应的 value,动画就简单粗暴地跑起来了。 然而事实总是没那么简单,问题在哪?
我们看到,上述动画基本是以毫秒级的频率在调用 setState,而 React 的每次 setState 都会重新调用 render 方法,并切遍历子元素进行渲染,即使有 Dom Diff 也可能扛不住这么大的计算量和 UI 渲染。
那么该如何优化呢?
- 关键词:
- ShouldComponentUpdate
- (静态容器)
- Element Caching(元素缓存)
- Raw DOM Mutation(原生 DOM 操作)
- ↑↑↓↓←→←→BA(秘籍)
ShouldComponentUpdate
学过 React 的都知道,ShouldComponentUpdate 是性能优化利器,只需要在子组件的 shouldComponentUpdate 返回 false,分分钟渲染性能爆表。
然而并非所有的子元素都是一成不变的,粗暴地返回 false 的话子元素就变成一滩死水了。而且组件间应该是独立的,子组件很可能是其他人写的,父元素不能依赖于子元素的实现。
(静态容器)
这时候可以考虑封装一个容器,管理 ShouldCompontUpdate,如图示:
小明和老王再也不用关心父元素的动画实现啦。
一个简单的<StaticContainer> 实现如下:
class StaticContainer extends React.Component {
render(){
return this.props.children;
}
shouldComponentUpdate(nextProps){
return nextProps.shouldUpdate; // 父元素控制是否更新
}
}
// 父元素嵌入StaticContainer
render() {
return (
<div style={{left: this.state.left}}>
<StaticContainer
shouldUpdate={!this.state.isAnimating}>
<ExpensiveChild />
</StaticContainer>
</div>
);
}
Element Caching 缓存元素
还有另一种思路优化子元素的渲染,那就是缓存子元素的渲染结果到局地变量。
render(){
this._child = this._child || <ExpensiveChild />;
return (
<div style={{left:this.state.left}}>
{this._child}
</div>
);
}
缓存之后,每次 setState 时,React 通过 DOM Diff 就不再渲染子元素了。
上面的方法都有弊端,就是条件竞争。当动画在进行的时候,子元素恰好获得了新的 state,而这时候动画无视了这个更新,最后就会导致状态不一致,或者动画结束的时候子元素发生了闪动,这些都是影响用户操作的问题。
Raw DOM Mutation 原生 DOM 操作
刚刚都是在 React 的生命周期里实现动画,事实上,我们只想要变更这个元素的 left 值,并不希望各种重新渲染、DOM DIFF 等等发生。
“React,我知道自己要干啥,你一边凉快去“
如果我们跳出这个生命周期,直接找到元素进行变更,是不是更简单呢?
简单易懂,性能彪悍,有木有?!
然而弊端也很明显,比如这个组件 unmount 之后,动画就报错了。
Uncaught Exception: Cannot call ‘style’ of null
而且这种方法照样避不开条件竞争—— 动画值改变的时候,有可能发生 setState 之后,left 又回到初始值之类的情况。
再者,我们使用 React,就是因为不想去关心 dom 元素的操作,而是交给 React 管理,直接使用 Dom 操作显然违背了初衷。
↑↑↓↓←→←→BA(秘籍)
唠叨了这么多,这也不行,那也不行,什么才是真理?
我们既想要原生 DOM 操作的高性能,又想要 React 完善的生命周期管理,如何把两者优势结合到一起呢?答案就是 Data Binding(数据绑定)
render(){
return(
<Animated.div style={{left: this.state.left}}>
<ExpensiveChild />
</Animated.div>
);
}
getInitialState(){
return {left: new Animated.Value(0)}; // 实现了数据绑定的类
}
onUpdate(value){
this.state.left.setValue(value); // 不是setState
}
首先,需要实现一个具有数据绑定功能的类 Animated.Value,提供 setValueonChange 等接口。 其次,由于原生的组件并不能识别 Value,需要将动画元素用 Animated 包裹起来,在内部处理数据变更与 DOM 操作。
一个简单的动画组件实现如下:
Animated.div = class extends React.Component{
componentWillUnmount() {
nextProps.style.left.removeAllListeners();
},
// componentWillMount需要完成与componentWillReceiveProps同样的操作,此处略
componentWillReceiveProps(nextProps) {
nextProps.style.left.removeAllListeners();
nextProps.style.left.onChange(value => {
React.findDOMNode(this).style.left = value + 'px';
});
// 将动画值解析为普通数值传给原生div
this._props = React.addons.update(
nextProps,
{style:{left:{$set: nextProps.style.left.getValue()}}}
);
},
render() {
return <div ...{this._props} />;
}
}
代码很简短,做的事情有:
- 遍历传入的 props,查找是否有 Animated.Value 的实例,并绑定相应的 DOM 操作。
- 每次 props 变更或者组件 unmount 的时候,停止监听数据绑定事件,避免了条件竞争和内存泄露问题。
- 将初始传入的 Animated.Value 值逐个转化为普通数值,再交给原生的 React 组件进行渲染。
综上,通过封装一个 Animated 的元素,内部通过数据绑定和 DOM 操作变更元素,结合 React 的生命周期完善内存管理,解决条件竞争问题,对外表现则与原生组件相同,实现了高效流畅的动画效果。
读到这里,应该知道为什么 Image Text 等做动画一定要使用 Animated 加持过的元素了吧?