有关Hook实现getDerivedStateFromProps的小思考

3,081 阅读3分钟

用类组件有一个很方便的生命周期就是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生命周期呢?

我的小想法

如果使用useStatesetStateprops的值赋值给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];
};

这样好像就完美复现了。

然后我还给封了个包