关于 React useState 的魔法(bushi) | 青训营笔记

84 阅读2分钟

这是我参与「第四届青训营 」笔记创作活动的第 5 天

React 函数式组件里,如果在 return 之前 setState 会发生什么?

import React, { useState } from 'react'
import ReactDOM from 'react-dom'

const Component = () => {
  const [count, setCount] = useState(0)
  setCount(1)
  return <div>count:{count}</div>
}

ReactDOM.render(<Component />, document.getElementById("app"))

可以看到控制台鲜红的报错信息

image.png

怎会如此?我们来打个 log,为避免死循环,我们加个判断条件,让它最多执行 5 次

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'

const Component = () => {
  const [count, setCount] = useState(0)
  console.log('count:',count)

  if (count < 5) {
    setCount(count + 1)
  }
  
  // 上述 if 相当于一个没有任何 dep 的 useEffect
  // useEffect(() => {
  //   if (count < 5) {
  //     setCount(count + 1)
  //   }
  // })
  
  return <div>count:{count}</div>
}

ReactDOM.render(<Component />, document.getElementById("app"))

控制台也会打出:count:0count:5,也就是说 Component 组件 render 了 5 次

我们再来试一下类组件

import React, { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'

class Component extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0
    }
  }
  
  render() {
    console.log('count:',this.state.count)

    if (this.state.count < 5) {
      this.setState({ count: this.state.count + 1 })
    }
    return <div>count:{this.state.count}</div>
  }
}
ReactDOM.render(<Component />, document.getElementById("app"))

会得到同样的结果,还会在控制台给你一个“温馨提示”

image.png

由此我们可以看到,函数式组件相当于类组件的 render()

每次 state 或者 props 有更新都会重新 render,所以要避免在 render 里 setState。

函数式组件可以用 useEffect,类组件里也有类似的 componentDidUpdate(会在更新后会被立即调用。首次渲染不会执行此方法。)相比 componentDidUpdate 来说,useState 要灵活得多,它可以根据第二个参数的不同,让 useEffect 成为 componentDidMount(第二个参数为空数组)或者 componentDidUpdate(第二个参数不传,则每次 render 都会调用,若为 [{变量 a}] 则只有在变量 a 改变时才会执行 useEffect,具体用法可以戳文档)

那么问题来了

为什么 useState 可以保存 state 呢?确保每次 render 的时候 state 都不会被初始化。它是怎么做到的?

于是好奇宝宝找来了源码。

function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = nextHook();
  const state: S =
    hook !== null
      ? hook.memoizedState
      : typeof initialState === 'function'
      ? // $FlowFixMe: Flow doesn't like mixed types
        initialState()
      : initialState;
  hookLog.push({primitive: 'State', stackError: new Error(), value: state});
  return [state, (action: BasicStateAction<S>) => {}];
}
type Hook = {
  memoizedState: any,
  next: Hook | null,
};

let currentHook: null | Hook = null;
function nextHook(): null | Hook {
  const hook = currentHook;
  if (hook !== null) {
    currentHook = hook.next;
  }
  return hook;
}

看起来每次调用 useState 的时候,会先调用 nextHook() 获取到当前的存放所有 Hook 的 currentHook 链表,链表为空的时候将 initialState 添加到链表里,否则取链表里上次记录到的 memoizedState 值。

(看来基础不牢的切图仔不是好的切图仔,数据结构与算法无处不在 orz 这就滚去学)

参考:《5 种有趣的 useEffect 无限循环类型》 《阅读源码后,来讲讲React Hooks是怎么实现的》