我们继续按照useState
的套路来看其他hook
。 主打一个快速高效
如果没有看过useState
的逻辑直接看这一篇的话可能会有点看不懂。 这一张主打砍菜, 作为过渡篇, 把简单常见的hook
先过一遍
一、useReducer
上一篇文章我们得知, 我们执行的updateState
本质上就是执行了updateReducer
的逻辑。 故我们先把useReducer
给搞定
useReducer
的使用频率相对于useState
的使用频率低很多,但是他的能力又要比useState
强。 可以说useState
是useReducer
的阉割版。
- 直接定位
mountReducer
。 可以看到这里的逻辑和useState
基本也一致的。但是就是lastRenderedReducer
的赋值上体现了差异。- 对于
useState
来说, 赋值的是定义好的basicStateReducer
。 该函数就是先判断了action
是函数还是值。 是值的话直接就返回了。 是函数的话就传入当前的state
。 然后返回该函数的返回值。 他的逻辑是固定的。 - 对于
useReducer
来说。 赋值的是传入的参数reducer
。updateReducer
在调用它的时候就传入当前的state
和action
。 具体的state
变更逻辑是我们可以自定义的, 所以会更灵活一些。
- 对于
所以他们的应用场景也所区别的。 useState
解决组件内状态更新的问题。 useReducer
解决组件复杂的状态更新问题
js
function mountReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = mountWorkInProgressHook();
let initialState;
// 这里是用来解决直接传入函数的话, 组件函数在多次调用的情况下。 函数参数也会被多次调用
// 故设置了可选参数Init用来解决该问题, 如果有设置的话此时就调用init(initialArg)
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = ((initialArg: any): S);
}
hook.memoizedState = hook.baseState = initialState;
const queue: UpdateQueue<S, A> = {
pending: null,
lanes: NoLanes,
dispatch: null,
// 这里就是和useState的区别
// useState的话这里的逻辑赋值的是定义好的
// useReducer的话这里赋值的是传入的参数reducer
lastRenderedReducer: reducer,
lastRenderedState: (initialState: any),
};
hook.queue = queue;
const dispatch: Dispatch<A> = (queue.dispatch = (dispatchReducerAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
核心区别讲了, 其他逻辑都差不多的,这里就不赘述了。 转战下一个
二、 useRef
- 直接看
mountRef
, 啊? 老铁四句话就给我打发了? 不过也是,mountRef
就是用来存数据的,也不提供函数变更, 也不会触发组件重新渲染, 那还需要啥自行车呀。 主要一个你给我啥, 我就给你存啥。 这里的逻辑就是将传入的东西包一层。 放在current
属性上 。然后将这个对象挂上hook
的memoizedState
。 完事
js
function mountRef<T>(initialValue: T): {|current: T|} {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
- 再看
updateRef
。就是拿到对应的hook
对象。 然后拿到memoizedState
上的值。这就是在mountRef
挂上的了。 其实useRef
本质就是利用了JS
的引用对象。 通过hook
链表保存了对应的对象引用, 所以我们怎么去修改current
都无所谓。React
拿到了引用就能够获取到current
的值。
js
function updateRef<T>(initialValue: T): {|current: T|} {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
三、 useMemo
- 直接看
mountMemo
,啊? 你小子也是几句话就给我打发了(看源码的日子是越来越好过了啊(bushi))。逻辑上就是把当前的value
和deps
给保存了
js
function mountMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps; // 这里对传入的deps做了一下转换
const nextValue = nextCreate(); // 执行传入函数拿到当前值
hook.memoizedState = [nextValue, nextDeps]; // 挂到hook上
return nextValue;
}
- 再看
updateMemo
。 其实和想的也一样, 就是比较deps
, 一样就直接返回保存的值。 不一样的话则再调用一次获取值
js
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
// 如果比较完一致的话,直接返回之前保存的值就OK了
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
// 走到这里就说明, deps发生了变化,故会再调用一次nextCreate获取值
const nextValue = nextCreate();
// 也更新在hook上保存的值
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
- 其中的
areHookInputsEqual
也可以看看。 还是对两个进行了遍历对比, 只要有一个不一样就返回false。 这里采用的还是is()
函数, 浅比较来的(可以说就是Object.is
的补丁包)
js
function areHookInputsEqual(
nextDeps: Array<mixed>,
prevDeps: Array<mixed> | null,
) {
if (prevDeps === null) {
return false;
}
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
四、useCallback
- 老规矩, 这里和
mountMemo
如出一辙啊。只不过这里没有调用,而是直接存起来
js
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
- 再看
updateCallback
, 这里也是采用了areHookInputsEqual
。 其他逻辑和updateMemo
的都很像, 不赘述了
js
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
五、useContext
这个hook
又区别于上面其他的hook
。 这个hook
还不能单纯使用, 需要配合createContext
一起。 不急, 我们,挨个解决。 先思考一下有哪些关键步骤需要了解
- 调用了
useContext
的逻辑, 具体是干了什么? 获取了哪里的值? 挂载阶段和更新阶段有什么区别吗? createContext
创建的context
结构是什么样的? 有哪些关键值?<SomeContext.Provider>
又做了什么? 怎么让传入的值和context
关联起来 ?
先看createContext
做了些什么吧。毕竟其他的工作都是围绕着他创建的context
对象进行展开的。 可以看到里面有三个重要的属性。 第一个是currrentValue
就是用来存放值的, 然后是Provider
和Consumer
一个作为提供者一个作为消费者都保存了context
对象。
再看调用useContext
会做什么吧,从 HooksDispatcherOnMount/ HooksDispatcherOnUpdate 可以看到。 无论在mount
阶段还是update
阶段调用它最终都是调用了readContext
。 所以我们先看readContext
。 这里一共做了两个事情
- 拿到
context
对象的_currentValue
, 返回 - 生成
contextItem
。挂载到当前Fiber
的dependencies
js
export function readContext<T>(context: ReactContext<T>): T {
// _currentValue和_currentValue2都有可能拿到值。 这里考虑适配吧
// 那我们是在哪里地方给他赋值的呢, 请看后面讲解
const value = isPrimaryRenderer
? context._currentValue
: context._currentValue2;
if (lastFullyObservedContext === context) {
// Nothing to do. We already observe everything in this context.
} else {
// 生成一个contextItem对象。 看到next就知道这又是一个链表
// next存放下一个contextItem对象
const contextItem = {
context: ((context: any): ReactContext<mixed>),
memoizedValue: value,
next: null,
};
// lastContextDependecy就是保持了context链表。
if (lastContextDependency === null) {
.....
lastContextDependency = contextItem;
// 这里把context对象挂上Fiber的dependencies
currentlyRenderingFiber.dependencies = {
lanes: NoLanes,
firstContext: contextItem,
};
if (enableLazyContextPropagation) {
currentlyRenderingFiber.flags |= NeedsPropagation;
}
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
}
// 返回拿到的值
return value;
}
- 这里为什么要挂上
Fiber
呢。 可以看下源码中以下两个用法。 也就是说你哪个组件用到context
了, 当context
变化的时候也该组件应该也变化。判断是否改变的就要依赖current Fiber
的存在
我们继续进<SomeContext.Provider>
看一下。 我们处理Fiber
的入口在beginwork
, 这里对不同Fiber
的tag
进行了Switch Case
的处理。 对于<SomeContext.Provider>
而言此时tag
为ContextProvider
。 故调用了updateContextProvider
, 其中又有一句关键的语句pushProvider(workInProgress, context, newValue);
,
我们直接看pushProvider
。可以看到这里就是一个赋值操作。 这也就解释了为什么我们在<SomeContext.Provider>
传值, 然后可以通过useContext
拿到。 其实就是通过context
对象的_currentValue
去对其进行保存
js
export function pushProvider<T>(
providerFiber: Fiber,
context: ReactContext<T>,
nextValue: T,
): void {
if (isPrimaryRenderer) {
push(valueCursor, context._currentValue, providerFiber);
context._currentValue = nextValue;
} else {
push(valueCursor, context._currentValue2, providerFiber);
context._currentValue2 = nextValue;
}
}
所以所有的hook
都会通过mountWorkInProgressHook
绑定到Fiber
的memoizedState
上吗? 答案肯定不是啦。 你看看useContext
就不是这样做。
六、useId
描述: useId
是一个 React Hook,可以生成传递给无障碍属性的唯一 ID。 具体使用可以参考 为了生成唯一id,React18专门引入了新Hook:useId。里面描述的也很清楚
- 我们先看
mountId
。 这里的主逻辑是分成了两步。 一个是跟服务端渲染相关的, 另一个则是正常的客户端渲染。这里的identifierPrefix
则是一开始调用createRoot
可以传入的options
属性。 为useId
的前缀- 先看客户端渲染, 就是维护了一个全局的递增的变量
globalClientIdCounter
。然后和前缀以固定的形态组成最后的id
- 再看涉及
hydrate
的, 上面的文章也讲述到了, 由于React Fizz
的到来, 渲染顺序可能不一致, 如果采用之前的常规方法可能会导致服务端渲染和客户端渲染拿到的ID
不一致. 但是对于服务端和客户端来说,Fiber
层级是一致的。这里是通过getTreeId()
的方法。 对于同一个组件内使用多个useId
的情况又使用了递增数字localIdCounter
进行处理。
- 先看客户端渲染, 就是维护了一个全局的递增的变量
拿到ID
之后再挂上hook
和返回即可
js
function mountId(): string {
const hook = mountWorkInProgressHook();
const root = ((getWorkInProgressRoot(): any): FiberRoot);
const identifierPrefix = root.identifierPrefix;
let id;
if (getIsHydrating()) {
const treeId = getTreeId();
id = ':' + identifierPrefix + 'R' + treeId;
// 在一个组件内多次调用了useId, 那么此时又需要保持一个递增的数字
const localId = localIdCounter++;
if (localId > 0) { // 第一个使用的是不会到这一步的, 后面使用的就会加上Hxx
id += 'H' + localId.toString(32);
}
id += ':';
} else {
const globalClientId = globalClientIdCounter++;
id = ':' + identifierPrefix + 'r' + globalClientId.toString(32) + ':';
}
hook.memoizedState = id;
return id;
}
updateId
的逻辑就很简单了, 只有初次调用useId的时候才会生成,后面调用的话都是直接将存起来的值取出来
js
function updateId(): string {
const hook = updateWorkInProgressHook();
const id: string = hook.memoizedState;
return id;
}
这一篇根据之前useState
涉及的逻辑把一些简单的hook
通关了, 下一篇讲解跟Effect
相关的hook