React Hooks 基础、实现、原理
- 题外话
- 为什么要有Hooks?
- 但是Class Component 的用法也有缺陷:
- 1.组件复用变的困难
- 2.JavaScript本身的缺陷
- 函数式
- React Hooks
- useState
- useEffect
- useCallback、useMemo
- useReducer
- 最后
题外话
2023了,新年快乐!转眼间就已经工作一年左右了,这一年提升很多,想到很久没有整理知识点了所以…
为什么要有Hooks?
官方给出的解释是:复杂组件变得难以理解、组件之间复用状态逻辑很难
React是以组件为粒度编排应用的,组件是代码复用的最小单元。
在设计上,React采用props属性来接收外部的数据,使用state属性来管理组件自身产生的数据(状态),也就是说props 是传递给组件的(类似于函数的形参),而 state 是在组件内被组件自己管理的(类似于在一个函数内声明的变量)。而为了实现(运行时)对数据变更做出响应需要,React采用基于类(Class)的组件设计。除此之外,React认为组件是有生命周期的。
但是Class Component 的用法也有缺陷:
1.组件复用变的困难
HOC是React中用于重用组件逻辑的一种高级技术实现模式,它本身是一个函数,接受一个组件并返回一个新的组件。 而HOC容易产生嵌套地狱,每一次HOC调用都会产生一个组件实例。
2.JavaScript本身的缺陷
稍微不慎就会出现因this的指向报错。没有类似Java/C++多继承的概念,类的逻辑复用是个问题。
函数式
当然React不只是只有类式写法,还有函数式,但是早期的函数式组件只是单纯地接收props、绑定事件、返回jsx,本身是无状态的组件,依赖props传入的handle来响应数据(状态)的变更,所以函数式组件不能脱离Class Comnent来存在。
这时候就诞生了Hook,使得组件自身能够通过某种机制再触发状态的变更并且引起re-render
React Hooks
useState
useState可以管理组件自身定义的状态,返回一个 state,以及更新 state 的函数。
如下:setCount函数用于更新 count,它接收一个新的 state 值并将组件的一次重新渲染加入队列,并且引起组件re-render。在组件在初始渲染期间,返回的状态 (state) 与传入的第一个参数值相同也就是0。
并且count只能通过setCount来改变。
import React, { useState } from 'react';
function Add() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
dispatchAction函数是更新state的关键,它会生成一个update挂载到Hooks队列上面,并提交一个React更新调度,后续的工作和类组件一致。
理论上可以同时调用多次dispatch,但只有最后一次会生效(queue的last指针指向最后一次update的state)
useState更新数据是直接覆盖的。
// useState() 首次render时执行mountState
function mountState(initialState) {
// 从当前Fiber生成一个新的hook对象,将此hook挂载到Fiber的hook链尾,并返回这个hook
var hook = mountWorkInProgressHook();
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: (state, action) => isFn(state) ? action(state) : action,
lastRenderedState: initialState
};
// currentlyRenderingFiber$1保存当前正在渲染的Fiber节点
// 将返回的dispatch和调用hook的节点建立起了连接,同时在dispatch里边可以访问queue对象
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
功能相当于setState!
function dispatchAction(fiber, queue, action) {
...
var update = {
action, // 接受普通值,也可以是函数
next: null,
};
var last = queue.last;
if (last === null) {
update.next = update;
} else {
last.next = update;
}
// 略去计算update的state过程
queue.last = update;
...
// 触发React的更新调度,scheduleWork是schedule阶段的起点
scheduleWork(fiber, expirationTime);
}
useEffect
useEffect 可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,所以需要返回一个函数,有些不需要。
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
// Similar to componentDidMount and componentDidUpdate:
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。。
默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它 在只有某些值改变的时候才执行,也就是说可以给 useEffect 传递第二个参数,它是 effect 所依赖的值数组,当它改变时effect 才会执行(每次改变后渲染结束),如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。
useEffect(() => {
document.title = `You clicked ${count} times`;
},[]);
与useState传入的是具体state不同,useEffect传入的是一个callback函数,与useState最大的不同是执行时机,useEffect callback是在组件被渲染为真实DOM后执行(所以可以用于DOM操作)
useEffect调用也会在当前Fiber节点的Hooks链追加一个hook并返回,它的memoizedState存放一个effect对象,effect对象最终会被挂载到Fiber节点的updateQueue队列(当Fiber节点都渲染到页面上后,就会开始执行Fiber节点中的updateQueue中所保存的函数)
HooksDispatcherOnMountInDEV = {
useEffect: function() {
currentHookNameInDev = 'useEffect';
...
return mountEffectImpl(Update | Passive, UnmountPassive | MountPassive, create, deps);
},
};
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
return hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
function pushEffect(tag, create, destroy, deps) {
var effect = {
tag: tag,
create: create, // 存储useEffect传入的callback
destroy: destroy, // 存储useEffect传入的callback的返回函数,用于effect清理
deps: deps,
next: null
};
.....
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
....
return effect;
}
function renderWithHooks() {
....
currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
....
}
useCallback、useMemo
useCallback本质上是添加了一层依赖检查。它以另一种方式解决了问题 - 我们使函数本身只在需要的时候才改变,而不是去掉对函数的依赖。
如果count保持不变,getClickNum 也会保持不变,我们的effect也不会重新运行。但是如果count修改了,getClickNum 也会随之改变,因此会重新请求数据。
返回一个 memoized 回调函数。
const getClickNum = useCallback(() => {
return `You clicked ${count} times`;
}, [count]);
useMemo用于缓存一些耗时的计算结果,只有当依赖参数改变时才重新执行计算
返回一个 memoized 值。
const getNum = useMemo(() => getcount(count)
, [count]);
简单理解:useCallback(fn, deps) === useMemo(() => fn, deps)
useReducer
useReducer用于管理复杂的数据结构,基本实现了redux的核心功能。当你想更新一个状态,并且这个状态更新依赖于另一个状态的值时,就可以用useReducer。
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
最后
1.Hooks的调用尽量只在顶层作用域进行调用。
2.不要在循环、条件或者是嵌套函数中调用Hook,否则可能会无法确保每次组件渲染时都以相同的顺序调用Hook。
3.Hooks 的串联不是一个数组,是一个链式的数据结构,从根节点 workInProgressHook 向下通过 next 进行串联。
4.React Hooks目前只支持函数组件