下面的空引用你在 react 项目里有用嘛?

像这样:

const userInfo = useAppSelector(state => state.app.userInfo || {} /*这个就是空引用*/)
复制代码

其中 useAppSelector 就是 react-redux 中的 useSelectoruseAppDispatch 同理。

不知道你在开发 react 的时候会不会仔细检查你的渲染,也就是理论渲染次数和实际渲染次数,准确的说不是渲染而是函数组件执行的理论次数和实际执行次数之间的差别。

让我们先看看最简单的例子,来说明我这里描述的理论执行次数和实际执行次数。

const UserInfo = () => {
  const userInfo = useAppSelector(state => state.app.userInfo || {})
  const dispatch = useAppDispatch()

  useEffect(() => {
    dispatch(updateUserInfo(userInfo.phone))
  }, [userInfo.phone])
  console.log("函数重新执行了")
  return <h1>{userInfo.name}</h1>
}
复制代码

像这样的函数组件,理论执行次数是多少次呢?

理论执行次数是两次,分别是初始化一次,当拿到最新数据后执行一次,上面这个很简单,的确也只是执行了两次,理论和实际相同,完美的代码。但是我们可能有这样的需求,也就是会对名字做处理,经过处理以后再显示,假设这个处理的逻辑很复杂。改造后代码为:

const UserInfo = () => {
  const userInfo = useAppSelector(state => state.app.userInfo || {})
  const dispatch = useAppDispatch()
  const [nameArr, setNameArr] = useState<string[]>([])

  // 1
  useEffect(() => {
    dispatch(updateUserInfo(userInfo.phone))
  }, [userInfo.phone])
  
  // 2
  useEffect(() => {
    setNameArr([userInfo.name])
  }, [userInfo])
  console.log("函数重新执行了")
  return <h1>{nameArr}</h1>
}
复制代码

你们觉得这个会执行多少次,假设刚开始的时候 userInfo 的值为 undefined ,答案是会发出警告:

