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
库。增强版的
useState
useState
在状态管理方面,细粒度太细。如表单提交案例中,同一表单类型的数据完全可以维护一个复杂数据对象即可。不推荐使用,使用自定义
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
重复渲染的问题的
Memo
const 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