我们从15年底开始使用 RN,版本号为0.16,到2月底为止,我们完成了三次版本的升级,最新版本为0.35。不得不说升级 RN 真是一件很费时费力的事情,期间版本变动较大。
我下面说一下我和我的团队在使用 RN 的一些问题,很可能我要说的这么问题,现在已经被解决啦。
基础知识
React 的生命周期
RN 中的每一个控件都是单独的,都有自己的生命周期,如图所示:
如图,可以把组件生命周期大致分为三个阶段:
- 第一阶段:是组件第一次绘制阶段,如图中的上面虚线框内,在这里完成了组件的加载和初始化;
- 第二阶段:是组件在运行和交互阶段,如图中左下角虚线框,这个阶段组件可以处理用户交互,或者接收事件更新界面;
- 第三阶段:是组件卸载消亡的阶段,如图中右下角的虚线框中,这里做一些组件的清理工作。
生命周期回调函数
下面来详细介绍生命周期中的各回调函数。
getDefaultProps
在组件创建之前,会先调用 getDefaultProps()
,这是全局调用一次,严格地来说,这不是组件的生命周期的一部分。在组件被创建并加载候,首先调用 getInitialState()
,来初始化组件的状态。
componentWillMount
然后,准备加载组件,会调用 componentWillMount()
,其原型如下:
|
|
这个函数调用时机是在组件创建,并初始化了状态之后,在第一次绘制 render()
之前。可以在这里做一些业务初始化操作,也可以设置组件状态。这个函数在整个生命周期中只被调用一次。
componentDidMount
在组件第一次绘制之后,会调用 componentDidMount()
,通知组件已经加载完成。函数原型如下:
|
|
这个函数调用的时候,其虚拟 DOM 已经构建完成,你可以在这个函数开始获取其中的元素或者子组件了。需要注意的是,RN 框架是先调用子组件的 componentDidMount()
,然后调用父组件的函数。从这个函数开始,就可以和 JS 其他框架交互了,例如设置计时 setTimeout
或者 setInterval
,或者发起网络请求。这个函数也是只被调用一次。这个函数之后,就进入了稳定运行状态,等待事件触发。
componentWillReceiveProps
如果组件收到新的属性(props),就会调用 componentWillReceiveProps()
,其原型如下:
|
|
输入参数 nextProps
是即将被设置的属性,旧的属性还是可以通过 this.props
来获取。在这个回调函数里面,你可以根据属性的变化,通过调用 this.setState()
来更新你的组件状态,这里调用更新状态是安全的,并不会触发额外的 render()
调用。如下:
|
|
shouldComponentUpdate
当组件接收到新的属性和状态改变的话,都会触发调用 shouldComponentUpdate(...)
,函数原型如下:
|
|
输入参数 nextProps
和上面的 componentWillReceiveProps
函数一样,nextState
表示组件即将更新的状态值。这个函数的返回值决定是否需要更新组件,如果 true
表示需要更新,继续走后面的更新流程。否者,则不更新,直接进入等待状态。
默认情况下,这个函数永远返回 true
用来保证数据变化的时候 UI 能够同步更新。在大型项目中,你可以自己重载这个函数,通过检查变化前后属性和状态,来决定 UI 是否需要更新,能有效提高应用性能。
componentWillUpdate
如果组件状态或者属性改变,并且上面的 shouldComponentUpdate(...)
返回为 true
,就会开始准更新组件,并调用 componentWillUpdate()
,其函数原型如下:
|
|
输入参数与 shouldComponentUpdate
一样,在这个回调中,可以做一些在更新界面之前要做的事情。需要特别注意的是,在这个函数里面,你就不能使用 this.setState
来修改状态。这个函数调用之后,就会把 nextProps
和 nextState
分别设置到 this.props
和 this.state
中。紧接着这个函数,就会调用 render()
来更新界面了。
componentDidUpdate
调用了 render()
更新完成界面之后,会调用 componentDidUpdate()
来得到通知,其函数原型如下:
|
|
因为到这里已经完成了属性和状态的更新了,此函数的输入参数变成了 prevProps
和 prevState
。
componentWillUnmount
当组件要被从界面上移除的时候,就会调用 componentWillUnmount()
,其函数原型如下:
|
|
在这个函数中,可以做一些组件相关的清理工作,例如取消计时器、网络请求等。
总结一下:
到这里,RN 的组件的完整的生命都介绍完了,在回头来看一下前面的图,就比较清晰了,把生命周期的回调函数总结成如下表格:
生命周期 | 调用次数 | 能否使用 setSate() |
---|---|---|
getDefaultProps | 1(全局调用一次) | 否 |
getInitialState | 1 | 否 |
componentWillMount | 1 | 是 |
render | >=1 | 否 |
componentDidMount | 1 | 是 |
componentWillReceiveProps | >=0 | 是 |
shouldComponentUpdate | >=0 | 否 |
componentWillUpdate | >=0 | 否 |
componentDidUpdate | >=0 | 否 |
componentWillUnmount | 1 | 否 |
父子控件
父子控件之间传值,如果是父控件给子空间传值,那么就利用子控件的props
值;如果子空间向父控件传值,我想到的是 事件分发,这可是一项很重要的技能,我们的不同控件传值主要用的就是它。
State/Props
每一个控件都有自己的 state 和 props 值,我们利用不同 state 值去实现页面的变化,利用 props 值去执行特定的操作。
ReactNative 的优缺点
优点
- 跨平台特性,一套代码可以实现两个平台的效果
- 原生的用户体验,这是它可以击败 H5 的地方。
- 开发静态页面效率高
- 网络请求封装的很好,fetch 、promise 使用起来很方便
缺点
- 不稳定,还处于完善的阶段。
- 调试Debug时,提示错误比较晦涩难懂。
下面讲讲我在利用 RN 开发应用的一下体会,
ListView 的多状态显示
现在可能已经不是问题了,但当时对我来说确实遇到困难了,这是我请示大神给予我的方案:将不同状态的元素放在不同的数组里,针对不同的数组去选择不同的 item 去渲染,这样就实现了不同的状态显示不同的item 的目的。
生命周期和数组的处理
我在开发统计分享界面的时候,需要加载三个数组,每一个数组的条目都很多,大约30条左右吧,最初做这个界面的时候,就直接 Var 三个变量,这样测试在测试的时候发现,不停的进进出出这个页面,应用程序会卡顿;最后我们分析可能是数组的加载导致的。我们的方式是:(1)将数组放在组件的内部去加载,同时在组件被卸载之后呢,将数组晴空;(2) 将绘图的时机进行调整,当有状态值发生改变时才会去重画图片。
自定义 SliderBar
刚开始使用RN 时,并没有 sliderBar 这个控件,但实际项目中还需要有这么一个控件,只能自定义啦。我们利用 PanResponse 的特性,获取手指点击区域的 XY value 来改变 View 的 width 属性实现动态的延展和缩放。
数据不一致的队列处理
我们的app 在使用中需要于硬件设备进行交互,有时候我发给设备的值,设备并没有执行,这就出现了页面显示数据和设备数据不一致的情况。我们想到的办法是:利用队列。将要传输的数据放到队列中,逐次向下传输,同时设备会把它现在的值进行上报,我们会对队列的最后一个数据和设备的数据进行比较,如果不一致,再重新发一次命令;如果一致,将队列晴空。在多说一句,队列需要自己用 js 去实现。
Map 处理不同设备数据
在开发过程中,某一个功能需要针对不同的设备设置不同的值,这个时候就需要 Map 的属性,还好 ES6 为我们提供了 Map,不需要我们自己去实现。
水波纹动画
我们原本想用前后两张图片朝着同一个方法移动的方式去实现,但是实现之后发现问题较多,最后我们利用 ART 控件去画了正弦曲线,效果很好,详细的原理单独找一篇去写。
RN 在两个平台的差异性
flex 属性实现
ios 针对 View 必须加上 Width 属性才能实现,Android 不需要。
Text 背景色
如果不设置背景色,Android默认是透明色,ios 默认是白色,ios 需要添加 backgroundColor 属性。
ListView 控件
ios需添加removeClippedSubviews={false}属性
BlE
我确实弄了一年多的低功耗蓝牙开发,但也仅仅限于应用层开发,没有深入到底层,我就说一下我在应用层开发遇到的问题。
- 关闭手机蓝牙,开启时需要执行一次搜索功能。主要针对三星设备
- 自动连接功能的实现,再执行连接之前先执行一次搜索的操作,如果搜索到设备就发起连接,否则不发起连接。
- 在连接的过程中,如果发生 133 错误,这个问题主要是手机本身蓝牙出现了问题。针对这个问题,我们做了多次连接的处理,当出现在这个状态码时,执行3次连接,如果3次连接还是没有成功,那么关闭 Gatt 端。
- 进行DFU 升级时,如果发生失败,需要调用 refreshGatt(…) 方法来清空 Gatt 的缓存,否则升级失败的可能性很大
- 搜索的时候,我们可以将公司的 UUID 放在一个 Bundle 里做一个集合,但是这种方式在 三星 S6 上出现了无法搜索到设备情况,所以暂时停滞了,但确实很简便。
- 针对哪些在设备连接成功之后需要发送的命令,我们可以用一个单独的线程去执行命令的发送。这个线程的生命周期和应用一致。
总结
最后写一下,自己这一年半的感悟吧,我的头的一句话还是挺触发我的,如下:
你完成的只是半成品
选择一个点深挖进去
逻辑的提升才是最重要的