React Hooks 进阶
SunShinewyf opened this issue · 2 comments
React Hooks 进阶
前言
上一篇简单地介绍了一下 React Hooks 的背景和 API 的使用,这一篇深入探索一下 React Hooks 的实践和原理。
React Hooks 实践
模拟 Class Component 的生命周期
有的时候还是需要根据不同的生命周期来处理一些逻辑,React Hooks 几乎可以模拟出全部的生命周期。
componentDidMount
使用 useEffect 来实现,如下:
useEffect(()=> {
//ComponentDidMount do something
},[]);
useEffect 第二个参数传空数组时,表示只会在执行一次。
componentWillUnMount
同样可以使用 useEffect 来实现,如下:
useEffect(()=> {
return ()=> {
// ComponentWillUnMount do something
}
},[])
componentDidUpdate
componentDidUpdate 生命周期在组件每次更新之后执行,除了初始化 render 的时候不执行,所以可以设置一个标志位来判断是否是第一次 render,使用 useEffect + useRef 配合就可以实现:
const firstRenderRef = useRef(true)
useEffect(()=>{
if(firstRenderRef.current){
// 如果是第一次 render,就设置为 false
firstRenderRef.current = false;
} else {
// componentDidUpdate do something
}
})
getDerivedStateFromProps
getDeriverdStateFromProps 是 react 新版本中用来替代 componentWillReceiveProps,它可以感知 props 的变化,从而更新组件内部的 state,用 hooks 模拟这个生命周期,可以这样实现:
function Child(props){
const [count,setCount] = useState(0);
if(props.count !== count){
setCount(props.count);
}
}
shouldComponentUpdate
React 16.6 引入 React.memo,是用来控制 Function Component 的重新渲染的,类似于 Class Component 的 PureComponent,可以跳过 props 没有变化时的更新,为了支持更加灵活的 props 对比,它还提供了第二个函数参数 areEqual(prevProps, nextProps),和 shouldComponentUpdate 相反的是,当该函数返回 true 时表示不更新函数,返回 false 则重新更新,用法如下:
function Child(props){
return <h2>{props.count}</h2>
}
// 模拟shouldComponentUpdate
const areEqual = (prevProps, nextProps) => {
//比较
};
const PureChild = React.memo(Child, areEqual)
除了上面这种方法可以模拟 shouldComponentUpdate 之外,React Hooks 还提供一个 useMemo 用来控制子组件重新渲染的,举一个例子如下:
// Parent 组件
function Parent() {
const [count,setCount] = useState(0);
const child = useMemo(()=> <Child count={count} />, [count]);
return <>{count}</>
}
// Child 组件
function Child(props) {
return <div>Count:{props.count}</div>
}
在上面的例子中,只有 Parent 组件中的 count state 更新了,Child 才会重新渲染,否则不会。
React Hooks 原理
还记得我们之前讲过的使用 React Hooks 的两条规则吗?
- 只在 React 函数和 自定义 Hooks 中使用,不要在普通 js 中使用 Hooks
- 只在顶层使用 Hook,不在循环、条件或者嵌套函数中调用 Hook
只能在 React 函数和自定义 Hooks 中使用
翻到 ReactHooks 对应的源码,贴出 Hooks 的定义如下:
// useState
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
// useEffect
export function useEffect(
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
) {
const dispatcher = resolveDispatcher();
return dispatcher.useEffect(create, inputs);
}
// useRef
export function useRef<T>(initialValue: T): {current: T} {
const dispatcher = resolveDispatcher();
return dispatcher.useRef(initialValue);
}
...
//其他的都类似
所有的 Hooks 基本都调用了这个 resolveDispatcher(),定位到 resolveDispatcher,代码如下:
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
invariant(
dispatcher !== null,
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
);
return dispatcher;
}
如果 ReactCurrentDispatcher.current 是空的,就会得出我们使用 Hooks 的方式不对,只有在 React 环境中才会给 ReactCurrentDispatcher 的 current 赋值,所以就可以解这个问题。
不在循环、条件或者嵌套函数中调用 Hook
为什么不能在循环、条件或者嵌套函数中调用 Hook,我们还是从源码出发寻找原因:
Hooks 的实现源码在 ReactFiberHooks.js。
在这个文件中,定义了 firstWorkInProgressHook 和 workInProgressHook 这两个全局变量,观察所有的 Hooks 实现,发现都执行了 const hook = mountWorkInProgressHook(),首先来看一下这个函数的实现:
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
firstWorkInProgressHook = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
我们来模拟一下定义多个 Hooks 时的流程:
- 初始时,组件还未渲染时,firstWorkInProgressHook = workInProgressHook = null;
- 初次渲染
- 定义第一个 Hook 时:firstWorkInProgressHook = workInProgressHook = hook;
- 定义第二个 Hook 时:workInProgressHook = workInProgressHook.next = hook;
- 定义第三个 Hook 时:workInProgressHook = workInProgressHook.next = hook;
这种结构就是一个链表结构,而每一个 Hook 的结构如下:
type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,
};
type Effect = {
tag: HookEffectTag,
create: () => (() => void) | void,
destroy: (() => void) | void,
deps: Array<mixed> | null,
next: Effect,
};
其中 memoizedState 存储当前 Hook 的结果,next 则连接到下一个 Hook,从而将所有 Hook 进行串联起来。这个链表结果存储在 Fiber 对象的 memoizedState 属性中,在 React 中,每个节点都对应一个 Fiber 对象,而 Fiber 的 memoizedState 用来存储该节点在上次渲染中的 state,这个属性是 Class Component 用来存储节点的 state 的,这也就是为什么 Hook 可以拥有 Class Component 功能的原因。
链表结构用图形显示如下:
在第二次渲染时,也就是 update 的时候,此时调用的是 Hook 对应的 update 方法,而 update 方法又分别执行了 updateWorkInProgressHook(),先来看看这个方法的实现:
function updateWorkInProgressHook(): Hook {
if (nextWorkInProgressHook !== null) {
// There's already a work-in-progress. Reuse it.
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
nextCurrentHook = currentHook !== null ? currentHook.next : null;
} else {
// Clone from the current hook.
invariant(
nextCurrentHook !== null,
'Rendered more hooks than during the previous render.',
);
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
queue: currentHook.queue,
baseUpdate: currentHook.baseUpdate,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list.
workInProgressHook = firstWorkInProgressHook = newHook;
} else {
// Append to the end of the list.
workInProgressHook = workInProgressHook.next = newHook;
}
nextCurrentHook = currentHook.next;
}
return workInProgressHook;
}
在这个方法中,它会获取渲染时生成的 Hooks,并获取当前 update 的是处于链表的哪个节点,然后返回。
假如在条件语句中使用 Hook,如下:
let condition = true;
const [state1,setState1] = useState(0);
if(condition){
const [state2,setState2] = useState(1);
condition = false;
}
const [state3,setState3] = useState(2);
初始渲染时,拿到的是 state1 => hook1,state2 => hook2,state3 => hook3,再次渲染时,condition 条件不满足,那么执行 state3 时拿到的就是 hook2,那整个逻辑就乱套了...
结语
React Hooks 解决了一部分问题,但同时自身也有一定的缺陷,比如要遵守一定规则、组件嵌套层次不明显导致 bug 定位难。所以在实际的开发实践中,还是要评估再选型。
👍
我宣布 w女士就是我女神了