什么是 React?
时至今日,React 如日中天,如果有人问“什么是 React?”,多半会被嘲讽无知。
但是如果能坐下来冷静的思考一下,什么是 React?为什么是 React?
索引
- React 时间简史
- 什么是 React?
React 基本素养
人机交互界面(UI)本质上是把数据以用户能够理解的方式展现出来,同时对交互作出响应。这里的交互可以来自于用户点击、麦克风、加速计甚至是眼神等,也可以是时间、空间、环境的变化。
React 只要做好两件事:
- 使用组件描述数据是如何变成界面的。
- 向开发者暴露修改数据的接口。
Component = (Data) => <View />
组件通过组合,以及数据的传递,构成了 App 的基本要素。
那么组件要如何“正确”的描述数据与界面之间的关系呢?
让我们来回顾一下 React 创建组件的历史。
初代 React 对象组件
最初的 React 组件是通过向 React.createClass
方法传递一个组件“配置”清单来创建组件的。
看起来有点像 Vue 或小程序,没错这就是 React 走过的弯路,却对其他框架的发展产生深远影响。
在初代 React 组件里,已经能看到一个组件的基本要素:属性、状态、渲染、回调以及生命周期。一个组件会把属性和状态当成数据源,通过渲染函数生成界面,之后监听事件和生命周期进入下一个渲染周期,周而复始的把生成的界面输出给 React,由 React 组合起来最终交由 ReactDOM 输出给浏览器。
注意这个特别的 mixins
字段,它的作用是把另一个对象和当前的配置合并,之所以会出现这样的设计,是因为当年还没有对象展开语法 { ...mixins }
,Object.assign
又太丑了。值得一提的是这种写法每个方法里的 this
都能正确指向当前组件,包括 mixins
里的方法,这又何尝不是一种优势啊。
这样的写法从 React 15.5.0 开始反对使用,反对的理由并不是这样写有多大的问题,只是因为 React 诞生之初 JavaScript 还没有正式的 class 标准。JavaScript 社区对继承有不同的理解,普遍采用基于原型链的继承方式,这是很多模块作者必须掌握的一项技能,写法多样,难以掌握,在大部分面向对象语言里一支独秀,于是 React 说:“陈独秀你坐下!”。
二代 React 类组件
随着 ES2015 草案敲定,JavaScript 终于拥有了官方认可的 class,虽然此时还是需要通过 Babel 把 class 编译成原型链继承代码,但对用户来说这一过程是透明的。
一个组件,继承自父类 React.Component
,状态变成类的字段,事件和生命周期变成类的方法,面向对象语言几十年的最佳实践都搬过来,几十种设计模式也套过来,一切看起来都是那么美好。
老板让我这个后端 Java 写 React,因为他觉得这两个东西看起来很像。
果真如此吗?
还记得初代组件里的 mixins
吗?如果要复用逻辑,我该如何 mixin 到类组件里呢?或许你首先想到的是继承,没错,确实有人这样做,例如 Taro UI 的 AtComponent。可是 mixins
的值是一个数组,每个组件都可以有选择的复用几套逻辑,JavaScript 可不许继承多个父类,Java 也不能,C++ 可以(搞不懂有多个爸爸是什么感觉)。
caution
这里提及 mixins 并不是在列举它的好处,实际上官方早已列举了 mixins 的种种罪状了。
可我该怎么复用逻辑呢?
官方给出的答案是 HOC(高阶组件),高阶组件是参数为组件,返回值为新组件的函数。
唔~ 我想复用逻辑,结果现在我有两个组件了。每多一次复用,就要再包一层,我自己的组件就这样被包成了粽子。端午节快乐! 甚至还有一种帮你包粽子的工具函数 compose
,例如 lodash.flowRight
,把原本难看的 C(B(A(Comp)))
改写成 compose(A, B, C)(Comp)
。把“高阶”玩出花样的要数 Redux
,它的 connect
函数是一个返回高阶组件的高阶函数!
这还不算完,因为高阶组件也是组件,最好指定名称 (displayName),否则在 React Developer Tools 里无法区分。就好像光看外皮你分不清粽子是什么馅的。如果原组件有静态方法,高阶组件要一并复制过来,否则你的行为和内部组件不符,就露馅了。
还能不能 Happy coding 啦?
社区稍候给出另一个答案 Render Props,术语 “render prop” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术。
既然伪装一个组件这么麻烦,索性来明的吧,我把数据传给你,怎么用是你的事情。
这种做法比 HOC 好那么一点,但对使用者不再透明,你必须显示的“接收”这些传入的值,再继续向下传递。那一天,人类终于回想起了曾一度被 回调地狱 所支配的恐怖。进而催生出 react-adopt 这个项目,像 compose
组合 HOC 一样去组合 Render Props。。。
逻辑复用是 React class 组件永远的痛。
现代 React 函数式组件
important
本节代码不保证正常运行,主要是为了说明原理。
React 曾经在很长一段时间里把函数写成的组件叫做“无状态组件 (StatelessComponent 或 SFC)”,原因不言自明,因为没有状态,不是不想,是不能。脑补“臣妾做不到哇”时的表情。
让我们看一下最初是如何用函数写组件的。
这么简单的一行代码的组件,我们实在不能对它奢求太多,毕竟配合 React.memo(FuncComp)
,会瞬间变成比 React.PureComponent
还纯的组件,属性不变时直接跳过渲染。
有什么办法可以让函数插上翅膀吗?
接下来让我们依次赋予它一些超能力,看看一个简单的函数是如何一步一步变成火箭的。
变量保持
组件的价值在于可重用,React 在处理 class 组件时,会在组件树中对应位置初始化 class 实例,需要时调用组件的 render 方法获取新的界面。
如果换成 function 组件,函数本身扮演了 render 方法,函数内部的局部变量在函数运行之前还没有初始化,运行之后会被回收,也就是说 function 只能通过参数调整输出结果。
如果想让函数组件满足最基本的需求“变量保持”,需要在函数外部存放变量,这个值发生变化不会触发组件重新渲染,因为是引用,读取到的值永远是最新的。
似乎可行,前提是 Fun 组件只用一次。造火箭是一个持续迭代的过程,我们先保留这个简单的例子。
状态保持
class 组件的另一个能力是保存组件的当前状态,并且在状态发生变化后更新组件,当然我们要遵守 React 的约定,状态字段名必须是 state,修改状态必须通过 this.setState
,React 也会信守承诺,做好组件更新工作。
状态保持可以看作是变量保持的增强版,同样需要在函数外部存放 state 值。
状态可以修改了,但是还缺少一种更新组件的手段,组件一般是被动更新的,所以我们先借用一下 React 的更新能力。
caution
下面示例中的 caller 是函数的废弃属性,不要用在真实项目上。
生命周期
所谓生命周期,是 React 在渲染过程中与组件之间的协调过程,组件的渲染是用户代码和 React 之间彼此配合的结果,这中间会发生若干次控制权的交接。
函数组件要想实现这样的效果,需要一个伴随组件渲染的函数。
这似乎没什么用,callback
只是换了一个地方执行,和直接写在组件里是一样的效果,让我们改造一下对运行条件施加一些限制。
为了模拟生命周期,我们在函数组件里放置一个 useEffect 函数,并且指定只有依赖值发生变化时才执行回调,这样一个有条件执行的 useEffect 兼具 componentDidMount 和 componentDidUpdate 特色。
让我们把 useEffect 和上一节提到的 useState 放在一起看下会产生什么样的效果。
一个比较粗糙的火箭造好了,我点火试了几次都能发射,希望你点火的时候别出什么意外。
或许你觉得这跟真正的 React 差远了,这些组件和 useXXX 函数都是单例的。没错,我在造火箭,一次性的火箭,我知道马斯克的火箭是可回收的,只要有时间继续深入,我们的火箭也能实现重复利用,而且是三级火箭在回收的过程中实现空中对接整体返回。
tip
以上介绍的是 React 最基础的 3 个 Hooks API,真实的 API 并不完全是这样运作的,这些例子只是向你展示 JavaScript 的另一面,函数式编程的魅力,即:函数是一等公民。
React Hooks 的意义
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
回顾 class 组件,逻辑复用是 class 组件永远的痛。不仅如此,单单是 JavaScript 中的 class,也一直饱受非议,原因很简单,class 诞生的时间太晚,还只是个襁褓中的婴儿,同时它也是标准委员会硬塞给 JavaScript 的半成品。JavaScript 曾经自诩是函数式语言(虽然真正的函数式语言认为它并不纯粹),现在有了 class,就再也解释不清了。
React 批评 JavaScript class 有以下几个问题:
- 难以理解的
this
,与其他语言存在巨大差异。 - 没有稳定的语法提案。你今天写的代码,未来可能会有别的含义。
- class 组件会无意中鼓励开发者使用一些让优化措施无效的方案。
React 组件一直更像是函数,而 Hook 则拥抱了函数。
每一个 Hook 代表一小块“逻辑”,Hook 可以包装,相关的 Hooks 可以组合,只要在外面再包裹一层函数即可,逻辑复用变得和组件复用一样自然。甚至在编辑器里选中几行 Hooks 点击重构就能生成一个新的自定义 Hook。
类似的例子不胜枚举,请参考 react-use,它的源码是你提高对 Hooks 认知的捷径。
什么是 React?
了解了 React 的发展过程,分析了 React 极力推荐的 Hooks API,让我们回到最初的问题:“什么是 React?”
主流的前端框架把屏幕当成电子纸,可以反复重绘。之前的前端框架则误以为屏幕是一块画布,一手拿着画笔,一手拿着涂改液。
回到这个经典的公式:
Component = (Data) => <View />
- Component 重复使用。
- Data 因时而变。
- View 因数而变。
现在你应该明白,为什么 Facebook 说:“React 组件一直更像是函数。”
组件只不过是把传入的参数当作数据,再返回一个恰当的界面罢了。
React 只不过是在正确的时间做了正确的事情。
刚一推出就坚持不用 HTML 模板,而是采用 React.createElement
创建节点,JSX 可以看成是妥协的产物,本质上还是 JavaScript。
中间经历了 class 组件一波三折,终于回归本源。
一个函数,几个钩子。
一桌、一椅、一扇、一抚尺而已。