用类组件有一个很方便的生命周期就是getDerivedStateFromProps
,我用这个生命周期最主要的还是实现一些受控组件。
但是函数组件没有生命周期的概念,所以自然也没有这个方法了,但是细心的同学一定可以看到官方文档上是有解答过这个问题的。
在看官方解答之前,先了解一下类组件的getDerivedStateFromProps
生命周期是什么时候执行的。
getDerivedStateFromProps的执行时机
getDerivedStateFromProps
在源码里Mount时和Update时都会触发,并且执行时机是同步的,在源码里就是简单的值的修改,所以也不会发起新的更新。
const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
if (typeof getDerivedStateFromProps === 'function') {
applyDerivedStateFromProps(
workInProgress,
ctor,
getDerivedStateFromProps,
newProps,
);
// 在这里就直接赋值给state了
instance.state = workInProgress.memoizedState;
}
官方FAQ的解答
我想一些同学如果没有看过官方的解答,可能会像下面这样做。
const [state,setState] = useState()
useEffect(()=>{
if('value' in props){
setState(props.value)
}
},[props])
因为我在最初用Hook
的时候就这样写过,这样写都不是组件频繁发起更新调度的问题,而是useEffect
是异步的,可能会有一些小问题。
抛出错误的方法,看看官方的解答。
function ScrollView({row}) {
const [isScrollingDown, setIsScrollingDown] = useState(false);
const [prevRow, setPrevRow] = useState(null);
if (row !== prevRow) {
// Row 自上次渲染以来发生过改变。更新 isScrollingDown。
setIsScrollingDown(prevRow !== null && row > prevRow);
setPrevRow(row);
}
return `Scrolling down: ${isScrollingDown}`;
}
官方的解答就直接放在函数体中直接修改的值。
按照逻辑来讲和getDerivedStateFromProps
的执行时机是一样的,但是如果你参照这样的方式实现,并在函数体里console
一下,就会发现函数体中内部的setState
方法好像是触发了函数组件的重新渲染,因为会console
多个值。
原因就在于函数组件无论是要获取新的Hook
的值还是干什么的,每次都会重新执行该函数组件,如果是在函数体里执行的setState
,React会记录下来。
简单的看一下源码逻辑。
// 运行函数组件后返回的children
let children = Component(props);
// 在函数组件执行过程中发起了更新
if (didScheduleRenderPhaseUpdateDuringThisPass) {
children = Component(props);
}
源码里执行函数组件的过程中如果发起了更新调度,就会同步的再执行一次函数组件来获取新的children
值。
// 执行过程中setState会进入if判断
if (fiber === currentlyRenderingFiber) {
// 记录下来是执行过程中发起的更新
didScheduleRenderPhaseUpdateDuringThisPass = true;
} else {
// 发起更新调度
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
所以好像触发了重新渲染,实际上只是函数组件再执行了一次,这样有什么问题呢?
然而官方的解答肯定不会有问题…我想如果是一个超级重的,像以前看别人写的3000行函数组件这样的肯定会有一些小小的性能影响。
那么问题来了,怎么才能做到像真正的getDerivedStateFromProps
生命周期呢?
我的小想法
如果使用useState
,setState
将props
的值赋值给state
的时候必定会让函数组件重新执行,如果我们能手动控制函数组件是否刷新不就完事儿了。
所以可以使用useRef
来存state
的值,然后单独执行useState
来发起更新调度。
const useDerivedState = (props) => {
const rerender = useState()[1];
const stateRef = useRef();
if (props?.value) {
stateRef.current = props.value;
}
const setState = (value) => {
stateRef.current =
typeof value === "function" ? value(stateRef.current) : value;
rerender({});
};
return [stateRef.current, setState];
};
这样好像就完美复现了。