这是我参与「第四届青训营」笔记创作活动的第10天,本篇笔记主要为 React 的设计思路,从未使用框架的编程痛点入手,一步一步地讲解 React 的基本原理,以及对应特点的实现思路。
React 设计思路
UI 编程痛点
- 状态更新,UI 不会自动更新,需要手动地调用 DOM 进行更新。
- 缺欠基本的代码层面的封装和隔离,代码层面没有组件化。
- UI 之间的数据依赖关系,需要手动维护,如果依赖链路长,则会遇到回调地狱。
响应式和转换式
转换式系统: 给定输入,求解输出
- 编译器
- 数值计算
响应式系统: 监听事件,消息驱动
- 监控系统
- UI 界面
事件发生后,执行回调函数,回调函数导致系统内的状态变更。
响应式编程
前端多了一个步骤,即状态变更后还需要进行 UI 更新。
我们的期望是:
- 状态更新,UI 自动更新。
- 前端代码组件化,具备基本的可复用,可封装能力。
- 且希望组件代码是有语义的,真的对应于视觉层的一块UI。
- 状态之间的互相依赖关系,只需声明即可。
组件化
- 组件是组件的组合 / 原子组件
- 组件内拥有状态(使组件有记忆),外部不可见
- 父组件可将状态传入组件内部
- 子组件对外暴露接口,可以消费父组件传入的状态
- 具有灵活的复用能力
状态归属问题
一般情况下,组件状态具备局部性。
状态在公共父结点中维护,才能保证在所有需要的子组件中共享。
即多个组件要共享状态时,需要将状态上移,状态归属于多个节点最近的祖宗节点。
这种问题会导致与我们希望组件复用的初衷背离。
跨组件状态通信:
- 状态的改变并不是由维护该状态的结点引起的。
- 由于函数是一等公民,可以当作值被传递。因此可以在维护结点中编写一个改变状态的函数,将这个函数传递给子组件,由子组件的相应逻辑来执行该函数。
React 是单向数据流
- 永远只能是父组件给子组件传递数据。
- 子组件不能给父组件传递数据。
- 但通过父组件传递一个改变状态的函数给子组件,子组件仍然可以实现改变父组件的状态。
组件设计
- 组件声明了状态和 UI 的映射。
- 输入多个状态,返回一个 UI。
- 状态改变,UI 会自动改变。
- 组件有
props/state两种状态。state:组件内部有自己的状态,外部不可见props:父组件可以给子组件传递状态
- 组件可由其他组件拼装而成。
生命周期
React (hooks) 的写法
Hooks 是可以挂载到组件生命周期上去执行的函数。
副作用函数:执行函数会给组件外部的数据造成影响,与外部系统发生交互,改变外部系统。
- 发起网络请求
- Web Storage 存储
纯函数:同样的参数传入会得到相同的输出结果。
会产生副作用的函数需要在 useEffect 中执行。
useEffect 的执行时机:
- 在组件 onmount 时会执行一次
- 在依赖数组中的依赖项发生改变时执行
- 没有传入依赖数组,则 useEffect 会在每次更新时调用。
- 如果依赖数组为一个空数组,则 useEffect 只会在组件挂载时执行一次。
const App = () => {
const [x, setX] = useState(0)
const [y, setY] = useState(0)
const sum = x + y
useEffect(() => {
document.title = `${x} - ${y}`
})
return {
<div>
<h1>和是{sum}, x={x}, y={y}</h1>
<button onClick={() => setX(x + 1)}>x + 1</button>
<button onClick={() => setY(y + 1)}>y + 1</button>
</div>
}
}
React 的实现
React 实现上的问题:
JSX不符合JS标准语法React代码无法直接在浏览器中运行
- 返回的
JSX发生改变时,如何更新 DOM State/Props更新时,要重新触发render函数render函数即组件函数
JSX 不符合 JS 语法
将一个语法的语言转换到另一个语言。
虚拟DOM:
Virtual DOM 是一种用于和真实 DOM 同步,而在 JS 内存中维护的一个对象,它具有和 DOM 类似的树状结构,并和 DOM 可以建立一一对应的关系。
由于组件是可以嵌套的,当父组件发生更新时,所有的子组件都要重新 render(即所有子组件函数都被重新执行)得到一个新的 虚拟 DOM 树 与 旧的虚拟 DOM 树 进行比对。成为性能瓶颈。
对新的虚拟 DOM 树中发生更新的部分渲染为 真实 DOM,即完成了 虚拟 DOM 的动态更新。
如何进行 Diff 操作:
将一棵树变为另外一棵树,所需要的最小的步骤:
- 完美的最小 Diff 算法,需要 O(n^3) 的复杂度。
- 牺牲理论最小 Diff,换取时间,得到了 O(n) 复杂度的算法:Heuristic O(n) Algorithm。可能并非最优解,但是是局部最优解。
两个树从根结点开始递归比较:
- 不同类型(type:img / div / …)的元素, 替换以该结点为根的整个子树
- 同类型的 DOM 元素,但属性发生了变化,使用 DOM API 更新元素属性。
- 同类型的组件元素,递归比较子结点。
React 状态管理库
解决状态上升的问题。
将状态抽离到 UI 外部进行统一管理,所有的组件直接与外部的状态管理库交互。
最好只将需要被多个组件共享的状态放入状态管理库中。
- 因为这种方法会降低组件的复用性。
- 使组件与外部的状态管理库强耦合。
推荐:
reduxxstate基于状态机的思想mobxrecoil
状态机
当前状态,收到外部事件,迁移到下一个状态。
应用级框架科普
Next.jsModern.jsblitz