Hooks 之 useState 原理解析
大纲链接 §
[toc]
逐步分析
useState基本原理和源码近似实现
1. 最简单的 useState 实现 ⇧
|
|
脑补点击
button会发生什么
- 首次渲染
render <App />- 调用
App函数,得到虚拟div,创建真实div
- 调用
- 用户点击
button按钮,调用setN(n + 1),再次render <App />- 调用
App函数,得到虚拟div,经过DOM Diff,局部更新真实div
- 调用
- 每次调用
App(),都会运行useState(0)?- 第
n次运行useState,每次得到的n并不相同
- 第
useState如何实现每次运行时,得到n的结果不一样?
脑补点击
button会发生什么
- 执行
setN的时候,会发生什么?n会变吗?App()会重新执行吗? - 如果
App()会重新执行,那么useState(0)的时候 ,n每次的值会有不同吗? - 通过
console.log可以得出答案App()会重新执行setN并不会改变state.n本身n的值会改变
分析推论
setNsetN一定会修改数据x,将n + 1存入x- 而不是直接赋值给原数据
n setN一定会触发<App />重新渲染re-render
useStateuseState一定会从x读取n的 最新值
x- 每个组件有自己的数据
x,将其命名为state
- 每个组件有自己的数据
尝试实现
React.useState
|
|
- 点击按钮,视图不更新,打印
n没有变化 - 因为每次运行
myUseState会将state重置 - 需要一个不会被
myUseState重置的变量 - 利用闭包,将变量声明在
myUseState外部
再次尝试实现
React.useState
|
|
以上代码存在的问题:如果一个组件用了 两个
useState怎么办
2. 如何让两个 useState 不冲突 ⇧
由于所有数据都放在同一个
_state中,所以会冲突1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35import React from "react"; import ReactDOM from "react-dom"; const rootElement = document.getElementById("root"); let _state; function myUseState(initialValue) { _state = _state || initialValue; function setState(newState) { _state = newState; render(); } return [_state, setState]; } const render = () => ReactDOM.render(<App />, rootElement); function App() { const [n, setN] = myUseState(0); const [m, setM] = myUseState(0); return ( <div className="App"> <p>{n}</p> <p> <button onClick={() => setN(n + 1)}>+1</button> </p> <p>{m}</p> <p> <button onClick={() => setM(m + 1)}>+1</button> </p> </div> ); } ReactDOM.render(<App />, rootElement);
改进思路
- 把
_state做成一个对象- 比如
_state = {n: 0, m: 0} - 不行,因为
useState(0)并不知道变量的命名,是n还是m
- 比如
- 把
_state做成数组- 比如
_state = [0, 0]
- 比如
使用多个
useState
|
|
- 代码示例,注意
index += 1、currentIndex和 在渲染方法中重置index = 0 - 不可以直接使用
index,需要先使用中间变量保存下currentIndexindex闭包变量,index += 1用来累计加一
- 这样会导致
_state越来越长,每次运行App,index没有被重置 - 理论上在每次渲染
App前,就应该重置index = 0- 防止每次渲染不断向
_state数组中添加新项,而不是修改对应值
- 防止每次渲染不断向
3. useState 不能写在 if 条件判断语句里 ⇧
_state数组方案缺点:if可能会打乱调用顺序
useState调用顺序- 若第一次渲染时,
n是第一个,m是第二个,k是第三个 - 则第二次渲染时必须保证调用顺序完全一致,不能跳过,颠倒顺序,否则数据就乱了
- 若第一次渲染时,
React不允许出现如下代码,查看报错
|
|
- 报错
React Hook "React.useState" is called conditionally. React Hooks must be called in the exact same order in every component render. - 点击后报错:
- 参考 React 源码赏析一则
- 另一个原因是
JS本身就不建议在条件语句中声明变量 Vue3使用另一种思路解决了这个问题…
代码中还存在两个问题
App用了_state和index,那其他组件用什么- 解决办法:给每个组件,创建一个
_state和index
- 解决办法:给每个组件,创建一个
- 放在全局作用域里重名怎么办
- 解决办法:放在组件对应 虚拟节点对象上
- 上图只画了
App组件的更新过程,两个ChildA组件也有同样的过程 - 每个组件节点都会使用自己独立的
_state和index
useState小结
- 每个函数组件对应一个
React节点**注意:目前代码对React的实现做了简化:React节点应该是FiberNode_state的真实名称为memorizedStateindex的实现则用到了链表
- 每个节点保存着各自的
state和index useState会读取state[index]index由useState出现的顺序决定setState会修改state,并触发更新
参考
使用 useRef 和 useContext解决 数据状态分身问题 ⇧
在使用
useState过程中的错误理解:setN(n + 1)会改变n
数据状态中
n的分身,代码示例1 2 3 4 5 6 7 8 9 10 11 12 13function App() { const [n, setN] = React.useState(0) const log = () => setTimeout(() => console.log(`n: ${n}`), 3000) return ( <div className="App"> <p>{n}</p> <p> <button onCLick={() => setN(n + 1)}>+1</button> <button onCLick={log}>log</button> </p> </div> ) }
两种操作,为什么
log出了旧数据?
- 1.点击
+1,再点击log:无bug - 2.点击
log,再点击+1:有bug
因为有多个
n,见时序图
setN(n + 1)永不会改变n- 每次
setN(n + 1)就会产生函数作用域内新的n,和旧的n同时存在与内存中 - 类似经典问题在
for循环中打印5个5
希望有一个贯穿始终的状态
- 使用全局变量:
window.xxx不实用 - 使用
useRefuseRef不仅可用于普通节点div,还能用于任意数据useRef的示例- 强制更新(追加更新
update(nRef.current))的示例(不推荐使用,不如去使用Vue3)
- 使用
useContextuseContext不仅能贯穿始终,还能贯穿不同组件useContext的示例
useRef 的一个作用:固定数据状态 ⇧
|
|
const nRef = React.useRef(0);// 类似对象的引用{current: 0}- 读取值
nRef.current- 类似
Vue3的ref响应式数据读取值需要.value
- 类似
- 有一个
bug是:当nRef.current += 1时改变了原来的值,但不会自动重新渲染App,因为不符合 「函数式」
可以手动触发
App更新
|
|
- 一直使用同一个对象
nRef解决bug
useContext 的作用 ⇧
实现切换主题的功能
|
|
- 初始化上下文,即全局变量
const themeContext = React.createContext(null); themeContext.Provider表示作用域范围,在标签内<themeContext.Provider>...</themeContext.Provider>- 只有此作用域里的组件(子孙组件)可以使用提供的数据
value={{ theme, setTheme }}
- 只有此作用域里的组件(子孙组件)可以使用提供的数据
- 在子组件中读取
const { setTheme } = React.useContext(themeContext);,即子孙组件调用父组件提供的全局方法
总结 ⇧
- 每次重新渲染,组件函数就会执行
- 对应所有
state都会出现数据的「分身」 - 可以使用
useRef/useContext,来得到符合预期的数据
Vue 3对比React
·未完待续·
参考文章 ⇧
- 讲义下载:React useState 原理.pdf
- 扩展阅读:React 源码赏析一则
- React Hooks 入门教程 阮一峰
- brickspert/blog 我的技术博客 React的最新相关文章
亮点
- 分析 useState 原理和源码
- useRef 的作用
- useContext 的作用
相关文章 ⇧
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名