2021SC@SDUSC
众所周知,React的组件创建方式,一种是类组件,一种是函数组件,一般来说,开发人员都喜欢使用类组件,因为功能全啊,但是React团队希望,组件不要变成复杂的容器,最好只是数据流的管道。开发者根据需要,组合管道即可,也就是说组件的最佳写法应该是函数,而不是类。但是,我们知道函数组件缺少很多类组件所拥有的,ex:state,为了鼓励大家尽量使用函数组件,Hooks应运而生:组件尽量写成纯函数,如果需要外部功能和副作用,就用Hooks把外部代码"钩"进来
这里只展示四种最常用的:
import React, {useState} from 'react'
const AddNumber = () => {
const [ num, setNum ] = useState(0)
const addNum = () => {
let unum = num
setNum(unum+=1)
}
return (
<>
{num}
>
)
}
在useState()中,它接受状态的初始值作为参数,它返回一个数组,其中数组第一项为一个变量,指向状态的当前值。类似this.state,第二项是一个函数,用来更新状态,类似setState。
注意:class this.setState更新是state是合并, useState中setState是替换。
组件之间共享状态
import React,{ useContext } from 'react'
const Test = () => {
const TestContext = React.createContext({})
const TestA =() => {
const { name } = useContext(TestContext)
return (
名字:{name}
)
}
const TestB =() => {
const { name } = useContext(TestContext)
return (
名字:{name}
)
}
return (
)
}
在使用React的过程中,如遇到状态管理,我们一般会用到Redux,而React本身是不提供状态管理的。而useReducer()为我们提供了状态管理。
const [state, dispatch] = useReducer(reducer, initialState);
reducer就是一个只能通过action将state从一个过程转换成另一个过程的纯函数;useReducer就是一种通过(state,action) => newState的过程,和redux工作方式一样。数据流: dispatch(action) ——> reducer更新state ——> 返回更新后的state
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}
>
);
}
useEffect()接受两个参数,第一个参数是你要进行的异步操作,第二个参数是一个数组,用来给出Effect的依赖项。
useEffect(() => {},[array])
只要这个数组发生变化,useEffect()就会执行。当第二项省略时,useEffect()会在每次组件渲染时执行。这一点类似于类组件的componentDidMount。下面我们通过代码模拟一个异步加载数据。
function Demo3() {
const [data, setData] = useState();
useEffect(() => {
console.log("useEffect—[]”);
fetch(“xxx网站”)
.then(res => res.json())
.then(res => {
setData(res);
});
}, []);
useEffect(() => {
console.log("useEffect ---> 无依赖");
});
useEffect(() => {
console.log(“useEffect 依赖data: data发生了变化”);
}, [data]);
return (
data: {JSON.stringify(data)}
);
}
export default Demo3;
effect在render后按照前后顺序执行。
effect在没有任何依赖的情况下,render后每次都按照顺序执行。
effect内部执行是异步的。
依赖[]可以实现类似componentDidMount的作用,但最好忘记生命周期, 只记副作用。
当我们使用hooks时是从react.js中引入那些方法
以useState为例:
import { useState } from 'react'
而react.js又是从ReactHooks.js中引入,代码如下:
export function useState(
initialState: (() => S) | S,
): [S, Dispatch>] {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
这个resolveDispatcher()又是:
function resolveDispatcher() {
const dispatcher = ReactCurrentDispatcher.current;
但是当我们去找到ReactCurrentDispatcher,却发现它的current此时为null
const ReactCurrentDispatcher = {
/**
* @internal
* @type {ReactComponent}
*/
current: (null: null | Dispatcher),
};
而对于Dispatcher,其实只是定义了一些派发者方法类型,实际项目开发过程中,可以稍微参照一下这边的泛型定义:
export type Dispatcher = {|
getCacheForType?: (resourceType: () => T) => T,
readContext(context: ReactContext): T,
useState(initialState: (() => S) | S): [S, Dispatch>],
useReducer(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch],
useContext(context: ReactContext): T,
useRef(initialValue: T): {|current: T|},
useEffect(
create: () => (() => void) | void,
deps: Array | void | null,
): void,
useInsertionEffect(
create: () => (() => void) | void,
deps: Array | void | null,
): void,
useLayoutEffect(
create: () => (() => void) | void,
deps: Array | void | null,
): void,
useCallback(callback: T, deps: Array | void | null): T,
useMemo(nextCreate: () => T, deps: Array | void | null): T,
useImperativeHandle(
ref: {|current: T | null|} | ((inst: T | null) => mixed) | null | void,
create: () => T,
deps: Array | void | null,
): void,
useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void,
useDeferredValue(value: T): T,
useTransition(): [boolean, (() => void) => void],
useMutableSource
但是到此为止,我们无法从current里面找到任何东西,所以我们需要将目光转向ReactFiberBeginWork.js,从函数组件执行开始找,首先我们看到两种情况,分别是初始化和更新:
value = renderWithHooks(
null,
workInProgress,
Component,
props,
context,
renderLanes,
);
nextChildren = renderWithHooks(
current,
workInProgress,
render,
nextProps,
ref,
renderLanes,
);
其中就有我们希望的current,其他的参数:workInProgress是 workInProgress Fiber,Component是组件本身,props就是我们用来传值的this.props,context是上下文,其余不在赘述,接下来我们看renderWithHooks(这里为了提高观感,我删去了部分开发情况的条件处理):
export function renderWithHooks(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes,
): any {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
let children = Component(props, secondArg);
if (didScheduleRenderPhaseUpdateDuringThisPass) {
let numberOfReRenders: number = 0;
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
invariant(
numberOfReRenders < RE_RENDER_LIMIT,
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
numberOfReRenders += 1;
currentHook = null;
workInProgressHook = null;
workInProgress.updateQueue = null;
ReactCurrentDispatcher.current = __DEV__
? HooksDispatcherOnRerenderInDEV
: HooksDispatcherOnRerender;
children = Component(props, secondArg);
} while (didScheduleRenderPhaseUpdateDuringThisPass);
}
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
const didRenderTooFewHooks =
currentHook !== null && currentHook.next !== null;
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
didScheduleRenderPhaseUpdate = false;
invariant(
!didRenderTooFewHooks,
'Rendered fewer hooks than expected. This may be caused by an accidental ' +
'early return statement.',
);
return children;
}
该函数:首先先置空即将调和渲染的workInProgress树的memoizedState和updateQueue,为什么这么做,因为在接下来的函数组件执行过程中,要把新的hooks信息挂载到这两个属性上,然后在组件commit阶段,将workInProgress树替换成current树,替换真实的DOM元素节点。并在current树保存hooks信息。
然后根据当前函数组件是否是第一次渲染,赋予ReactCurrentDispatcher.current不同的hooks(HooksDispatcherOnMount or HooksDispatcherOnUpdate)。对于第一次渲染组件,那么用的是HooksDispatcherOnMount hooks对象。 对于渲染后,需要更新的函数组件,则是HooksDispatcherOnUpdate对象,那么两个不同就是通过current树上是否memoizedState(hook信息)来判断的,当然如果current不存在(null),证明是第一次渲染函数组件。
接下来let children = Component(props, secondArg);开始真正的执行函数组件,当然这里也有一个很妙的地方:ReactCurrentDispatcher.current = ContextOnlyDispatcher;就是说我们没有在函数组件内部调用的hooks,都是ContextOnlyDispatcher对象上hooks,并且里面的大部分都会调用throwInvalidHookError()来发出警告:
export const ContextOnlyDispatcher: Dispatcher = {
readContext,
useCallback: throwInvalidHookError,
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useInsertionEffect: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
useRef: throwInvalidHookError,
useState: throwInvalidHookError,
useDebugValue: throwInvalidHookError,
useDeferredValue: throwInvalidHookError,
useTransition: throwInvalidHookError,
useMutableSource: throwInvalidHookError,
useSyncExternalStore: throwInvalidHookError,
useOpaqueIdentifier: throwInvalidHookError,
unstable_isNewReconciler: enableNewReconciler,
};
function throwInvalidHookError() {
invariant(
false,
'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://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);
}
react-hooks就是通过这种函数组件执行赋值不同的hooks对象方式,判断在hooks执行是否在函数组件内部,捕获并抛出异常的。
这里借用掘金社区的一个图来直观的了解该过程,但是来源我也不太清楚,网上找的,如果作者看见,请联系我。
接下来让我们看看HooksDispatcherOnMount 和HooksDispatcherOnUpdate:
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useOpaqueIdentifier: mountOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useOpaqueIdentifier: updateOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
};
很明显可以看出来,这里使用了两套不同的方法,这两套方法的联系以及源码的分析,我会放在下一篇文章去讲。