原理篇:hooks
Q: React 引入hooks的原因
A: 让函数组件可以做类组件的事,可以有自己的state,可以处理一些副作用,获取ref。
hooks 与 fiber (workInProgress)
hooks主要以三种形态存在于react中:
- HooksDispatcherOnMount:函数组件初始化,建立fiber与hooks之间的关系
- HooksDispatcherOnUpdate: 函数组件的更新,需要 hooks 去获取或者更新维护状态。
- ContextOnlyDispatcher: 防止在函数外部调用,直接报错
所有函数组件的触发是在 renderWithHooks 方法中:
let currentlyRenderingFiber
function renderWithHooks(current,workInProgress,Component,props){
// 初始化时把处理中的fiber赋值给currentlyRenderingFiber,每个hooks内部读取的就是currentlyRenderingFiber的内容。
currentlyRenderingFiber = workInProgress;
// memoizedState用来存放hooks列表
workInProgress.memoizedState = null;
workInProgress.updateQueue = null; /* 清空状态(用于存放effect list) */
// 如果是初始化阶段使用HooksDispatcherOnMount,更新阶段使用HooksDispatcherOnUpdate
// React 通过赋予 current 不同的 hooks,以此来监控 hooks 是否在函数组件内部调用
ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate
// 此时,函数组件在Component函数内部被真正执行,对应的hooks也会被依次执行
let children = Component(props, secondArg);
ReactCurrentDispatcher.current = ContextOnlyDispatcher; /* 将hooks变成第一种,防止hooks在函数组件外部调用,调用直接报错。 */
}
注意:函数组件触发时把处理中的fiber赋值给currentlyRenderingFiber,后面的源码会调用。
在组件初始化的时候,每一次hooks的执行,都会调用mountWorkInProgressHook。
function mountWorkInProgressHook() {
const hook = {
memoizedState: null, // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// 在renderWithHooks函数中已经将workInProgress赋值给currentlyRenderingFiber
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 每一个 hooks 通过 next 链表建立起关系
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
如下例子:
export default function Index(){
const [ number,setNumber ] = React.useState(0) // 第一个hooks
const [ num, setNum ] = React.useState(1) // 第二个hooks
const dom = React.useRef(null) // 第三个hooks
React.useEffect(()=>{ // 第四个hooks
console.log(dom.current)
},[])
return <div ref={dom} >
<div onClick={()=> setNumber(number + 1 ) } > { number } </div>
<div onClick={()=> setNum(num + 1) } > { num }</div>
</div>
}
hooks链表如下图:
Q:hooks为什么要放在函数顶部,不能写在条件判断语句中?
A:在更新阶段,会先复用一份hooks,形成新的hooks链表。如果放在if语句中,会造成链表对比不一致的情况。
useState
在react中,useState会mountState函数中初始化
function mountState(
initialState
){
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// 如果 useState 第一个参数为函数,执行函数得到state
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
// 保存更新信息
const queue = (hook.queue = {
...
});
// 负责更新的函数
const dispatch = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
)))
return [hook.memoizedState, dispatch];
}
useState通过dispatchAction 来触发state更新。
dispatchAction 定义如下:
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
)
const [ number , setNumber ] = useState(0)
dispatchAction 就是setNumber,dispatchAction前两个参数已被固定写死,我们传入的是第三个参数action。
下面分析dispatchAction内部实现:
function dispatchAction(fiber, queue, action){
/* 第一步:创建一个 update */
const update = { ... }
const pending = queue.pending;
// 第一次更新
if (pending === null) {
update.next = update;
} else { /* 再次更新 */
update.next = pending.next;
pending.next = update;
}
if( fiber === currentlyRenderingFiber ){
/* 说明当前fiber正在发生调和渲染更新,那么不需要更新 */
}else{
if(fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)){
const lastRenderedReducer = queue.lastRenderedReducer;
// 上一次的state
const currentState = queue.lastRenderedState;
// 本次需要更新的state
const eagerState = lastRenderedReducer(currentState, action);
// 如果两次更新state相同,则不更新
if (is(eagerState, currentState)) {
return
}
}
scheduleUpdateOnFiber(fiber, expirationTime); /* 发起调度更新 */
}
}
多次调用同一个setState/useState为何会合并处理?
useState 触发更新的本质是updateReducer,源码如下:
function updateReducer(){
// 第一步把待更新的pending队列取出来。合并到 baseQueue
const first = baseQueue.next;
let update = first;
// 当同一个useState在执行时,会继续给newState赋值,而不是向下执行
do {
newState = reducer(newState, action);
} while (update !== null && update !== first);
hook.memoizedState = newState;
return [hook.memoizedState, dispatch];
}
useEffect
当我们调用useEffect的时候,在组件第一次渲染的时候会调用mountEffect方法
function mountEffect(create,deps){
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// pushEffect创建一个 effect, 如果存在多个effect就会形成副作用链表
hook.memoizedState = pushEffect(
HookHasEffect | hookEffectTag,
create, // useEffect 第一次参数,就是副作用函数
undefined,
nextDeps, // useEffect 第二次参数,deps
)
}
对于函数组件,可能存在多个 useEffect / useLayoutEffect ,hooks 把这些 effect,独立形成链表结构,在 commit 阶段统一处理和执行。
更新流程就是判断两次deps是否相等:
function updateEffect(create,deps){
const hook = updateWorkInProgressHook();
// 如果deps没有变化,则更新effect list就可以了
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(hookEffectTag, create, destroy, nextDeps);
return;
}
// 如果deps依赖项发生改变,赋予 effectTag。在commit阶段会根据 effectTag 判断执行effect
currentlyRenderingFiber.effectTag |= fiberEffectTag
hook.memoizedState = pushEffect(HookHasEffect | hookEffectTag,create,destroy,nextDeps)
}
useRef
useRef 就是创建并维护一个 ref 原始对象。
function mountRef(initialValue) {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref; // 创建ref对象。
return ref;
}
更新:
function updateRef(initialValue){
const hook = updateWorkInProgressHook()
return hook.memoizedState // 取出复用ref对象。
}
useMemo
function mountMemo(nextCreate,deps){
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
function updateMemo(nextCreate,nextDeps){
const hook = updateWorkInProgressHook();
const prevState = hook.memoizedState;
const prevDeps = prevState[1]; // 之前保存的 deps 值
if (areHookInputsEqual(nextDeps, prevDeps)) { //判断两次 deps 值
return prevState[0];
}
const nextValue = nextCreate(); // 如果deps,发生改变,重新执行
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}