介绍
React Native是Facebook在React.js Conf2015大会上推出的一个用于开发Android和iOS App的一个框架,主要编程语言是JavaScript,UI使用JSX(一种类似于XML的UI描述语言
核心理念:
即拥有Native的用户体验,又保留React的开发效率
RN和web中的DOM树:
- web通过DOM管理Element,而DOM的效率是很低的,为了提高DOM的效率,React.js提供了Virtual DOM,这项技术的工作是完全放在内存中完成的,而且是增量修改DOM树,所以效率非常高
- React.js主要用于web开发,在运行时使用React.js将虚拟DOM装换成了实际DOM
- React Native主要用于开发Android和iOS App,就是在运行时将虚拟DOM映射成Android和iOS的本地控件
优势:
1.跨平台(iOS 和android)
2.热更新
开始RN
常见的RN技能树如下,既然是开山篇,我会将一些基础的地方进行讲解,如果有机会的话,在对如下做补充
React:
JSX
React 使用 JSX 来替代常规的 JavaScript。JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。是对React.createElement()
快捷使用方式,
1.JSX 执行更快,因为它在编译为 JavaScript 代码后进行了优化。
2.它是类型安全的,在编译过程中就能发现错误。
3.使用 JSX 编写模板更加简单快速
const b = (
<View
foo='hello'
bar={baz}>
<Text>42</Text>
</View>
)
var b = React.createElement(
View,
{
foo: 'hello',
bar: baz },
React.createElement(
Text,
null,
'42'
)
);
Components
Components 是组成各种UI的基础,RN的UI部分就是以特定的顺序去渲染不同的components,components可以互相包裹形成一个树状结构,树的根节点被成为 根components,其他被称为子components。
创建方式
ES6
以前只能通过React.createClass
来创建components,ES6对类和继承有语法级别的支持,用ES6创建组件的方式更加优雅
class Greeting extends React.Component {
constructor(props) {
super(props);
this.state = {count: props.initialCount};
}
render() {
return (
<View>
<Text>Hello, {this.state.count}</Text>;
</View>
)
}
}
component类型
React中有两种组件:函数组件Functional Components
和类组件Class Components
,上图的是声明一个Class Components
的方式,下面声明了一个函数式component。
function Welcome = (props) => {
const sayHi = () => {
alert(`Hi ${props.name}`);
}
return (
<div>
<h1>Hello, {props.name}</h1>
<button onClick ={sayHi}>Say Hi</button>
</div>
)
}
性能。函数组件中,你无法使用State,也无法使用组件的生命周期方法,这就决定了函数组件都是展示性组件(Presentational Components),接收Props,渲染DOM,而不关注其他逻辑。
目前React还是会把函数组件在内部转换成类组件,所以使用函数组件和使用类组件在性能上并无大的差异。但是,React官方已承诺,未来将会优化函数组件的性能,因为函数组件不需要考虑组件状态和组件生命周期方法中的各种比较校验。
flexbox:
弹性盒模型:
旨在通过弹性的方式来对齐和分布容器中内容的空间,使其能适应不同屏幕,为盒装模型提供最大的灵活性
主要思想:
让容器有能力让其子项目能够改变其宽度、高度(甚至是顺序),以最佳方式填充可用空间;
数据流:
在任何应用中,数据必不可少,我们需要直接改变页面上的一块区域来使得视图刷新,或者间接改变其他地方的数据。React的数据是自顶向下单向流动的,即从父组件到子组件中,组件的数据存储在props和state中。
props:
是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新的props来重新渲染子组件,否则子组件的props以及展现形式不会改变。
state:
和props不同的地方在于,state是完全私有的,完全由该组件的内部状态控制。组件内部只能通过this.setState()
来修改state,React会调用组件的生命周期方法并且重新调用render,重新对组件进行渲染。
总结:
1.state是组件自己管理数据,控制自己的状态,可变;
2.props是外部传入的数据参数,不可变;
没有state的叫做无状态组件,有state的叫做有状态组件;
3.多用props,少用state。只有props的组件叫无状态组件,函数组件也是属于无状态组件,比较易于维护。
生命周期:
一个组件的生命周期可以分成3个阶段
实例化:
constructor(props):
components初始化,这里会收到父类传来的props,可以在这里对state
进行初始化
getDefaultProps:
对组件props
进行默认值设置
getInitialState:
对组件state
进行默认值设置
componentWillMount:
组件将要被加载到视图之前调用
render:
发生第一次render,这个方法返回null,表示不渲染
componentDidMount
组件加载成功并被成功渲染出来之后,所要执行的后续操作(网络,订阅消息,定时器),一般都会在这个函数中进行
存在化阶段:
componentWillReceiveProps(nextProps):
当父component重新传入props,可以在这里调用setState
方法
shouldComponentUpdate(nextProps, nextState):
根据nextProps和nextState,决定是否需要重新渲染,默认实现返回true。
componentWillUpdate(nextProps, nextState):
在上面方法调用前调用,这个方法内不要调用setState:
,这里一次渲染处理已经开始了
render
渲染
componentDidUpdate(prevProps, prevState)
渲染完毕
销毁阶段:
componentWillUnmount
用于释放一些资源,比如清理定时器,停止监听消息等
diff算法:
虚拟DOM
基于React进行开发时所有的DOM构造都是通过虚拟DOM进行,每当数据变化时,React都会重新构建整个DOM树,然后React将当前整个DOM树和上一次的DOM树进行对比,得到DOM结构的区别,然后仅仅将需要变化的部分进行实际的浏览器DOM更新。
而且React能够批处理虚拟DOM的刷新,在一个事件循环(Event Loop)内的两次数据变化会被合并。
尽管每一次都需要构造完整的虚拟DOM树,但是因为虚拟DOM是内存数据,性能是极高的,而对实际DOM进行操作的仅仅是Diff部分,因而能达到提高性能的目的。
总结:
所有的 DOM 变动,都先在虚拟 DOM 上发生,然后再将实际发生变动的部分,反映在真实 DOM上
算法简析
查找任意两棵树之间最少修改数的时间复杂度是O(n^3)。但
React使用一种简单但强大的启发式方式来优化到了接近O(n)
节点比较
逐层进行节点比较
虚拟DOM
React只会按层比较两棵树。这样就显著的降低了问题的复杂度,Web应用通常只会在子节点之间横向移动。
exp: 如果两棵dom树如下所示
A节点被整个移动到D节点下,直观的考虑DOM Diff操作应该是
A.parent.remove(A);
D.append(A);
但因为React只会简单的考虑同层节点的位置变换,对于不同层的节点,知识简单的创建和删除。当根节点发现子节点A不见了,就会直接销毁A;而当D发现自己多了一个子节点A,则会创建一个新的A作为子节点。
A.destroy();
A = new A();
A.append(new B());
A.append(new C());
D.append(A);
在实现自己的组件时,保持稳定的DOM结构会有助于性能的提升。例如,我们有时可以通过CSS隐藏或显示某些节点,而不是真的移除或添加DOM节点。
但是很遗憾,rn没有提供最直接的办法,rn官方的意思是不需要提供隐藏的方法,因为可以根据状态来决定是否渲染组件。
列表节点的比较:
列表节点的操作通常包括添加、删除和排序。例如下图,我们需要往B和C直接插入节点F,而在React中,我们只会告诉React新的界面应该是A-B-F-C-D-E,由Diff算法完成更新界面.
这时如果每个节点都没有唯一的标识,React无法识别每一个节点,那么更新过程会很低效
React会逐个对节点进行更新,转换到目标节点。而最后插入新的节点E,涉及到的DOM操作非常多。diff总共就是移动、删除、增加三个操作,而如果给每个节点唯一的标识(key),那么React优先采用移动的方式,能够找到正确的位置去插入新的节点,如下图所示:
渲染子树:
当调用setState时,组件会重建子节点的虚拟DOM。如果在根元素上调用setState,整个React应用都会被重新渲染。所有的组件,包括没有发生改变的组件都会调用自己的render方法。这样效率低的可怕。
在编写React代码时,我们每次不需要在发生改变后就直接调用根元素的setState
,只需要接受到改变事件的节点或者它的一些上层节点调用该方法,很少需要上溯到顶部。
选择使用PureComponent
或者重写shouldComponentUpdate
选择性的渲染子树
PureComponent
当组件的props
或者state
发生变化的时候:React会对组件当前的Props和State分别与nextProps和nextState进行比较,当发现变化时,就会对当前组件以及子组件进行重新渲染,否则就不渲染。有时候为了避免组件进行不必要的重新渲染,我们通过定义shouldComponentUpdate
来优化性能。为了避免样板代码于是React就提供了PureComponent
来自动帮我们做这件事。
只是对props和state进行浅比较(shadow comparison),当props或者state本身是嵌套对象或数组等时,浅比较并不能得到预期的结果,这会导致实际的props和state发生了变化,但组件却没有更新的问题。最简单避免上述情况的方式,就是避免使用可变对象作为props和state,取而代之的是每次返回一个全新的对象。
也可以考虑用Immutable.js
来创建不可变对象,通过它来简化对象比较,提高性能
:JS和Native
RN需要一个JS的运行环境, 在IOS上直接使用内置的javascriptcore, 在Android 则使用webkit.org官方开源的jsc.so。
RN 会把应用的JS代码(包括依赖的framework)编译成一个js文件 , RN的整体框架目标就是为了解释运行这个js 脚本文件,如果是js 扩展的API, 则直接通过bridge调用native方法; 如果是UI界面, 则映射到virtual DOM这个虚拟的JS数据结构中,通过bridge 传递到native , 然后根据数据属性设置各个对应的真实native的View。
bridge是一种JS 和 原生代码通信的机制, 用bridge函数传入对方module 和 method即可得到异步回调的结果。
RN内部(主要有二个线程,UI main thread, JS thread。 UI thread创建一个APP的事件循环后,就挂在looper等待事件 , 事件驱动各自的对象执行命令。 JS thread 运行的脚本相当于底层数据采集器,业务逻辑主要运行在该线程。也是发生 API 调用,以及处理触摸事件等操作的线程 。js线程不断上传数据,转化成UI 事件, 更新数据到原生支持的视图是批量进行的,并且在事件循环每进行一次的时候被发送到原生端。通过bridge转发到UI thread, 从而改变真实的View。 所以只要任何的UI thread或者js thread发生了卡顿处理的时间过长就会造成界面的卡顿。
内存优化:
内联引用
:require 代替 import可以延迟模块或文件的加载,直到实际需要该文件。(和对图片的使用方法一样。)
拆包
:RAM 格式的 bundle 则对此进行了优化,即启动时只加载 50MB 中实际需要的部分,之后再逐渐按需加载更多的包。
这里不详细叙述了,感兴趣的同学可以看官方文档的performance
混编
RN和Native通过bridge通讯
混编这块比较简单,没什么好讲的我以iOS为例子,具体可以看[demo地址] 里面配上了一些讲解 demo
热更新
Code-Push
1.申请登录code-push帐号
2.在code-push上注册android和iOS应用
3.在项目里配置
staging
和production
的key4.项目更新依赖
5.push 打包
在真正的项目中,我们一般会分为开发版(Test),灰度版(Staging)和发布版(Production),在Test中我一般是用来跟踪code-push的执行,在Staging中其实是和Production是同样的代码,但是当要热修复线上版本时,先会发布热更新到Staging版,在Staging测过后再通过promoting推到Production中去。
code-push常用命令:
发布更新
打包更新一体化
打包当前版本到staging
code-push release-react RNApp ios
code-push release-react RNApp ios --t 1.0.0 --dev false --d Staging --des "加入美团" --m true
1.
--t
对应ios的版本
2.
--dev
是否启用开发模式,default:false
3.
-d
发布更新环境 default:staging
4.
-des
描述
5.
-m
强制更新
打包更新分离
打包
react-native bundle -—platform ios -—entry-file index.js -—bundle-output ./bundles/main.jsbundle -—assets-dest ./bundles -—dev false
更新
code-push release RNApp ./bundles/main.bundle 1.0.0 --deploymentName Production --description "1.支持文章缓存。" --mandatory true
查看所注册的app
code-push app ls
查看对应app的详情
code-push deployment ls RNApp -k
添加环境
code-push deployment add RNApp Test
查看具体环境的部署信息
code-push deployment h RNApp Staging
常用场景命令
使用patch打补丁,修改元数据属性
使用场景:
例如当你已经发布了一个更新,但是到有些情况下,比如--des需要修改,--targetBinaryVersion写错了,这个时候就可以code-push patch RNApp Production --label v4 --targetBinaryVersion 8.6.1
使用promote将Staging推到Production
当你在指定的部署环境下测试更新时,例如Staging,测试通过后,想把这个更新发布到正式生产环境Production中,则可以使用code-push promote RNApp Staging Production
,这时可以修改一些元数据,例如--description、--targetBinaryVersion、--rollout等。
使用rollback回滚
使用场景:当你发布的更新测试没通过时,可以回滚到之前的某个版本。code-push rollback RNApp Production
,当执行这个命令时它会在RNApp上的Production部署上再次发布一个release,这个release的代码和元属性与Production上倒数第二个版本一致。也可以通过可选参数--targetRelease来指定rollback到的版本,例如code-push rollback RNApp Production --targetRelase v2
,则会新建一个release,这个release的代码和元属性与v2相同
使用debug查看是否使用了热更新版本
使用场景:当你想知道code-push的状态时,比如正在检查是否有更新包,正在下载,正在安装,当前加载的
bundle路径等,对于iOS可以使用code-push debug ios
较难理解的发布参数
Mandatory
代表是否强制性更新,这个属性只是简单的传递给客户端,具体要对这个属性如何处理是由客户端的codePush.sync
的mandatory
参数决定的
-mandatory |
updateDialog |
描述 |
---|---|---|
true |
true |
更新提示框只有一个按钮【确认更新】用户没法拒绝安装这个更新 |
false |
true |
更新提示框会弹出两个按钮,一个是【确认更新】,一个是【取消更新】 |
true |
false |
静默更新 |
false |
false |
静默更新 |
Disabled
默认是为false,顾名思义,这个参数的意思就是这个更新包是否让用户使用,如果为true,则不会让用户下载这个更新包
场景:当你想发布一个更新,但是却不想让这个更新立马生效,比如想对外公布一些信息后才让这个更新生效,这时候就可以使用code-push promote RNApp Staging Production --disabled false
来发布更新到正式环境,在对外公布信息后,使用code-push patch RNApp Production --disabled true
来让用户可以使用这个更新。
Rollout
场景:用来做灰度更新。用来指定可以接收到这个更新的用户的百分比,取值范围为0-100,不指定时默认为100。如果你希望部分用户体验这个新的更新,然后在观察它的崩溃率和反馈后,在将这个更新发布给所有用户时,这个属性就非常有用。当部署中的最后一个更新包的rollout值小于100有三点要注意
1.不能发布新的更新包,除非最后一个更新包的rollout值被patch为100。
2.当rollback时,rollout值会被置空(为100)。
3.当promote去其他部署时,rollout会被置空(为100),可以重新指定--rollout。
理解安装指标
active
成功安装并运行当前release的用户的数量,这个数字会根据用户成功installed这个release或者离开这个release(installed了别的更新包,或者卸载了App),总之有它就知道当前release的活跃用户量
Total
成功installed这个release的用户的数量,这个数量只会增不会减。
Pending
当前这个release被下载的数量,但是还没有被installed,因此这一个数值会在release被下载时增长,在installed时降低
Rollbacks 这个数字代表在客户端自动回滚的数量,理想状态下,它应该为0,如果你发布了一个更新包,在installing中发生crash,code-push将会把它回滚到之前的一个更新包中。
总结:
RN在几种跨平台的技术中,生态系统最丰富,第三方库很齐全,遇到问题大多数都是前人踩过的坑,可以解决。可以完全替代webView来和原生模块进行混编。支持热更新。
缺点:内存会变大问题,结合好的编程习惯用高性能的组件,和内联,拆包等优化技术目前使用来看其实不会涨的太大。而且可以加快页面出现的速度。
不支持多包同时进行加载,必须打成一个bundle包。业界有解决方案,但是基于之前的RN版本,不知现在可不可以用,而且涉及改源码,升级难以维护。