Is the following null reference useful in your react project?

like this:

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

Among them useAppSelectoris react-reduxin the useSelectorsame useAppDispatchway.

I don’t know if you will carefully check your rendering when developing react, that is, the theoretical rendering times and the actual rendering times. To be precise, it is not rendering but the difference between the theoretical and actual execution times of function component execution.

Let's start with the simplest example to illustrate the theoretical and actual execution times I've described here.

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>
}
复制代码

What is the theoretical number of executions for a functional component like this?

The number of theoretical executions is twice, one for initialization, and one for execution when the latest data is obtained. The above is very simple, and it is indeed only executed twice. The theory and practice are the same, perfect code. But we may have such a requirement, that is, to process the name and display it after processing, assuming that the logic of this processing is very complicated. The modified code is:

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>
}
复制代码

How many times do you think this will be executed, assuming userInfothe is undefined, the answer is a warning:

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)
复制代码

Among them, 51 means that it has been executed 51 times. Of course, the actual execution times are much more than this number. In our project, it is always executed, which leads to direct stuck.

Through this warning, we know the reason, that is, because the userInfochange causes setNameArrthe execution, but setNameArrthe execution causes userInfothe change, so there are many executions. Let's take a look at the theoretical execution times:

  1. component initialization;
  2. Since it will be executed useEffectno matter what, so the 2 places setNameArr([userInfo.name])will be executed, and after the execution is completed, it will be rendered once because of the nameArrchange .
  3. dispatch(updateUserInfo(userInfo.phone))The execution is completed, reduxcausing the data in it to be updated, that is, to userInfochange, so it will be updated once;
  4. userInfochange, then the setNameArr([userInfo.name])will be executed, so it needs to be rendered again.

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

现在有一个问题出现,为啥执行 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>
}
复制代码

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

Guess you like

Origin juejin.im/post/7082694799031009311