内容简介:React & React Native 不只是一种框架,它更是一种思维方式和方法论。Glow 使用 React Native 至今一年半有余,项目里也有越来越多的组件被重构成 React Native。在使用 React Native 开发的过程中,我们对 React 和 React Native 本身的思想、架构也有了越来越深入的理解。而这些思想又开始逐渐反作用到 Native 的开发,影响着我们在其他 Native 组件开发过程中的架构选择和实现思路,促使我们重新审视 Native 的开发方式。通过
React & React Native 不只是一种框架,它更是一种思维方式和方法论。
Glow 使用 React Native 至今一年半有余,项目里也有越来越多的组件被重构成 React Native。在使用 React Native 开发的过程中,我们对 React 和 React Native 本身的思想、架构也有了越来越深入的理解。而这些思想又开始逐渐反作用到 Native 的开发,影响着我们在其他 Native 组件开发过程中的架构选择和实现思路,促使我们重新审视 Native 的开发方式。
通过这个系列的文章,我们想把从 React 和 React Native 中所学,总结成一些有用的经验,为团队将来无论是 React Native 还是 Native 的开发提供有价值的指导。更长远的,我们希望基于这些经验构建一个新的 Native 开发框架,以提升开发效率和代码质量。
因此,本文:
- 不是 React 或 React Native 的教程,你并不能通过阅读本文学会如何进行 React 或 React Native 的开发。但如果你已经开始或正准备开始学习和使用 React 或 React Native,本文会对你理解它们的机制有所帮助。
- 不是 Native 开发或 Swift 的教程,前半部分的教程并不涉及 UIKit,也没有太多 Swift 的奇技淫巧,所以你不能通过这些文章学会如何开发一个完整的 App。
- 虽然基于 Swift 作解读,但是这些思想广泛适用于任何平台任何语言,它只是一种方法论。
初步打算分为以下方面来写:
- React 的核心思想,React Element 和 React Comopnent
- React 如何渲染和缓存 Components
- React Native 如何基于 React Components 布局和渲染 Native UI
- Props & State
- React Native 的线程模型
- Redux 的核心思想和应用
但是到你读到这一行的时候,除了第一章,其他章节的内容都还可能发生变化,我也会在写作过程中把更多的想法加入进来。
Part 1 正文现在开始,这一部分的代码都可以直接在 Xcode 的 Playground 中执行。
点这里从 Github 下载本文对应的 Playground
React 的核心概念?
React 里一个很重要的概念是:
所谓 UI(无论是一个 App,一个页面,还是一个组件)都可以理解成是一种数据结构(描述原始数据)到另一种数据结构(描述 UI)的转化(Transformation)
怎么理解呢,比如我们有一种描述“用户”的数据结构:
struct User { let name: String let job: String }
我们有一个“用户”的实例:
let allen = User(name: "Allen", job: "iOS Engineer")
我们直接定义这么一个“名片组件”并用它生成一个实例:
func NameCard(user: User) -> String { return "<View><Text>Name: \(user.name)</Text><Text>Job: \(user.job)</Text></View>" } let result = NameCard(user: allen)
得到了:
<View><Text>Name: Allen</Text><Text>Job: iOS Engineer</Text></View>
这样我们就完成了从一种数据结构到另一种数据结构(这个例子里只是伪 XML 的一个 string)的转化,这就是 UI,也是 React 的本质。看似简单,但这种抽象的力量比看上去强大的多。这个“组件”其实就类似 React 里的 Component。在 React 或者说 JS 里,更有意思的是,非原生的 ES6 里的 class,其实真的也只是一个函数,而非真的类。
纯函数(Pure Function)
在继续展开之前我们先插一嘴纯函数的概念,对纯函数有所理解的读者可以跳过这段。
在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数:
- 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关。
- 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
-- Wikipedia
举个例子,我们想为上一章节中的名片改一下字体大小,一种“不纯”的做法是:
struct Constants { static let nameFontSize = 16 } func NameCard(user: User) -> String { return "<View><Text fontSize=\"\(Constants.nameFontSize)\">Name: \(user.name)</Text><Text>Job: \(user.job)</Text></View>" }
主要的缺点很明显:
-
这个
NameCard
只支持一种fontSize
,可重用性差 -
同样的输入(
user
),会因为 Constants 的变化得到不同的输出,可测试性会变差 - 理论上的多线程安全性会变差
改成纯函数的实现则是:
struct Constants { static let nameFontSize = 16 } func NameCard(user: User, nameFontSize: Int) -> String { return "<View><Text fontSize=\"\(nameFontSize)\">Name: \(user.name)</Text><Text>Job: \(user.job)</Text></View>" } let result = NameCard(user: allen, nameFontSize: Constants.nameFontSize)
这样一来, NameCard
的可重用性和可测试性都变得更好了。
这里只是一个用于区分纯函数和非纯函数例子,因为外部变量被定义为常量,所以前后的可测试性的差别不会太大,但想象如果一个函数内部依赖外部的一个全局变量而非常量,例如一个 timer,那它们的可测试性就会差很多。
所以无论是 React 还是 Swift 的开发过程中,我们都鼓励尽可能的抽象出和定义一系列纯函数来实现业务逻辑,以提高代码可读性、可维护性和可测试性。类似的,我们鼓励尽可能使用 Immutable 实例也是出于一样的目的,用以避免没有预期的副作用。
组合/构建(Composition)
前面提到的 NameCard
是一个相当原子(没有引用其他组件)的组件,但一个复杂的组件,或者一个页面,往往由很多子组件构成,或者可以把他们理解成一堆子组件的一个容器(container),比如:
let allen = User(name: "Allen", job: "iOS Engineer") let nella = User(name: "Nella", job: "Reenigne SOi") let users = [allen, nella] // ... func NameCardList(users: [User]) -> String { let nameBoxes = users.map { NameCard(user: $0) } let innerNodes = nameBoxes.joined(separator: "\n") return "<List>\n\(innerNodes)\n</List>" } let result = NameCardList(users: users)
得到:
<List> <View><Text>Name: Allen</Text><Text>Job: iOS Engineer</Text></View> <View><Text>Name: Nella</Text><Text>Job: Reenigne SOi</Text></View> </List>
这样的抽象与组合,大大提高了代码的可读性(Readability)、可维护性(Maintainability)、可复用性(Reusability)和可测试性(Testability)。这也是 React 里用 Component 抽象所有 UI 的意义所在。
通过这种组合,我们也对各种逻辑进行了合理有效的封装,可以避免常见的 Massive View Controller。
React Element,抽象的抽象
就像《盗梦空间》里的多层梦境一样,如果说 Component 是对 UI 的抽象,那 React Element 就是第二层抽象,他把 Component 再一次抽象成另一种/层数据结构,用以描述 Component 的状态。
在讨论 React Element 的实现之前,我们先回头看一下上面的组件在实际应用中会有哪些缺点/弱点:
- UI 的构建是线性且同步的,意味着这个构建过程无法打断,也无法通过多线程/多任务提升效率
- 真正构建子组件的过程是内联(inline)的,不能很方便的在系统层面进行监督(supervise)和缓存结果
- 内存开销,这一点其实也是 1 带来的,每次实例化一个容器组件,所有的子组件都同时被实例化
React 中引入 Element 的作用就是解决以上问题,所以 Element 应该有以下特性:
- 把 Component 的状态描述与构建分离
- 高度抽象 Component 的状态,便于在系统层面做 diff 和缓存
- 轻量,降低渲染前的内存开销
简单来说,以上一节里的例子来说, (component: NameCardList, users: users)
这两个数据,已经足够描述整个 App 的状态了,即便子树中的 NameCard
还没有被渲染。Element 就是用来描述 (component: NameCardList, users: users)
这样的数据对。
参照 React 的实现和约定:为了把构建分离出来,我们把子树的构建,放入 Component 的 render
方法中去;为了统一 Component 初始化的接口,我们把 Component 所需参数统一为 props
参数,并通过范型加以约束; children
也是 React 中的 convention,用来传递子树。
基于这些条件,我们定义了如下 protocols 和 base classes:
public protocol PropsProtocol { var children: Array<ElementProtocol>? { get } } public protocol RenderableProtocol { func render() -> ElementProtocol? } public protocol ComponentProtocol: RenderableProtocol { associatedtype P: PropsProtocol var props: P { get set } init(props: P) } public protocol ElementProtocol { func createComponent() -> RenderableProtocol } struct Element<T: ComponentProtocol>: ElementProtocol { let componentClass = T.self let props: T.P func createComponent() -> RenderableProtocol { return componentClass.init(props: props) } }
如何定义和使用 Component 和 Element 呢,以 NameCard 为例:
struct NameCardProps: PropsProtocol { let children: Array<ElementProtocol>? let user: User } class NameCard: Component<NameCardProps> { override func render() -> ElementProtocol? { return nil } } let result = NameCard(props: NameCardProps(children: nil, user: allen)) print(result)
得到结果:
Element<NameCard>( componentClass: __lldb_expr_4.NameCard, props: __lldb_expr_4.NameCardProps( children: nil, user: __lldb_expr_4.User(name: "Allen", job: "iOS Engineer") ) )
例如在在 NameCardList
的 render
方法里组合 NameCard
:
struct NameCardListProps: PropsProtocol { let children: Array<ElementProtocol>? let users: Array<User> } class NameCardList: Component<NameCardListProps> { override func render() -> ElementProtocol? { let children = props.users.map { Element<NameCard>(props: NameCardProps(children: nil, user: $0)) } return Element<View>(props: ViewProps(children: children)) } } let root = Element<NameCardList>(props: NameCardListProps(children: nil, users: users)) print(root)
得到结果:
Element<NameCardList>( componentClass: __lldb_expr_4.NameCardList, props: __lldb_expr_4.NameCardListProps( children: nil, users: [ __lldb_expr_4.User(name: "Allen", job: "iOS Engineer"), __lldb_expr_4.User(name: "Nella", job: "Reenigne SOi") ] ) )
可见,当我们定义一个 NameCardList
Element 时,内存里仅有描述该状态的最小数据集,我们会在下一节讲如何构建真正的 Component 树。
至此,我们完成了把 UI 抽象成 Component,和把 Component 抽象成 Element 两大任务。结果看似简单,但这是整个 React 中的基石,也是后面章节展开的基础。
直到现在,所有的代码尚未涉及 UIKit,所以这些代码完全可以脱离 UIKit 运行。这样一来:
- 我们的 UI 逻辑也可以像业务逻辑一样,脱离平台特性而存在,提高了代码的可复用性
- 我们把可单元测试的粒度也从业务逻辑扩展到了 UI 层面,让以往需要 UI Automation 覆盖的代码逻辑可以用 UT 覆盖
所谓 JSX
写过 React 或者 React Native 的同学可能会说,这里的 render 和 React 的 JSX 完全不一样,React 中的 render 可能是这样:
const element = ( <h1 className="greeting"> Hello, world! </h1> );
其实 JSX 只是一种语法糖,上述代码最终会被翻译成:
const element = React.createElement( 'h1', {className: 'greeting'}, 'Hello, world!' );
而 createElement
的前三个参数就分别是 type
、 props
和 children
,其实与本文描述的结构是一致的。
Component 树的渲染
上一节我们已经得到了 Element 这一数据结构,他的渲染就变得很简单,我们如下定义一个 Global 的 render 方法,通过遍历,得到完整的树:
struct Node { let component: RenderableProtocol let children: Array<Node>? } func render(_ root: ElementProtocol) -> Node { let component = root.createComponent() var children: Array<Node> = [] if let childElement = component.render() { children = [render(childElement)] } return Node(component: component, children: children) } print(render(root))
我们用这个方法渲染上一节得到的 Root Element,得到:
Node( component: NameCardList( props: NameCardListProps( children: nil, users: [ __lldb_expr_6.User(name: "Allen", job: "iOS Engineer"), __lldb_expr_6.User(name: "Nella", job: "Reenigne SOi") ] ) ), children: Optional([__lldb_expr_6.Node( component: View( props: ViewProps( children: Optional([ __lldb_expr_6.Element<__lldb_expr_6.NameCard>( componentClass: __lldb_expr_6.NameCard, props: __lldb_expr_6.NameCardProps( children: nil, user: __lldb_expr_6.User(name: "Allen", job: "iOS Engineer") ) ), __lldb_expr_6.Element<__lldb_expr_6.NameCard>( componentClass: __lldb_expr_6.NameCard, props: __lldb_expr_6.NameCardProps( children: nil, user: __lldb_expr_6.User(name: "Nella", job: "Reenigne SOi") ) ) ]) ) ), children: Optional([]) )]) )
注意,我们新定义的 Node 是用来 hold Component 的实例的,所以可以理解为 Node Tree 就是 Component 的实例树。这里有一点容易搞混,Node 的 children
和 props
中的 children
并非一种东西,前者是 Component 实例的数组,后者是 Element 的数组。
因此,每一棵 Node Tree 就对应着整个 App 某一时刻的完整状态,当某些数据发生变化的时候,我们就可以通过重新遍历 Element 来决定是否需要增删改 Node Tree,这就是之后会提到的 diff 算法、rerender 过程以及 cache 的基础。
但细心的读者会发现这里的 render 到 View 为止就没有继续往下了,因为 View、Text、Image 这类 Component 被称为 Native UI Component,他们最终会被映射到一个真正的 Native View 上,因此,他们的 render 过程会涉及到 UIKit 以及最终的渲染,会在后续文章中再做展开。
总结
-
Component:
“所谓 UI 就是一种数据结构到另一种数据结构的转化”,Component 就扮演这一角色,把数据从
props
转化成 Elements - Element: 描述 UI 状态的、轻量的、临时的中间数据结构(未实例化 Component)
- Node: Component 的实例树,Element 的渲染结果,描述了一个 Component 完整的当前状态
通过定义 Component、Element 和 Node,我们完成了从数据到 UI 的转化,UI 的组合,UI 状态与渲染的分离,UI 的渲染。这些概念,就是 React 最核心、最基本的概念。
这里无形中也引入了“单向数据流”的概念(一个 user 数据从全局变量,被传递到 NameCardList 再到 NameCard,最终被组装成 View 和 Text),这一概念也是该模型的优点之一,后面讲到 state 的时候也会再次展开。
因为生成和销毁 Element 的开销要远小于操作 Node Tree 和/或 Native UI 的开销,所以这样一种“开发过程用 Element 来描述 UI,渲染引擎负责维护 Component 实例,以及最终 Native UI 的映射关系”的框架,很大程度上提高了开发效率,也提高了代码的规范化和最终的执行效率。
以上所述就是小编给大家介绍的《用 Swift 解读 React/React Native: Part 1 - React Element & React Component》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Phoenix解读 | Phoenix源码解读之索引
- Phoenix解读 | Phoenix源码解读之SQL
- Flink解读 | 解读Flink的声明式资源管理与自动扩缩容设计
- 解读阿里巴巴 Java 代码规范,第 2 部分: 从代码处理等方面解读阿里巴巴 Java 代码规范
- websocket 协议解读
- AQS源码详细解读
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。