51UserInfo.tsx:28 函数重新执行了
react-dom.development.js:67 Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
    at UserInfo (http://localhost:3000/static/js/bundle.js:402:78)
    at header
    at div
    at App
    at Provider (http://localhost:3000/static/js/bundle.js:39684:20)
复制代码

其中 51 代表执行了 51 次,当然实际执行次远多于这个数,在我们项目里面是一直都是执行,导致直接卡死。

通过这个警告我们知道了原因,也就是由于 userInfo 改变导致 setNameArr 执行,但是呢 setNameArr 执行又导致 userInfo 改变,所以出现了很多次的执行。下面我们看看理论执行次数:

  1. 组件初始化;
  2. 由于 useEffect 无论怎样都会执行,所以 2 处的 setNameArr([userInfo.name]) 会被执行,执行完成后由于 nameArr 改变,所以会渲染一次。
  3. dispatch(updateUserInfo(userInfo.phone)) 执行完成,导致 redux 里面的数据更新,也就是 userInfo 改变,所以会更新一次;
  4. userInfo 改变,那么 setNameArr([userInfo.name]) 就会执行,所以还需要再渲染一次。

也就是理论是四次,其实我们编写代码最想要的是两次次,应该就是初始化一次,拿到数据一次。稍后会讲到怎样做到我们最想要的两次,现在我们先做到不执行五十多次,不出现警告。

现在有一个问题出现,为啥执行 setNameArr 会导致 userInfo 改变,我们明明在代码里面没有操作 userInfo ,于是我想着让 setNameArr 直接返回空,或者返回字符串啥的,首先先看返回空数据,结果依旧,也就是即便返回空数组 setNameArr([]) 仍然存在问题,为啥?分析不出原因,于是我们尝试改变返回字符串,也就是 setNameArr('') 的方式,这个时候发现居然正常了,渲染了三次,疑惑,为啥是三次??

让我们看看现在的代码:

const UserInfo = () => {

  const userInfo = useAppSelector(state => state.app.userInfo )
  const dispatch = useAppDispatch()
  // const [nameArr, setNameArr] = useState<(string | undefined)[]>([])
  const [nameArr, setNameArr] = useState<string>('')

  useEffect(() => {
    console.log("dispatch(updateUserInfo(userInfo?.phone))")
    dispatch(updateUserInfo(userInfo?.phone))
  }, [userInfo?.phone])

  useEffect(() => {
    console.log("setNameArr([userInfo?.name])")
    // setNameArr([userInfo?.name])
    setNameArr('')
  }, [userInfo])

  // const nameArr = useMemo(() => {
  //   return [userInfo?.name]
  // }, [userInfo])

  console.log("函数重新执行了")

  return <h1>{nameArr}</h1>
}
复制代码
  1. 首先第一次初始化渲染;
  2. 然后两个 useEffect 执行,第一个 useEffect 执行后是等待结果返回,先不看,我们看第二个 setNameArr('') 的执行会导致 nameArr 改变,所以会重新渲染一次;
  3. 结果来了, userInfo 改变会重新渲染一次。

就是这三次,为啥上面的第四次没渲染,原因是虽然执行了 setNameArr('') ,但由于改变的值与之前的值相同,所以没有触发重新渲染。于是我们想到之前返回空数组为啥会重新渲染,因为空数组跟空数组不是同一个,那么就会重新渲染,可是即便如此,难道不是应该四次嘛,为啥会无数次。

这个时候我们就得知道 hooks 一些源码相关的知识了,hooks 靠的重新调用函数,这也是为啥会使用 useCallback 包括里面函数的原因,因为每一次执行函数,里面的所有变量都要重新初始化,所以我们在函数中定义的局部变量每一次渲染都是相同的值,因为函数执行完毕,函数中的局部变量就要被销毁,下一次执行的时候就会重新创建,之所以使用 useCallback 能做到不重新创建,使用 useCallback 会保存当前函数的引用,下一次只要发现第二个参数里面的值都没有改变就不重新创建函数,否则一直使用之前保存的函数。

useState 也是每一次都要跟上一次的 state 作比较,如果没有改变就不需要重新渲染,否则就会重新执行函数,而 useAppSelector 也是根据你返回的值跟上一次返回的值作比较,如果没有改变,那么就不需要更新,否则需要更新。

现在我们再看 useAppSelector 那行代码,你就会发现当 useInfo 的值是 undefined 时就会传递一个空对象,也就是说当没有拿到值之前,每一次执行, useAppSelector 取到的值都是 undefined ,也就是说每一次都会返回一个新的空对象,也就是说每一次 useInfo 的值都会改变,也就是每一次执行都会伴随着 setNameArr([userInfo?.name]) 的执行,而每一次 setNameArr([userInfo?.name]) 执行都会返回新数组,返回新数组,引用也就改变,导致函数会重新执行,函数重新执行 useAppSelector 取到的值又是新空对象,又导致 setNameArr([userInfo?.name]) 的执行,这样就一直执行下去,直到接口拿到数据,如果碰巧你的接口最终没有返回数据,那很遗憾你的程序会被卡死。

现在我们知道了原因,我们再回过头看,发现就像警告说的那样我们不应该采用这样的方式来进行渲染,因为这样的方式会存在这样的风险,还有就是 redux 中初始化的值尽量不要是 undefined

那不使用这样的方式还有其他方式嘛,答案是肯定的。我们可以把 userInfo.name 比喻成函数中的 x 变量,而我们最后要显示的那个值是 y ,其中官方提供的 useMemo 就相当于 y 。所以我们改造代码:

const UserInfo = () => {

  const userInfo = useAppSelector(state => state.app.userInfo )
  const dispatch = useAppDispatch()

  useEffect(() => {
    console.log("dispatch(updateUserInfo(userInfo?.phone))")
    dispatch(updateUserInfo(userInfo?.phone))
  }, [userInfo?.phone])


  const nameArr = useMemo(() => {
    return [userInfo?.name]
  }, [userInfo])

  console.log("函数重新执行了")

  return <h1>{nameArr}</h1>
}
复制代码

这样就完美了,因为这样的代码只会渲染两次,也就是最初说的两次,至于为啥是两次,你可以自己分析一下。

猜你喜欢

转载自juejin.im/post/7082694799031009311