浅谈react native中Animated实现原理

1,842 阅读4分钟

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 渲染。

image.png

那么该如何优化呢?

  • 关键词:
    • ShouldComponentUpdate
    • (静态容器)
    • Element Caching(元素缓存)
    • Raw DOM Mutation(原生 DOM 操作)
    • ↑↑↓↓←→←→BA(秘籍)

ShouldComponentUpdate

学过 React 的都知道,ShouldComponentUpdate 是性能优化利器,只需要在子组件的 shouldComponentUpdate 返回 false,分分钟渲染性能爆表。

image.png

然而并非所有的子元素都是一成不变的,粗暴地返回 false 的话子元素就变成一滩死水了。而且组件间应该是独立的,子组件很可能是其他人写的,父元素不能依赖于子元素的实现。

(静态容器)

这时候可以考虑封装一个容器,管理 ShouldCompontUpdate,如图示:

image.png

小明和老王再也不用关心父元素的动画实现啦。

一个简单的<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,我知道自己要干啥,你一边凉快去“

如果我们跳出这个生命周期,直接找到元素进行变更,是不是更简单呢?

image.png

简单易懂,性能彪悍,有木有?!

然而弊端也很明显,比如这个组件 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} />;
    }
}

代码很简短,做的事情有:

  1. 遍历传入的 props,查找是否有 Animated.Value 的实例,并绑定相应的 DOM 操作。
  2. 每次 props 变更或者组件 unmount 的时候,停止监听数据绑定事件,避免了条件竞争和内存泄露问题。
  3. 将初始传入的 Animated.Value 值逐个转化为普通数值,再交给原生的 React 组件进行渲染。

综上,通过封装一个 Animated 的元素,内部通过数据绑定和 DOM 操作变更元素,结合 React 的生命周期完善内存管理,解决条件竞争问题,对外表现则与原生组件相同,实现了高效流畅的动画效果。

读到这里,应该知道为什么 Image Text 等做动画一定要使用 Animated 加持过的元素了吧?