Hooks
之 useState
原理解析
大纲链接 §
[toc]
逐步分析 useState
基本原理和源码近似实现
1. 最简单的 useState
实现 ⇧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function App() {
console.log('App 运行了')
const [n, setN] = useState(0)
console.log(`n: ${n}`)
return (
<div className="app">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
</div>
)
}
ReactDOM.render(<App />, rootElement)
|
脑补点击button
会发生什么
- 首次渲染
render <App />
- 用户点击
button
按钮,调用setN(n + 1)
,再次 render <App />
- 调用
App
函数,得到虚拟div
,经过DOM Diff
,局部更新真实div
- 每次调用
App()
,都会运行useState(0)
?
useState
如何实现每次运行时,得到n
的结果不一样?
脑补点击button
会发生什么
- 执行
setN
的时候,会发生什么?n
会变吗?App()
会重新执行吗?
- 如果
App()
会重新执行,那么useState(0)
的时候 ,n
每次的值会有不同吗?
- 通过
console.log
可以得出答案
App()
会重新执行
setN
并不会改变state.n
本身
n
的值会改变
分析推论
setN
setN
一定会修改数据x
,将n + 1
存入x
setN
一定会触发<App />
重新渲染re-render
useState
x
尝试实现 React.useState
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
|
import React from 'react'
import ReactDOM from 'react-dom'
const rootElement = document.getElementById('root')
function myUseState(initialValue) {
console.log('myUseState运行')
let _state = initialValue
function setState(newState) {
_state = newState
render()
}
return [_state, setState]
}
// 借用ReactDOM.render
const render = ReactDOM.render(<App />, roootElement)
function App() {
console.log('App运行')
const [n, setN] = myUseState(0)
console.log(`n: ${n}`)
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => {setN(n + 1)}>+1</button>
</p>
</div>
)
}
ReactDOM.render(<App />, rootElement)
|
- 点击按钮,视图不更新,打印
n
没有变化
- 因为每次运行
myUseState
会将state
重置
- 需要一个不会被
myUseState
重置的变量
- 利用闭包,将变量声明在
myUseState
外部
再次尝试实现 React.useState
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
35
|
import React from 'react'
import ReactDOM from 'react-dom'
const rootElement = document.getElementById('root')
let _state
function myUseState(initialValue) {
console.log('myUseState运行')
// _state = _state === undefined ? initialValue : _state
// _state = _state || initialValue // 有0值判断的bug
_state = _state ?? initialValue // 空值合并运算符 保证常量不为 null 或者 undefined
function setState(newSstate) {
_state = newState
render()
}
return [_state, setState]
}
// 借用ReactDOM.render
const render = ReactDOM.render(<App />, roootElement)
function App() {
console.log('App运行')
const [n, setN] = myUseState(0)
console.log(`n: ${n}`)
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => {setN(n + 1)}>+1</button>
</p>
</div>
)
}
ReactDOM.render(<App />, rootElement)
|
以上代码存在的问题:如果一个组件用了 两个 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
35
|
import 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
做成数组
使用多个useState
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
35
36
37
38
39
40
41
|
let _state = []
let index = 0
function myUseState(initialValue) {
const currentIndex = index
console.log('currentIndex', currentIndex)
index += 1
// 等价于 // _state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex]
_state[currentIndex] = _state[currentIndex] ?? initialValue
const setState = newState => {
console.log('index', index)
_state[currentIndex] = newState
console.log('_state', _state)
render()
}
return [_state[currentIndex], setState]
}
const render = () => {
index = 0 // 重置index 防止每次渲染不断向_state中添加
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);
|
- 代码示例,注意
index += 1
、currentIndex
和 在渲染方法中重置index = 0
- 不可以直接使用
index
,需要先使用中间变量保存下currentIndex
index
闭包变量,index += 1
用来累计加一
- 这样会导致
_state
越来越长,每次运行App
,index
没有被重置
- 理论上在每次渲染
App
前,就应该重置index = 0
- 防止每次渲染不断向
_state
数组中添加新项,而不是修改对应值
3. useState
不能写在 if
条件判断语句里 ⇧
_state
数组方案缺点:if
可能会打乱调用顺序
useState
调用顺序
- 若第一次渲染时,
n
是第一个,m
是第二个,k
是第三个
- 则第二次渲染时必须保证调用顺序完全一致,不能跳过,颠倒顺序,否则数据就乱了
React
不允许出现如下代码,查看报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// ...
function App() {
const [n, setN] = React.useState(0);
let m, setM
if(n % 2 === 1) { // if 打乱了调用顺序
const [m, setM] = React.useState(0);
}
// 此时如果还有一个数据k,就会被提前到第二的位置
// const [k, setK] = React.useState(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>
);
}
// ...
|
- 报错
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
的真实名称为memorizedState
index
的实现则用到了链表
- 每个节点保存着各自的
state
和index
useState
会读取state[index]
index
由useState
出现的顺序决定
setState
会修改state
,并触发更新
参考
使用 useRef
和 useContext
解决 数据状态分身问题 ⇧
在使用useState
过程中的错误理解:setN(n + 1)
会改变n
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function 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
不实用
- 使用
useRef
- 使用
useContext
useRef
的一个作用:固定数据状态 ⇧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
const nRef = React.useRef(0); // 类似对象的引用 {current: 0}
const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
return (
<div className="App">
<p>{nRef.current} 这里并不能实时更新</p>
<p>
<button onClick={() => (nRef.current += 1)}>+1</button>
<button onClick={log}>log</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
|
const nRef = React.useRef(0);
// 类似对象的引用 {current: 0}
- 读取值
nRef.current
- 类似
Vue3
的ref
响应式数据读取值需要.value
- 有一个
bug
是:当nRef.current += 1
时改变了原来的值,但不会自动重新渲染App
,因为不符合 「函数式」
可以手动触发App
更新
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
|
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
const nRef = React.useRef(0); // 类似对象的引用 {current: 0}
const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
// 手动触发`App`更新
// const [n, setN] = React.useState(null)
// const setN = React.useState(null)[1]
const update = React.useState(null)[1]
return (
<div className="App">
<p>{nRef.current} 这里并不能实时更新</p>
<p>
<button onClick={
() => {
nRef.current += 1
// setN(nRef.current)
update(nRef.current)
}
}>+1</button>
<button onClick={log}>log</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
|
useContext
的作用 ⇧
实现切换主题的功能
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
35
36
37
38
39
40
41
42
43
|
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const rootElement = document.getElementById("root");
const themeContext = React.createContext(null); // 初始化上下文,即全局变量
function App() {
const [theme, setTheme] = React.useState("red");
return (
<themeContext.Provider value={{ theme, setTheme }}>
<div className={`App ${theme}`}>
<p>{theme}</p>
<div>
<ChildA />
</div>
<div>
<ChildB />
</div>
</div>
</themeContext.Provider>
);
}
function ChildA() {
const { setTheme } = React.useContext(themeContext);
return (
<div>
<button onClick={() => setTheme("red")}>red</button>
</div>
);
}
function ChildB() {
const { setTheme } = React.useContext(themeContext);
return (
<div>
<button onClick={() => setTheme("blue")}>blue</button>
</div>
);
}
ReactDOM.render(<App />, rootElement);
|
- 初始化上下文,即全局变量
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
参考文章 ⇧
亮点
- 分析 useState 原理和源码
- useRef 的作用
- useContext 的作用
相关文章 ⇧
- 作者: Joel
- 文章链接:
- 版权声明
- 非自由转载-非商用-非衍生-保持署名