特点
跨平台:React Native 使用了 Virtual DOM(虚拟 DOM),只需编写一套代码,便可以将代码打包成不同平台的 App,极大提高了开发效率,并且相对全部原生开发的应用来说,维护成本也相对更低。
上手快:相比于原生开发,JavaScript 学习成本低、语法灵活。允许让 Web 开发者更多地基于现有经验开发 App。React Native 只需使用 JavaScript 就能编写移动原生应用,它和 React 的设计理念是一样的,因此可以毫不夸张地说:你如果会写 React,就会写 React Native!
原生体验:由于 React Native 提供的组件是对原生 API 的暴露,虽然我们使用的是 JavaScript 语言编写的代码,但是实际上是调用了原生的 API 和原生的 UI 组件。因此,体验和性能足以媲美原生应用。
热更新:React Native 开发的应用支持热更新,因为 React Native 的产物是 bundle 文件,其实本质上就是 JS 代码,在 App 启动的时候就会去服务器上获取 bundle 文件,我们只需要更新 bundle 文件,从而使得 App 不需要重新前往商店下载包体就可以进行版本更新,开发者可以在用户无感知的情况下进行功能迭代或者 bug 修复。但是值得注意的是,AppStore 禁止热更新的功能中有调用私有 API、篡改原生代码和改变 App 的行为
核心原理
JavaScriptCore
JavaScriptCore 是 JavaScript 引擎,通常会被叫做虚拟机,专门设计来解释和执行 JavaScript 代码。在 React Native 里面,JavaScriptCore 负责 bundle 产出的 JS 代码的解析和执行。
JS Engine
React Native 需要一个 JS 的运行环境,因为 React Native 会把应用的 JS 代码编译成一个 JS 文件(x x.bundle),React Native 框架的目标就是解释运行这个 JS 脚本文件,如果是 Native 拓展的 API,则直接通过 bridge 调用 Native 方法
Bridge
在 React Native 中,原生端和 JavaScript 交互是通过 Bridge 进行的,Bridge 的作用就是给 React Native 内嵌的 JS Engine 提供原生接口的扩展供 JS 调用。理论上,任何原生代码能实现的效果都可以通过 Bridge 封装成 JS 可以调用的组件和方法, 以 JS 模块的形式提供给 RN 使用。
绿色的是我们应用开发的部分,我们写的代码基本上都是在这一层。
蓝色代表公用的跨平台的代码和工具引擎,一般我们不会动蓝色部分的代码。
黄色代表平台相关的 bridge 代码,做定制化的时候会添加修改代码。
红色代表系统平台的功能,另外红色上面有一个虚线,表示所有平台相关的东西都通过 bridge 隔离开来了,红色部分是独立于 React Native 的。
性能优化
渲染
减少 re-render
React.memo
React.memo 是 React v16.6 中引入的新功能,是一个专门针对 React 函数组件的高阶组件。当想要避免子组件不必要的重新渲染(即便父组件发生了更改),你可以使用 React.memo 打包子组件 – 只要 props 不发生改变,就不会重复渲染。
默认情况下,它和 PureComponent 一样,都是进行浅比较,因为就是个高阶组件,在原有的组件上套一层就可以了1
2
3const MemoButton = React.memo(function Button(props) {
return <button color={this.props.color} />;
});如果想和 shouldComponentUpdate 一样,自定义比较过程,React.memo 还支持传入自定义比较函数:
1
2
3
4
5
6
7
8
9
10function Button(props) {
return <button color={this.props.color} />;
}
function areEqual(prevProps, nextProps) {
if (prevProps.color !== nextProps.color) {
return false;
}
return true;
}
export default React.memo(MyComponent, areEqual)值得注意的是,areEqual() 这个函数的返回值和 shouldComponentUpdate 正好相反,如果 props 相等,areEqual() 返回的是 true,shouldComponentUpdate 却返回的是 false。
React.useMemo
React.userCallback
图片优化
- 使用 react-native-fast-image 替代原有的 Image 组件
它的底层用的是 🔗 iOS 的 SDWebImage 和 🔗 Android 的 Glide 。这两个明星图片下载管理库,原生开发同学肯定很熟悉,在缓存管理,加载优先级和内存优化上都有不错的表现。而且这些属性都是双平台可用,这个库都封装好了,但是官网上只有基础功能的安装和配置,如果想引入一些功能(比如说支持 WebP),还是需要查看 SDWebImage 和 Glide 的文档的。
使用 WebP
使用 webP 同样的视觉效果,图片体积会明显减少。而且可以显著减小 CodePush 热更新包的体积(热更新包里,图片占用 90% 以上的体积)。
虽然 WebP 在前端解压耗时可能会多一点点,但是考虑到传输体积缩小会缩短网络下载时间,整体的收益还是不错的。 - 图床定制图片
一般比较大的企业都有内建图床和 CDN 服务,会提供一些自定制图片的功能,比如说指定图片宽高,控制图片质量。借用云端图片定制功能,前端可以轻松通过控制 URL 参数控制图片属性。
创建和调用分离
对象创建和调用分离,其实更多的是一种编码习惯。
我们知道在 JavaScript 里,啥都是对象,而在 JS 引擎里,创建一个对象的时间差不多是调用一个已存在对象的 10 多倍。在绝大部分情况下,这点儿性能消耗和时间消耗根本不值一提。但在这里还是要总结一下,因为这个思维习惯还是很重要的。
启动
常见的优化方案主要分为三个方向
缩:缩小 Bundle 的总体积,减少 JS 加载和解析的时间
延:动态导入(dynamic import),懒加载,按需加载,延迟执行
拆:拆分公共模块和业务模块,避免公共模块重复引入
但是 React Native 的打包工具不是 webpack 而是 Facebook 自研的 Metro,虽然配置细节不一样,但道理是相通的
减小 JS Bundle 体积
Metro 打包 JS 时,会把 ESM 模块转为 CommonJS 模块,这就导致现在比较火的依赖于 ESM 的 Tree Shaking 完全不起作用,而且根据官方回复,Metro 未来也不会支持 Tree Shaking ,因为这个原因,我们减小 bundle 体积主要是三个方向:
对于同样的功能,优先选择体积更小的第三方库
利用 babel 插件,避免全量引用
制定编码规范,减少重复代码
使用 react-native-bundle-visualizer 查看包体积
使用 babel-plugin-import 对包进行按需引入
使用 babel-plugin-transform-remove-console 在打包发布的时候移除 console 语句,减小包体积的同时还会加快 JS 运行速度
代码的抽象和复用:代码中重复的逻辑根据可复用程度,尽量抽象为一个方法,不要用一次复制一次
删除无效的逻辑:这个也很常见,随着业务的迭代,很多代码都不会用了,如果某个功能下线了,就直接删掉,哪天要用到再从 git 记录里找
删除冗余的样式:例如引入 ESLint plugin for React Native,开启 “react-native/no-unused-styles” 选项,借助 ESLint 提示无效的样式文件
Inline Requires
Inline Requires 可以理解为懒执行,注意我这里说的不是懒加载,因为一般情况下,RN 容器初始化之后会全量加载解析 JS Bundle 文件,Inline Requires 的作用是延迟运行,也就是说只有需要使用的时候才会执行 JS 代码,而不是启动的时候就执行。React Native 0.64 版本里,默认开启了 Inline Requires 。
值得注意的是,Metro 的自动 Inline Requires 配置,目前是不支持 export default 导出的,这个需要特别注意一下,社区也有相关的文章,呼吁大家不要用 export default 这个语法,感兴趣的可以了解一下