Dawninest

React Native | 读iOS源码

ReactNative 的本质

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
是在 JS 端编写 React 代码,通过 JavaScriptCore 引擎,把 JS 端编写的组件和事件转换成 Native 原生组件进行渲染  

React 编写代码(跨端,热更新) -> JSCore引擎解析JS代码(内存消耗,性能消耗) ->映射成Native组件(Native性能)

ReactNative 启动是从创建一个 RCTRootView 作为入口视图容器开始运作的,创建 RCTRootView 时,会先创建一个 JSBridge 作为 Native 端与 JS 端交互的桥梁。整个 RN 的启动工作基本都是在创建 JSBridge 时做的。

JSBridge 的核心是 RCTBatchedBridge ,JSBridge 的工作主要在 RCTBatchedBridge 初始化中处理。启动流程采用 GCD 来进行多线程操作,其中大部分耗时操作是在并发队列com.facebook.react.RCTBridgeQueue中

RCTBatchedBridge 启动主要进行六个准备工作:
1.加载 JSBundle 代码(并行队列异步执行)
将 JavaScript 源码加载到内存中,方便之后注入和执行,这一步中,React 中的 JSX 语法已经转换成 JavaScript

2.初始化 Native Modules(同步执行)
同步初始化所有不能被懒加载的供 JS 端调用的 Native 模块
找到所有 Native 需要暴露给 JavaScript 的类(即被标记有宏:RCT_EXPORT_MODULE()的类),方便后面把这些模块信息注入 JS 端

3.初始化 JSCExecutor(与第4步在同一并行队列同时执行)
JSCExecutor 是JavaScriptCore引擎,负责JS端和Native端的通信
初始化时,创建一个优先级跟主线程优先级同级的单独 JS 线程,同时创建一个 Runloop,让 JS 线程能循环执行不会退出
初始化时,通过 JavaScriptCore 作为引擎,创建 JS 执行的上下文环境,并向 JS 上下文中注入 JS 与 Native 通信的方法

4.创建 Module 配置表(与第3步在同一并行队列同时执行)
Module 配置表: 把所有模块信息集中收集起来,保存到一个数组中,经过序列化后,注入到JS中。JS 端通过 Native端注入的 nativeRequireModuleConfig 方法,根据 module 名可以查询该模块配置信息
创建 Module 配置表,与初始化 JSCExecutor 的操作一起被加入并发队列

5.注入 Module 配置信息到 JSCExecutor(第34步执行完再执行)
当初始化 JSCExecutor 和创建 Module 配置表工作都准备好后,会将 module 模块配置信息注入 JS 端

6.执行 JSBundle 代码(前5步都执行完再执行)
以上五步操作都执行完成后,执行 JSBundle 中的 JavaScript 源码。至此,JavaScript 和 Objective-C 都具备了向对方交互的能力,启动流程的准备工作算是全部完成了。
启动完成之后,就会进入渲染层,渲染层分js层和native两部分

JS 层渲染 diff 算法
React 通过setState界面刷新时,并不会马上对所有真实的 DOM 节点进行操作,而是先通过 diff 算法计算。然后,再对有变化的 DOM 节点进行操作(native 是对原生 UI 层进行操作),具体刷新步骤如下:
1.state 变化,生成新的 Virtual Dom;
2.比较 Virtual Dom 与之前 Virtual Dom 的异同;
3.生成差异对象;
4.遍历差异对象并更新真实 DOM;

DOM 操作很耗时,使用 JS 对象来模拟 DOM Tree,在渲染更新时,先对 JS 对象进行操作,再批量将 JS 对象 Virtual Dom 渲染成 DOM Tree,从而减少对 DOM 的操作,提升性能

Virtual Dom 本质是用来模拟 DOM 的 JS 对象。一般含有标签名(tag)、属性(props)和子元素对象(children)三个属性

React Diff 算法相对于传统的 diff 算法,复杂度从 O(n^3)降到 O(n)
React基于以下的两个假设,减少了不必要的计算:
1.两个相同组件将会生成相似的DOM结构,两个不同组件将会生成不同的DOM结构。
2.对于同一层次的一组子节点,它们可以通过唯一的id进行区分。

对于假设 1: 两个相同组件,一般指的是相同的类,包含 React 官方定义的组件(View,Text)和程序员自定义的组件(这也是React 组件化开发的一个原因,可以提升 diff 算法的效率);
对于假设 2: 一般指的是使用map遍历生成的列表视图或者使用ListView/FlatList等列表组件;

相同类型节点的比较:
由于新旧节点类型相同,DOM 结构没有发生变化,仅对属性(style)进行重设从而实现节点的转换和界面的更新,这种情况,通过这类diff算法计算后,会调用 Native的 updateView 来刷新界面

不同节点类型的比较:
首先抽象成 DOM tree 节点模型,然后从父节点到子节点意义对比,最后确定需要更新的节点最小单位
通过这类diff算法计算后,会调用 Native的 manageChildren 来刷新界面

列表节点的比较:
在渲染列表节点时,它们一般都有相同的结构,只是内容有些不同而已,常见的,如使用map遍历生成的列表视图或者ListView/FlatList等列表组件,如果开发的时候没有写 key,编译器会给出警告提示
通过唯一的 key 进行区分,通过给每个节点添加唯一的 key,可以极大的简化 diff 算法,减少对 DOM 的操作。列表节点的比较主要有添加节点、删除节点、节点排序三种场景,js层diff算法计算后,会调用 Native的 manageChildren 来刷新界面

AppRegistry
而在React Native 中,AppRegistry是RN应用的入口函数。
AppRegistry负责注册运行React Native应用程序的JaveScript入口,程序入口组件使用AppRegistry.registerComponent来注册。当注册完应用程序组件后,Native系统(OC)就会加载jsbundle文件并触发AppRegistry.runApplication运行应用

其他:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Javascript 严格模式
use strict
目的:
消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
消除代码运行的一些不安全之处,保证代码运行的安全;
提高编译器效率,增加运行速度;
为未来新版本的Javascript做好铺垫。


Component和PureComponent

React.PureComponent 与 React.Component 几乎完全相同,但 React.PureComponent 通过props和state的浅对比来实现 shouldComponentUpdate()
在PureComponent中,如果包含比较复杂的数据结构,可能会因深层的数据不一致而产生错误的否定判断,导致界面得不到更新。
如果定义了 shouldComponentUpdate(),无论组件是否是 PureComponent,它都会执行shouldComponentUpdate(),并根据结果来判断是否 update。如果组件未实现 shouldComponentUpdate() ,则会判断该组件是否是 PureComponent,如果是的话,会对新旧 props、state 进行 shallowEqual 比较,一旦新旧不一致,会触发 update

在React Native中尺寸是没有单位的,它代表了设备独立像素