このような:
const userInfo = useAppSelector(state => state.app.userInfo || {} /*这个就是空引用*/)
复制代码
それらの中useAppSelector
にreact-redux
はuseSelector
同じuseAppDispatch
方法があります。
反応を開発するときにレンダリング、つまり理論上のレンダリング時間と実際のレンダリング時間を注意深くチェックするかどうかはわかりません。正確には、レンダリングではなく、関数コンポーネントの理論上の実行時間と実際の実行時間の違いです。実行。
ここで説明した理論上の実行時間と実際の実行時間を説明するために、最も簡単な例から始めましょう。
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>
}
复制代码
このような機能コンポーネントの理論上の実行回数はいくつですか?
理論上の実行回数は、初期化と最新データ取得後の実行の2回です。上記は非常に単純で、実際には2回しか実行されません。理論と実践は同じ、完璧なコードです。ただし、この処理のロジックが非常に複雑であると仮定すると、名前を処理して処理後に表示するという要件がある場合があります。変更されたコードは次のとおりです。
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
変更が発生するため、実行が多くなります。理論上の実行時間を見てみましょう。
- コンポーネントの初期化。
useEffect
何があっても実行されるので2箇所setNameArr([userInfo.name])
実行し、実行完了後はnameArr
変更。dispatch(updateUserInfo(userInfo.phone))
実行が完了するとredux
、その中のデータが更新されます。つまり、userInfo
変更されるため、1回更新されます。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>
}
复制代码
- 首先第一次初始化渲染;
- 然后两个
useEffect
执行,第一个useEffect
执行后是等待结果返回,先不看,我们看第二个setNameArr('')
的执行会导致nameArr
改变,所以会重新渲染一次; - 结果来了,
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>
}
复制代码
这样就完美了,因为这样的代码只会渲染两次,也就是最初说的两次,至于为啥是两次,你可以自己分析一下。