响应式系统与 React | 青训营笔记

69 阅读5分钟

这是我参与「第四届青训营」笔记创作活动的第10天,本篇笔记主要为 React 的设计思路,从未使用框架的编程痛点入手,一步一步地讲解 React 的基本原理,以及对应特点的实现思路。

React 设计思路

UI 编程痛点

  1. 状态更新,UI 不会自动更新,需要手动地调用 DOM 进行更新。
  2. 缺欠基本的代码层面的封装和隔离,代码层面没有组件化。
  3. UI 之间的数据依赖关系,需要手动维护,如果依赖链路长,则会遇到回调地狱。

响应式和转换式

转换式系统: 给定输入,求解输出

  • 编译器
  • 数值计算

响应式系统: 监听事件,消息驱动

  • 监控系统
  • UI 界面

事件发生后,执行回调函数,回调函数导致系统内的状态变更。

image.png

响应式编程

前端多了一个步骤,即状态变更后还需要进行 UI 更新。

image.png

我们的期望是:

  1. 状态更新,UI 自动更新。
  2. 前端代码组件化,具备基本的可复用,可封装能力。
    • 且希望组件代码是有语义的,真的对应于视觉层的一块UI。
  3. 状态之间的互相依赖关系,只需声明即可。

组件化

  • 组件是组件的组合 / 原子组件
  • 组件内拥有状态(使组件有记忆),外部不可见
  • 父组件可将状态传入组件内部
    • 子组件对外暴露接口,可以消费父组件传入的状态
    • 具有灵活的复用能力

状态归属问题

一般情况下,组件状态具备局部性。
状态在公共父结点中维护,才能保证在所有需要的子组件中共享。
即多个组件要共享状态时,需要将状态上移,状态归属于多个节点最近的祖宗节点。
这种问题会导致与我们希望组件复用的初衷背离。

跨组件状态通信:
  • 状态的改变并不是由维护该状态的结点引起的。
  • 由于函数是一等公民,可以当作值被传递。因此可以在维护结点中编写一个改变状态的函数,将这个函数传递给子组件,由子组件的相应逻辑来执行该函数。
React 是单向数据流
  • 永远只能是父组件给子组件传递数据。
  • 子组件不能给父组件传递数据。
  • 但通过父组件传递一个改变状态的函数给子组件,子组件仍然可以实现改变父组件的状态。

组件设计

  1. 组件声明了状态和 UI 的映射。
    • 输入多个状态,返回一个 UI。
    • 状态改变,UI 会自动改变。
  2. 组件有 props / state 两种状态。
    • state:组件内部有自己的状态,外部不可见
    • props:父组件可以给子组件传递状态
  3. 组件可由其他组件拼装而成。

生命周期

image.png

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 语法

将一个语法的语言转换到另一个语言。

image.png

虚拟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 更新元素属性。
  • 同类型的组件元素,递归比较子结点。

image.png

React 状态管理库

解决状态上升的问题。

将状态抽离到 UI 外部进行统一管理,所有的组件直接与外部的状态管理库交互。

image.png

最好只将需要被多个组件共享的状态放入状态管理库中。

  • 因为这种方法会降低组件的复用性。
  • 使组件与外部的状态管理库强耦合。

推荐:

  • redux
  • xstate 基于状态机的思想
  • mobx
  • recoil

状态机

当前状态,收到外部事件,迁移到下一个状态。

image.png

应用级框架科普

  1. Next.js
  2. Modern.js
  3. blitz