React-Hooks 基础原理解析
# 0.前言
由于在工作中并没有使用过Hooks,但考虑目前使用函数式编程已是大势所趋。
所以本篇博客就来好好的梳理这部分的知识点,属于边学边写,尽量将每个 Hooks 钩子函数的使用特性给梳理清楚。
# 1.useState 原理解析
# 使用说明
const [n, setN] = React.useState(0) // 简单数据类型
使用 setXXX 方法更新变量时,有几个特别注意的点,新生一般容易犯的错误:
当对
复杂数据类型修改状态时,需始终保持数据的immutable特性。const [user, setUser] = React.useState({name: 'Jack', age: 18}) const onClick = () =>{ setUser({ ...user, // name: 'Frank' //这里的name覆盖之前的name }) }1
2
3
4
5
6
7否则,在
obj引用地址不发生变化的话,React就认为数据没有发生改变。⭐️在使用
set函数对变量进行更新时,更推荐使用函数写法,如下:const [n, setN] = React.useState(0) // 简单数据类型 const onClick = ()=>{ setN(n+1); // 新手写法 setN(x => x + 1); // 推荐写法 }1
2
3
4
5以上两者的区别是,
setN接受的是一个n+1的变量,而后者的写法setN接受的是一个表达式,其中x始终能获取到最新的n。更推荐表达式的原因:
- 避免可能出现的
stale closure(陈旧闭包)问题。 - 懒计算,如
setN(()=>( 1+2+3+4+5 )),只有执行时才会去进行运算。
- 避免可能出现的
# useState原理分析
在刚接触Hooks 组件的时候一直有个疑问,在真实的项目中,往往需要使用多个 useState 时,见伪代码如下:
function App(){
const [n1, setN1] = React.useState(0)
const [n2, setN2] = React.useState(0)
console.log(n1);
console.log(n2);
const onClick1 = ()=>{
setN1(x=>x+1);
}
const onClick2 = ()=>{
setN2(x=>x+1);
}
return (<div>.....</div>)
}
2
3
4
5
6
7
8
9
10
11
12
13
以上,是 useState 的基本操作,但是在使用的时候一定会问两个问题:
- 在多次调用
React.useState是如何对应具体的变量的,为何不冲突呢? - 将变量
n1打印后,n1的变量每次都会发生改变,而最近的n1数值保存在什么地方呢?
下面这段代码是模拟实现setState,非实际 React 源码:
let _state = [] ;// 使用 数组 顺序地存储变量的最新值,如[n1,n2]
let index = 0;
const myUseState = initialValue => {
_state[index] = _state[index] === undefined ? initialValue : _state[index];
const setState = newValue =>{
render(); // 触发 render 函数
}
index += 1; // 更新 数组 索引
return [_state[index], setState]
}
// 封装渲染
conset render = ()=>{
let index = 0;
ReactDom.render(<App/> ,rootElement);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在上述代码中,核心思路是:使用 _state 数组 + index 的形式,"顺序" 缓存了变量,可通过 index 找到变量的位置并对其进行修改。
而这一切都源于数组的有序性,这也解释了,为何 useState 不允许卸载if 语句中,因为这样会破坏useState 的调用顺序。
需要说明的是:
在
React源码中,在使用setN时,实际上是生成一个新的组件,在这个组件对应的vNode中挂载着上述代码中的_state数组以及index,而非全局作用域。通过DOM diff算法最终完成视图层的更新。组件的更新过程如下图所示:

由上图可知,同一个时间一个变量会存在新旧之分,在两个
<App/>组件中,同时挂载这新旧两个_state以及index。
注:在源码中,
React节点应该是FiberNode,_state的真实变量名为memoizedState,index的实现则使用到的是链表结构。
# 使用 useRef 保证一份数据地址
在 React 的设计思路中最最核心的一点保持数据的immutable 特性,因此在设计 useState 这个api 时在值的保存以及更新方面,始终会创建一个 New State ,即上述新旧两个_state 挂载到虚拟节点上。
但若要保存数据独一份,并且做到视图的更新。在 Vue 中采用的ref 引用的方式实现的。而这边我们也可以通过 React.useRef + 构造 update 函数模拟出相同的效果。
function App(){
const nRef = React.useRef(0);
const [,update] = React.useState(null); // 仅是为了实现更新
return(
<div className="App">
<button onClick={()=>{
nRef.current +=1; // Ref 的改变无法导致视图层的更新
update(nRef.current); // 当 nRef 变化时,则手动触发视图更新,
}}>
</div>
)
}
ReactDOM.render(<App/>, rootElement);
2
3
4
5
6
7
8
9
10
11
12
13
除了上述的做法外,我们也可以采用 useContext 以及 redux 中的store 达到相同的效果,这两者在维护数据地址的作用上基本相同,类似于维护了一个局部的全局作用域 ,由于篇幅原因就不再赘述了。
# 2. 核心 Hooks 整理
# 1. useState
上述已经对 useState 这个 api 做了比较深入的分析,这里就总结核心结论:
useState使用时建议推荐接受函数的形式,避免陷入stale closure。const [n,setN] = React.useState(0) setN(x=>x+1); // 函数形式1
2在进行状态更新时,始终返回
newValue,对于复杂数据类型可以使用扩展运算符的形式,也可以使用各类api库,如facebook的Immutable.js库,或者immer.js库。
# 2.useReducer
该函数主要的特性如下:
迷你版的
redux状态库。此
api在Hooks函数中支持较晚,主要是仿造redux进行的状态管理库,简化了redux的功能以及api的使用,相当于小型的redux库。增强版的
useStateuseState在状态管理方面,细粒度太细。如表单提交案例中,同一表单类型的数据完全可以维护一个复杂数据对象即可。不推荐使用,使用自定义
Hooks可以达到更好的状态拆分形态,即使useReduer对redux再简化,使用起来还是略微显得有些重。
# 3.useContext
核心功能:创造一个局部的全局变量(上下文)。
使用方法:
- 使用
C = createContext(initial)。 - 使用
<C.provider>圈定作用域。 - 使用时直接通过
useContext(C)来获取"全局变量"。
在使用
useContext时,注意对函数引用地址的缓存,有时间这里补一个案例。
# 4.useEffect | useLayoutEffect
这个钩子函数的名字起的不好,此钩子并非只能将 副作用 操作放在此钩子中,实际上称为 afterRender 更好,因为每次在 render 后运行,可以用于替代以下生命周期函数:
componentDidMount: 初始componentUpdate: 更新componentWillUnmount:卸载
这里第二个钩子useLayoutEffect ,需要对React中的渲染机制有一定了解,主要由以下链条:
render 转化为vNode Dom diff 操作 经过DOM 树解析等操作 useLayEffect() 将 DOM 转化为屏幕上真实的pixels 像素点( render 结束) useEffect()
主要有以下特点:
useLayoutEffect总是比useEffect先执行。- 推荐使用
useEffect优先渲染,原因是为了用户体验,即先让页面加载出来再说。 - 什么使用用?当
useEffect中存在DOM操作时。 - 什么时候不用? 据说
SSR中不存在useLayoutEffect()钩子函数,若需要做到同构效果的话,不推荐使用。
# 5.Memo | useMemo | useCallback
这三个函数均是优化React 重复渲染的问题的
Memoconst App = React.memo((props)=>{ ... })1等价于
Class extends PureComponent,一层浅比较。useMemo: 缓存引用地址。useCallback:属于useMemo的语法糖,当使用useMemo缓存一个function时useMemo(()=>{ return (x)=>{ console.log(x) } },[m])1
2
3
4
5如果使用
callback时,则为:useCallback((x)=>{console.log(x)},[m])1
# 6. useRef
该 api的使用目的:当希望维护一个引用地址保持不变的变量时,可以使用useRef()
使用方法:
const count = useRef(0);
// 从 current 上读取数值
count.current +=1;
2
3
注意此时,count.value 变化时,在React 中是不会触发视图更新的,此时我们需要构造一个update 函数,这一点在前文已经叙述过了。
在
Vue3中同样存在ref,不同之处在于Vue3会自动触发render操作。
# 7.forwardRef
在Class 组件中有两个特殊的关键字是不能被占用的,即 key 和 ref,而在 Hooks 中,是不存在 ref 属性的,如果我们希望使用 ref 字段的话,希望在原有组件上包一层 forwardRef。
function App(){
const buttonRef = useRef(null); // 最好还是要传下 null
return(
<div>
<Button ref={buttonRef}>按钮</Button>
</div>
)
}
// 可以从 forwarRef 包裹的组件中获取 props 以及 ref 属性
const Button = React.forwardRef((props,ref)=>{
return <button ref={ref} {...props} />
})
const rootElement = document.getElementById("root");
ReactDOM.render(<App/>, rootElement)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16