如何理解 React
前言
第一次接触 React 时,React Hook 还尚未问世。彼时的 React 还是 class 组件的天下,编写组件时往往是以下结构:
1 | import React, { Component } from "react"; |
React 16.8 抛出了 Hook,风向转向了函数式组件。Hook 允许我们在不编写 class 的情况下使用 state 以及其他的 React 特性,最常用的便是useState()
以及useEffect()
。
再后来我接触到了 Vue2.x 以及新生的 Vue3.x,对于中小型项目(特别是一个前端一把梭的项目)来说再合适不过。数据双向绑定和各种v-
语法糖极大地吸引我使用它。
如今参加工作,我又将重新拾起 React,重新学习它、理解它。
以下是我的一些收获和思考,与您分享,同时欢迎指正和讨论。
React 是什么
这个故事得从 2013 年开始讲起,一段由 Pete Hunt 带来的名为《React: Rethinking best practices》时长大约半小时的视频(BV1eZ4y1W79a)
Pete 是这样描述 React 的:
- A library for creating user interfaces.(用于创建用户界面的库)
- Renders your UI and responds to events.(渲染你的 UI 并响应事件)
- AKA:The V in MVC.(又名:MVC 中的 V)
首先了解一下 React 诞生的背景:前端界面越来越复杂,如何将服务端或用户输入的大量动态数据高效地反映到复杂的用户界面上。
在 React 之前,用户界面的创建或更新往往需要显式的 DOM 操作,这种方式又被称为命令式。比如在原生 JavaScript 中:
1 | document.getElementById("demo").innerHTML = "Hello World!"; |
或是在 JavaScript 库的集大成者 jQuery 中:
1 | $("#demo").html("<b>Hello world!</b>"); |
当数据和事件规模较小时,这种思路尤为高效:指哪改哪。与之相对应的是声明式,React 和 Vue 都采用了这一思路。当数据和事件的规模急剧增长,命令式是让人糟心的。这倒不是说它的效率低,如果编写得当,它的效率仍高于声明式。问题出在编写得当这一步,这一工作对人来说是糟心的。
比起命令式注重变化的过程,声明式更加注重结果。它不会明确指出 A 要怎么变成 B,它只要 A 变成 B。这一点是通过一幅蓝图——虚拟 DOM 实现的,其本质是一个包含大量 DOM 相关信息的 JavaScript 对象。至于 A 要如何变成 B,这是交给 Diff 算法(负责找出差异)和 Renderer(负责渲染 DOM)的工作。每次更新时,React 会比较新旧虚拟 DOM 子树的最小差异,并将更新事件放入队列,再批量执行所有更新。
构建组件,而非模板
这一点是虚拟 DOM 带来的新思路。过去在 PHP 时代,模板大行其道。之前在接触 Laravel 时写过几个页面——它太符合直觉了,哪里变化就在哪里插入变量;复杂一些就使用流程控制和占位符——总之,你想看到的元素永远呆在它该在的地方。
现在,我们选择构建组件(它将逻辑和标记紧密耦合),这样的好处是显而易见的:关注点分离(高内聚低耦合)。在这一思路下,UI 事实上变成了由若干小组件构成的大组件,其中的小组件又进一步可分,而这些组件又都是可复用的。
React 本身无法为你完成关注点分离——这是你的工作,但它确确实实为此提供了强大的工具。还记得模板带来的直觉感吗,JSX 可以在一定程度上替代满足。它允许你书写类 HTML 语法(最后会被转为 React 的 DOM 命令以实现)。
每次更新,整个刷新
Our intellectual powers are rather geared to master static relations and our powers to visualize processes evolving in time are relatively poorly developed.
(我们的智力更擅长把握静态关系,而对于动态过程则很不擅长)
——Dijkstra
正如上言所说,人类期待确定的事情。因此,每当有数据更新,React 组件就会整个刷新,这一点保证了每个位置的数据都是最新的。这也意味着 React 组件是一个幂等函数(用相同参数重复执行,获得相同结果)。
JSX
模板语言在 JavaScript 中的呈现
1 | const e = <h1>Hello, world!</h1>; |
JSX 既非字符串,也非 HTML,它是一个 JavaScript 的语法扩展。JSX 由 React 发明,但并非 React 专属,如今 Vue 同样支持 JSX。当 JSX 融入 TypeScript 特性便是 TSX。
JSX 本质上是函数调用和表达的语法糖,JSX 元素节点会被编译成 React Element 形式。
1 | // 编译前 |
这里给出对应关系表
jsx 元素类型 | react.createElement 转换后 | type 属性 |
---|---|---|
element 元素类型 | react element 类型 | 标签字符串,例如 div |
fragment 类型 | react element 类型 | symbol react.fragment 类型 |
文本类型 | 直接字符串 | 无 |
数组类型 | 返回数组结构,里面元素被 react.createElement 转换 | 无 |
组件类型 | react element 类型 | 组件类或者组件函数本身 |
三元运算 / 表达式 | 先执行三元运算,然后按照上述规则处理 | 看三元运算返回结果 |
函数执行 | 先执行函数,然后按照上述规则处理 | 看函数执行返回结果 |
Component
组件本质上是类和函数,但组件承载了渲染视图的 UI 和更新视图的方法。因此,函数与类上的特性在 React 组件上同样具有,比如原型链,继承,静态属性等。
1 | // react/src/ReactBaseClasses.js |
对于类组件来说,底层只需要实例化一次,实例中保存了组件的 state 等状态。对于每一次更新只需要调用 render 方法以及对应的生命周期就可以了。但是在函数组件中,每一次更新都是一次新的函数执行,一次函数组件的更新,里面的变量会重新声明。
为了能让函数组件可以保存一些状态,执行一些副作用钩子,React Hooks 应运而生。
组件通信
React遵循单向数据流。
props和callback是最基本的通信方式,父组件通过props将数据传递给子组件,子组件通过执行props中的callback函数来触发父组件的方法。
除此之外还有另外四种方式:
- Ref
- 状态管理方式(Redux/Mobx)
- Context(上下文)
- EventBus
State
在React中,UI的改变来自于state的改变。在类组件中setState
是更新组件的主要方式。
1 | setState(obj,callback) |
当一次事件中触发一次setState
,首先会产生本次更新的优先级;接着React会从Fiber Root根部Fiber向下调和子节点,找到发生更新的组件、合并state并触发render函数。在commit阶段替换真实DOM完成更新。
在函数组件中,useState
赋予函数组件像类一样拥有state的能力。
1 | const [state,dispatch]=useState(initialState) |
在类组件setState
中第二个参数callback
或生命周期中的componentDidUpdate
可用于监听state改变。在函数组件中,可以把state作为依赖项传入useEffect
的第二个参数。
Props
Props是组件通信的重要手段。
Props可以是:
- 子组件的渲染数据源
- 通知父组件的回调函数
- 组件传递
- 渲染函数
- render props
- render component
在React中,props作为组件是否更新的重要准则。
在类组件中,生命周期componentWillReceiveProps
可以监听props变化(未来的替代方案是getDerivedStateFromProps
)。在函数组件中,useEffect
可以作为props变化的监听函数。
Life Cycle
React提供了一些生命周期钩子函数,让我们能够在合适的时间做合适的事。
生命周期的执行流程可以分为①组件初始化;②组件更新;③组件销毁三个阶段。
- 初始化阶段
- constructor 执行
- getDerivedStateFromProps 执行
- componentWillMount 执行
- render 函数执行
- componentDidMount 执行
- 更新阶段
- componentWillReceiveProps 执行
- getDerivedStateFromProps 执行
- shouldComponentUpdate 执行
- componentWillUpdate 执行
- render 函数执行
- getSnapshotBeforeUpdate 执行
- componentDidUpdate 执行
- 销毁阶段
- componentWillUnmount 执行
Hooks
Hooks的出现:①让函数组件也能做类组件的事,有自己的状态;②解决逻辑复用难的问题;③拥抱函数式编程。
React Router
React构建的是单页面应用(SPA),路由的切换本质上是组件的切换。
路由的两种方式:
- History:
https://www.xxx.com/home
- Hash:
https://www.xxx.com/#/home
React Router同样提供了一些Hooks。如useHref
、useLocation
、useParams
等。
Redux
为什么需要状态管理:①组件之间共用数据;②复杂组件之间通信。
Redux设计原则:①单向数据流;②State已读;③纯函数执行。
参考文献
- React 官方文档
- React Router 官方文档
- Redux 官方文档
- 掘金《React 进阶实践指南》
- 霍春阳《Vue.js 设计与实现》