前段时间开始着手React项目的开发,关于React的一些思想也有了一些体会(尤其是同vue之间的差异),特梳理&总结相关内容,便于理解。
++✓ 🇨🇳 开篇:通过 state 阐述 React 渲染++
说在前面
React中,有两种原因会导致组件的渲染:
- 组件的 初次渲染。
- 组件(或者其祖先之一)的 状态发生了改变。
State setter 函数更新变量(状态发生改变)并触发 React 再次渲染组件。
useState
Hook 提供了这两个功能:
- State 变量 用于保存渲染间的数据。
- State setter 函数 更新变量并触发 React 再次渲染组件。
核心要点
「React 组件显示到屏幕,包括三个步骤:」
- 触发:
- 组件的初次渲染。
- 组件(或者其祖先之一)状态发生了改变。
- 渲染组件
- 在进行初次渲染时, React 会调用根组件。
- 对于后续的渲染, React 会调用内部状态更新触发了渲染的函数组件。
- 提交到DOM
- 对于初次渲染, React 会使用
appendChild()
DOM API 将其创建的所有 DOM 节点放在屏幕上。 - 对于重渲染, React 将应用最少的必要操作(在渲染时计算!),以使得 DOM 与最新的渲染输出相互匹配。
- 对于初次渲染, React 会使用
示例
通过 setInterval 实现每秒+1
javascript
import React, { useState, useEffect } from "react";
export default () => {
// 定义计数
const [count, setCount] = useState(0);
/* 需求:实现每1秒+1 */
useEffect(() => {
const interval = setInterval(() => setCount(count + 1), 1000)
return () => clearInterval(interval)
}, [])
return (
<>
<p>count: {count}</p>
</>);
}
上述无法实现想要的效果!setInterval
函数每隔1秒执行一次,但 count
结果一直是1。
以下是 setInterval
函数通知 React 要做的事情:
前提:useEffect(() => {}, [])
^[1](#前提:useEffect(() => {}, []) 1只执行一次,不会在组件任何的 props 或 state 发生改变时重新运行。)^只执行一次,不会在组件任何的 props 或 state 发生改变时重新运行。
在第一次渲染期间,count
为 0
。
setCount(count + 1)
:count
是0
所以setCount(0 + 1)
- React 准备在下一次渲染时将
count
更改为1
。
- React 准备在下一次渲染时将
setCount(count + 1)
:count
是0
所以setCount(0 + 1)
- React 准备在下一次渲染时将
count
更改为1
。
- React 准备在下一次渲染时将
- 每隔1秒,执行一次上述操作
尽管每1秒调用一次 setNumber(count + 1)
,但在 这次渲染 中 count
一直是 0
,每1秒将 state 设置成 1
。
一个 state 变量的值永远不会在一次渲染的内部发生变化, 即使其事件处理函数的代码是异步的。它的值在 React 通过调用组件"获取 UI 的快照"时就被"固定"了。
React 执行函数 => 计算快照 => 更新 DOM 树
当 React 调用组件时,它会为特定的那一次渲染提供一张 state 快照。组件会在其 JSX 中返回一张包含一整套新的 props 和事件处理函数的 UI 快照 ,其中所有的值都是 根据那一次渲染中 state 的值^[2](#根据那一次渲染中 state 的值2)^ 被计算出来的!
下述例子,更容易说明上述「快照」的含义。点击一次按钮,alert
弹出 0
而不是 5
。
html
<button onClick={() => {
setNumber(number + 5);
setTimeout(() => {
alert(number);
}, 3000);
}}>+5</button>
结合上述问题,下述提供一些方案 >>>
给 useEeffect 添加响应依赖
😹性能较差,每次setInterval都会被销毁&重建(导致 Effect 在每次 count
更改时再次执行 cleanup 和 setup)
javascript
useEffect(() => {
const interval = setInterval(() => setCount(count + 1), 1000)
return () => clearInterval(interval)
}, [count])
通过更新函数设置 state 值
👍函数式更新,该函数将接收先前的 state ,并返回一个更新后的值。这样定时器每次拿到的是最新的值
javascript
useEffect(() => {
const interval = setInterval(() => setCount(v => v + 1), 1000)
return () => clearInterval(interval)
}, [])
React 将更新函数 放入 队列 中。然后,在下一次渲染期间,它将按照相同的顺序调用它们:
v => v + 1
将接收0
作为待定状态,并返回1
作为下一个状态。v => v + 1
将接收1
作为待定状态,并返回2
作为下一个状态。- 实现每1秒加1...
延伸:
html
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
}}>增加数字</button>
setNumber(number + 5)
:number
为0
,所以setNumber(0 + 5)
。React 将 "替换为5
" 添加到其队列中。setNumber(n => n + 1)
:n => n + 1
是一个更新函数。 React 将 该函数 添加到其队列中。
总结:
- 设置 state 不会更改现有渲染中的变量,但会请求一次新的渲染。
- React 会在事件处理函数执行完成之后处理 state 更新。这被称为批处理。
- 要在一个事件中多次更新某些 state,你可以使用
setNumber(n => n + 1)
更新函数。
借助 ref
👉useRef
返回一个可变的 ref 对象,返回的 ref 对象在组件的整个生命周期内保持不变。将定时器函数提取出来,每次定时器触发时,都能取到最新到 count
javascript
const counterRef: any = useRef(null)
counterRef.current = () => {setCount(count + 1)}
useEffect(() => {
const interval = setInterval(() => counterRef.current(), 1000)
return () => clearInterval(interval)
}, [])
使用 useLatest hook
👉使用返回当前最新值的 Hook(ahooks),可以避免闭包问题。
useLatest 返回的永远是最新值^3^
javascript
const latestCountRef = useLatest(count);
useEffect(() => {
const interval = setInterval(() => setCount(latestCountRef.current + 1), 1000)
return () => clearInterval(interval)
}, [])