React: Hooks原理解析
最简单的 useState 实现
useState 原理
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function App() {
const [n, setN] = React.useState(0);
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
你以为的n一开始是0,然后通过setN改变n,这是一般人看这个代码脑补的过程,但是,这个过程是错误的。
这里是实际的情况:
脑补之后
问自己几个问题
- 执行setN的时候会发生什么?n会变吗?App() 会重新执行吗?
要重新渲染UI,因为把n变了。n不会变,而是说我要把n变了,这个时候n是不变的,也就是说setN并不会改变n。App() 当然会重新执行
- 如果 App() 会重新执行,那么 useState(0) 的时候,n每次的值会有不同吗?
通过console.log你会发现,n每次的值会有不同 : 自己试试
分析
setN
- setN一定会修改数据x,将n+1存入x(而不是直接把他复制给n)
- setN一定会触发****重新渲染(re-render)
useState
- useState肯定会从x读取n的最新值
x
- 每个组件有自己的数据x,我们将其命名为state
尝试实现 React.useState
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
function myUseState(initialValue) {
var state = initialValue;
function setState(newState) {
state = newState;
render();
}
return [state, setState];
}
// 不用在意 render 的实现
const render = () => ReactDOM.render(<App />, rootElement);
function App() {
const [n, setN] = myUseState(0);
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
参考资料: React Hooks 原理
完全没有变化啊
- 因为myUseState会将state重置
- 我们需要一个不会被myUseState重置的变量
- 那么这个变量只要声明在myUseState外面即可
再尝试实现 React.useState
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
let _state;
function myUseState(initialValue) {
_state = _state === undefined ? initialValue : _state;
function setState(newState) {
_state = newState;
render();
}
return [_state, setState];
}
// 不用在意 render 的实现
const render = () => ReactDOM.render(<App />, rootElement);
function App() {
const [n, setN] = myUseState(0);
return (
<div className="App">
<p>{n}</p>
<p>
<button onClick={() => setN(n + 1)}>+1</button>
</p>
</div>
);
}
ReactDOM.render(<App />, rootElement);
如何让两个 useState 不冲突
Q: 看了上面的步骤, useState就这么简单?
A: 别急,还有问题
Q: 如果一个组件用了两个 useState怎么办?
A: 由于所有数据都放在**_state**,所以会冲突
改进思路
把 _state 做成一个对象
- 比如
_state = {n: 0, m: 0)}
- 不行,因为useState(0)并不知道变量叫n还是m
把 _state 做成数组
- 比如
_state = [0, 0]
- 貌似可行,我们来试试看
多个 useState
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
let _state = [];
let index = 0;
function myUseState(initialValue) {
const currentIndex = index;
index += 1;
_state[currentIndex] = _state[currentIndex] || initialValue;
const setState = (newState) => {
_state[currentIndex] = newState;
render();
};
return [_state[currentIndex], setState];
}
// 不用在意 render 的实现
const render = () => {
index = 0;
ReactDOM.render(<App />, rootElement);
};
function App() {
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(0);
console.log(_state);
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 , currentIndex 和 第 21 行
_state 数组方案缺点
useState 调用顺序
- 若第一次渲染时n是第一个,m是第二个,k是第三个
- 则第二次渲染时必须保证顺序完全一致
- 所以React不允许出现下面的代码
不允许出现的代码
现在的代码还有一个问题
- App 用了 _state 和 index ,那其他组件用什么?
解决办法: 给每个组件创建一个 _state 和 index
- 又有问题: 放在全局作用域里重名了咋整?
解决办法: 放在组件对应的虚拟节点对象上
图示
上图只画了 App 组件的更新过程,两个 ChildA 组件也有相同的过程
总结
- 每个函数组件对应一个 React 节点
- 每个节点保存着 state 和 index
- useState 会读取 state[index]
- index 由 useState 出现的顺序决定
- setState 会修改 state,并触发更新
这里对 React 的实现做了简化,React 节点应该是 FiberNode, _state 的真实名称为 memorizedState,index 的实现则用到了链表,有兴趣的可以看这里
useRef
n 的分身
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");
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>
);
}
ReactDOM.render(<App />, rootElement);
两种操作
- 点击 +1 再点击log
没有 bug
- 点击log再点击**+1**
有 bug
- 问题,为什么log出了旧数据?
setN不会改变n
因为有多个 n
贯穿始终的状态
Q: 看了这么多,如果我就希望有一个贯穿始终的状态,怎么做?
A: 办法很多
全局变量
- 用 window.xxx 即可,但太 low 了
useRef
useContext
- useContext 不仅能贯穿始终,还能贯穿不同组件
- useContext 例子
总结
- 每次重新渲染,组件函数就会执行
- 对应的所有 state 都会出现分身
- 如果你不希望出现分身, 可以用 useRef 或 useContext 等