带着原理重读 React 官方文档(二)
# Props 的只读性和 State 的响应性
# Props 的只读性
原文
- 组件无论是使用函数声明还是通过 class 声明,都绝不能修改自身的 props。
- 所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
此处引用 “纯函数” 的概念来说明 Props 的值应该是不可更改的。什么是纯函数呢?
WIKI: 纯函数
在程序设计中,若一个函数符合以下要求,则它可能被认为是纯函数:
- 此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由 I/O 设备产生的外部输出无关。
- 该函数不能有语义上可观察的函数副作用,诸如 “触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
纯函数具有两个特征:
- 相同的输入,总是会得到相同的输出。
- 执行过程中没有任何副作用。
注意不要以此认为函数式组件是纯函数,即使无状态函数式组件(完全受控组件)也才勉强算做纯函数,即使如此,组件的顶层也无法保证没有任何的副作用、不受组件外部环境的影响。但是好在函数式组件并不依赖于纯函数的原理,组件的可交互性和复杂性根本无法以纯函数保证!
但是我们可以从纯函数的角度去思考组件的优化方向,纯函数本质上是从静态的观点去思考,提供更好的性能和稳定性保障。下面我们就从这个角度出发再次剖析组件的优化理解:
- 尽量减少属性的变化,尤其是无效的变化(对于
memo
组件而言)。保持相同的属性,可以一定程度上减少组件的重复渲染。从源码中有:
// reconcileSingleElement: src/react/packages/react-reconciler/src/ReactChildFiber.new.js
// 复用节点
const existing = useFiber(child, element.props);
// 新建节点
const created = createFiberFromElement(element, returnFiber.mode, lanes);
2
3
4
5
可以看出,React 从 JSX 中获取 Props,显然 Props 的引用是每次都发生了变化。意思就是说,如果父组件发生了重新渲染,正常情况下子组件一定会发生重新渲染。React 默认并不会对 Props 做浅比较,如果需要的话,需使用 PureComponent
或者 memo
。注意,这只能作为一种优化的手段,不能对此形成依赖的范式,因为这可能会造成意想不到的 bug。
综上来看,要想完全成为纯函数式的组件,除了需要是无状态,还需要经过 memo
的缓存处理以保证属性无变化,这样 React 才能避开重新渲染。但是这样的场景,实际上使用场景很有限。
- 组件顶层不应暴露任何副作用!
副作用指的是函数在执行过程中产生了外部可观察变化。这种变化导致纯函数所执行的结果不可预测。这对于函数式组件很有借鉴意义。任何副作用都应该在 useEffect
或者 useLayoutEffect
中注册,不能暴露在组件的顶层环境。
参考:
- 纯函数是什么?怎么合理运用纯函数? (opens new window)
- javascript - Does React make any guarantees that the `props` object reference stays stable? - Stack Overflow (opens new window)
- How to use React.memo() to improve performance (opens new window)
- RFClarification: why is `setState` asynchronous? · Issue #11527 · facebook/react (opens new window)
# Props 的变化可能来自于 State
Props 的变化归根结底是因为组件感知到祖先组件的状态的变化(引用变化),这种状态的变化通过 props 或者 context 的方式传递,能够被组件所捕捉到。
Props 的变化从 State 的变化而被感知。当 React 意识到 Props 的变化时,已经是在渲染的过程中了。React 并不会因为 Props 的变化做出立即反应,而是通过 State 的变化间接地感知 Props 的变化。当然 Props 的变化对于单一组件而言,在内部 State 未发生变化的情况下,React 必须对其做出反应。当然更多的情况是,React 会得到一个新的 Props 的对象引用,从而重新渲染子组件。虽然这种情况可以通过一些方式来阻止,如通过浅比较甚至深比较的方式比对 Props 的变化,我们仍然会对 React 重新渲染的花费感到担忧。一个可以让我们如释重负的事实是,重新渲染并不是意味着重新渲染视图,视图仍然是按需渲染,这是 React 的原则。
# 重新渲染!== 重新渲染 DOM 视图
我们从上所讲的 “重新渲染” 并不是代表着 React 将重新创建 DOM 结构并渲染视图。这里的 “重新渲染” 意为组件的重新渲染,当然渲染必须要走调和的流程,在调和的过程中,组件中的节点仍然会被复用,同样地副作用标记也会进行下去。但是很快 React 会意识到组件中并无需要实质更新的节点。如果说要计算这种 “重新渲染” 的成本的话,我认为就是一次 DFS 的成本。这种成本跟创建 DOM 节点实例和渲染 DOM 节点相比要小得多。所以,我们大可舒心。
# State 的响应性
与 Vue 响应式原理不同,React 中单向数据流的模型决定了 React 对于响应式更新是粗粒度的、合并的和异步的。React 的响应式是应用级的,响应式的节奏遵循调度器的调度。如果我们同样从 “帧” 的观点来理解这种响应式的话,在 “帧” 与 “帧” 之间实际上是响应性的收集阶段,从这阶段在 React 中被称为 Batch
阶段可见一二,在 “帧” 的绘制(渲染到屏幕)阶段实际上是消费响应式的阶段,从这阶段的源码中你能看到很多 flush
、 Effect
类似的字眼。
任何的响应式系统都需要解决两个问题:
- 副作用的追踪。
- 副作用的消费。
现在我们来就这一点对比下 React 和 Vue 的响应式系统。
Vue3
中的响应式系统:
- 副作用的追踪:ViewModal(响应式系统,如 ref 和 reactive ) 会追踪响应式数据的变化,并收集与响应性相关联的副作用(收集响应式通常需要一定的 API 入口),称为
track
; - 副作用的消费:当响应式数据改变而被追踪时,
trigger
会消费收集到的副作用,副作用的执行会对 DOM 视图执行细粒度的更新。
React
中的响应式系统:
- 副作用的追踪:
Batch
阶段中,React 将从setState
收集到更新,并形成更新队列。这种更新即是响应式数据的变化状态,它们将缓存于 React 的 Fiber 系统中。 - 副作用的消费:
Render
阶段,React 将消费收集到的更新并进行调和过程,调和过程将会影响到视图的渲染。
可以看到,在实现响应式的特性上,React 和 Vue 实际上采取了不同的方式。不同的方式之间,响应式的细粒度不同,当然也各有利弊。但是不论是何种方式,都必须要保证 DOM 更新是细粒度的,而不可是全量的。
注意
也有一些观点认为,React 其实并没有很 Reactive。甚至 React 官网中也明确表示 React 的设计理念并没有那么 Reactive,这其中调度器的存在有很大的影响。React 坚持 pull
的方式,而非 push
的方式。参见:
# 正确地使用 State
原文
关于 setState () 你应该了解三件事:
- 不要直接修改 State
- State 的更新可能是异步的
- State 的更新会被合并
对于 State 的理解,此三点是重中之重。从原理来理解这三点可能会更加的深刻。
React 中的 State 状态必须经过 setState 的收集才能才能被响应。State 在稳定组件渲染中是静态的,React 需要状态的副作用被观测到才能在下一次渲染中对 State 做出正确的响应。直接修改 State 没有任何的作用,因为 State 的变化并不会被观测,而是被丢弃。另外,Props 也是不可修改的,因为在组件中 Props 实际上是受控的数据。这背后的根本原因还是在于 React 的受控的渲染机制。
State 的更新是异步的,原因有二:一是状态的变化是通过状态链表管理的;二是状态的变化(副作用)需要通过调度器的回调才能在渲染过程中被消费。至于这里为什么是说是 “可能”,我还不清楚是否有一定的方法可以使状态同步的被跟踪。既然 State 的更新是通过链表(环形链表)管理的,在真正的渲染时机时状态变化的副作用将会被批量的消费,因此,State 的更新会被合并。
参考:
# 数据是向下流动的
React 中对于单向数据流的思想解释的非常巧妙和深刻。
原文
- 不管是父组件或是子组件都无法知道某个组件是有状态的还是无状态的,并且它们也并不关心它是函数组件还是 class 组件。这就是为什么称 state 为局部的或是封装的的原因。除了拥有并设置了它的组件,其他组件都无法访问。但是组件可以选择把它的 state 作为 props 向下传递到它的子组件中。这通常会被叫做 “自上而下” 或是 “单向” 的数据流。任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中 “低于” 它们的组件。
- 如果你把一个以组件构成的树想象成一个 props 的数据瀑布的话,那么每一个组件的 state 就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。
- 在 React 应用中,组件是有状态组件还是无状态组件属于组件实现的细节,它可能会随着时间的推移而改变。你可以在有状态的组件中使用无状态的组件,反之亦然。
# 组件间不关心组件的实现细节
按照 React 基于 Props 和 State 的反向数据流模型,组件之间并不关心其实现的细节,如组件的类型、组件的状态等。(这和 Flutter 不同,Flutter 会严格区分有状态组件和无状态组件)。组件的实现细节只是承载和传递数据流的一种方式,只要能够符合 React 总体的数据流的原理,其实现可以是多样的。从组件树的角度来看,React 最大程度上实现了组件间的逻辑解耦,使得我们可以将类组件和函数式组件进行混用,同时也不用关注组件是否是有状态组件,但是这样做的代价是,React 调和器必然要为这样的复杂性买单,这也是其源码看似很复杂的原因。
事实上,这并不代表 React 不关心组件的实现细节,React 关心组件的类型并提供组件的渲染和数据流的机制,也关心组件的状态以实现状态的更新。
# “自上而下” 的数据流
“自上而下” 的数据流核心在于 Props 的流动性,Props 可以从父组件流向子组件,这是构成 React 中数据流从根组件(祖先组件)流向叶子组件(子组件)的根本原因。当然如果单凭 Props 流动性还不足以支撑交互的复杂性,因为在相邻的两次 “渲染” 之间,Props 是静态的,况且 React 从根本上缺少了更新的驱动力,于是 State 的概念便应运而生。State 允许组件内部存在单向的数据流,并且这样的数据流可以转化为 Props “向下传递”!State 如此重要,是因为:
- State 是 Props 变化的 “驱动力”。
- State 是 React 渲染的 “驱动力”。
这两个 “驱动力” 使得 State 的重要性完全不亚于 Props。
- 绝大部分的 Props 的变化来源于 State 的变化。当然也有可能来源于 Props 的变化,但是如果 Props 的流动超过三层组件,就应该反思组件的结构设计是否合理,是否考虑使用状态管理工具了。State 的变化导致 Props 的变化,同时 React 调和器会关心 Props 的变化(Props 的最新状态),这使得 Props 的变化可以向下传递。
- State 的变化驱动着 React 的渲染。State 的变化会发起渲染的调度请求,促使调度器产生回调。如果没有 State,React 就不能实现更新渲染!
# React 因 Props 和 State 的变化而重新渲染
关于 Props 和 State 变化引起 React 重新渲染,有如下的区别:
- Props 的变化是被动的被调和器所感知的,React 并不收集 Props 的变化。
- State 的变化时主动的被调和器所感知的(经由调度器),React 会主动的收集 State 的变化。
这两种的区别,便是 “主动” 和 “被动” 的区别,也是 “push” 和 “pull” 的区别。
State 才是 React 重新渲染的根本原因。Props 的变化会是 React 重新渲染,但是 Props 的变化在大部分时间是还 State 的变化所影响的。
# State 是局部的
State 是局部的,是组件内部封闭的。但是 State 如果转化为 Context 就可以为组件树所公用,这得益于 context.Provider
实现了基于组件树的、局部的、封闭的状态共享。
# 事件处理
原文
- 在 React 中另一个不同点是你不能通过返回 false 的方式阻止默认行为。你必须显式地使用 preventDefault。
- e 是一个合成事件。React 根据 W3C 规范来定义这些合成事件,所以你不需要担心跨浏览器的兼容性问题。React 事件与原生事件不完全相同。
React 中事件处理与原生 DOM 稍有不同,在原生的 DOM 中,通常需要操作 DOM 并通过 addEventListener
添加事件监听器。React 中有完整的事件系统,所有的事件均在 JSX 中显式地定义和绑定。事件绑定以 Props 的方式落实在 DOM 上,支持传递
# React 与 Rxjs
参考:
- Rxjs + React 实战,看完你就知道为什么说 angular 在 5 年后等你 - 知乎 (opens new window)
- Reactive Programming with React and RxJs | Better Programming (opens new window)
参考:
- A Visual Guide to React Rendering - Props | Alex Sidorenko (opens new window)
- reactjs - React: Parent component re-renders all children, even those that haven't changed on state change - Stack Overflow (opens new window)
- State & 生命周期 – React (opens new window)