用类组件有一个很方便的生命周期就是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会记录下来。
简单的看一下源码逻辑。
// 运行函数组件后返回的childrenlet 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];};
这样好像就完美复现了。