<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          React Hooks 使用誤區(qū),駁官方文檔

          共 17775字,需瀏覽 36分鐘

           ·

          2021-12-28 21:05

          大廠技術(shù)  高級前端  Node進階

          點擊上方 程序員成長指北,關(guān)注公眾號

          回復1,加入高級Node交流群

          作為 React Hooks 庫 ahooks[1]  的作者,我應該算一個非常非常資深的 React Hooks 用戶。在兩年多的 React Hooks 使用過程中,我越來越發(fā)現(xiàn)大家(包括我自己)對 React Hooks 的使用姿勢存在很大誤區(qū),歸根到底是官方文檔的教程很不嚴謹,存在錯誤的指引。

          1. 不是所有的依賴都必須放到依賴數(shù)組中

          對于所有的 React Hooks 用戶,都有一個共識:“useEffect 中使用到外部變量,都應該放到第二個數(shù)組參數(shù)中”,同時我們會安裝 eslint-plugin-react-hooks[2] 插件,來提醒自己是不是忘了某些變量。

          以上共識來自官方文檔:

          我愿稱該條規(guī)則為萬惡之源,這條規(guī)則以高亮展示,所有的新人都很重視,包括我自己。然而在實際的開發(fā)中,發(fā)現(xiàn)事情并不是這樣的。

          下面舉一個比較簡單的例子,要求如下:

          1. 有兩個字段 User 和 Email,都是可以隨時變化的
          2. 只有當 User 變化時,打印 User 和 Email 的值

          這個例子比較簡單,先貼下源碼:

          function App({
            const [email, setEmail] = useState('');
            const [user, setUser] = useState('Tom');

            useEffect(() => {
              console.log(user, email);
            }, [user]);

            return (
              <div style={{ padding: 64 }}>
                <label style={{ display: 'block' }}>
                  User:
                  <select value={user} onChange={(e) => setUser(e.target.value)}>
                    <option value="Tom">Tom</option>
                    <option value="Jack">Jack</option>
                  </select>
                </label>
                <label style={{ display: 'block', marginTop: 16 }}>
                  Email:
                  <input value={email} onChange={e => setEmail(e.target.value)} />
                </label>
              </div>

            );
          }

          我們能看到示例代碼中,useEffect 是不符合 React 官方建議的,email 變量沒有放到依賴數(shù)組中,ESLint 警告如下:


          那如果按照規(guī)范,我們把依賴項都放到第二個數(shù)組參數(shù)中,會怎樣呢?

          useEffect(() => {
            console.log(user, email);
          }, [user, email]);

          如上的代碼雖然符合了 React 官方的規(guī)范,但不滿足我們的業(yè)務需求了,當 email 變化時,也觸發(fā)了函數(shù)執(zhí)行。

          此時陷入了困境,當滿足 useEffect 使用規(guī)范時,業(yè)務需求就不能滿足了。當滿足業(yè)務需求時,useEffect 就不規(guī)范了。

          我的建議為:

          1. 不要使用 eslint-plugin-react-hooks 插件,或者可以選擇性忽略該插件的警告。
          2. 只有一種情況,需要把變量放到 deps 數(shù)組中,那就是當該變量變化時,需要觸發(fā) useEffect 函數(shù)執(zhí)行。而不是因為 useEffect 中用到了這個變量!

          2. deps 參數(shù)不能緩解閉包問題

          假如完全按第二個建議來寫代碼,很多人又擔心,會不會造成一些不必要的閉包問題?我的結(jié)論是:閉包問題和 useEffect 的 deps 參數(shù)沒有太大關(guān)系。

          比如我有一個這樣的需求:當進入頁面 3s 后,輸出當前最新的 count。代碼如下:

          function Demo({
            const [count, setCount] = useState(0);

            useEffect(() => {
              const timer = setTimeout(() => {
                console.log(count)
              }, 3000);
              return () => {
                clearTimeout(timer);
              }
            }, [])

            return (
              <button
                onClick={() =>
           setCount(c => c + 1)}
              >
                click
              </button>

            )
          }

          以上代碼,實現(xiàn)了初始化 3s 后,輸出 count。但很遺憾,這里肯定會出閉包問題,哪怕進來之后我們多次點擊了 button,輸出的 count 仍然為 0。

          那假如我們把 count 放到 deps 中,是不是就好了?

            useEffect(() => {
              const timer = setTimeout(() => {
                console.log(count)
              }, 3000);
              return () => {
                clearTimeout(timer);
              }
            }, [count])

          如上代碼,此時確實沒有閉包問題了,但在每次 count 變化時,定時器卸載并重新開始計時了,不滿足我們的最初需求了。

          要解決的唯一辦法為:

          const [count, setCount] = useState(0);

          // 通過 ref 來記憶最新的 count
          const countRef = useRef(count);
          countRef.current = count;

          useEffect(() => {
            const timer = setTimeout(() => {
              console.log(countRef.current)
            }, 3000);
            return () => {
              clearTimeout(timer);
            }
          }, [])

          雖然上面的代碼,很繞,但確實,只有這個解決方案。請記住這段代碼,功能真的很強大。

          const countRef = useRef(count);
          countRef.current = count;

          上面的例子,可以發(fā)現(xiàn),閉包問題是不能僅僅通過遵守 React 規(guī)則來避免的。我們必須清晰的知道,在什么場景下會出現(xiàn)閉包問題。

          2.1 正常情況下是不會有閉包問題的

          const [a, setA] = useState(0);
          const [b, setB] = useState(0);

          const c = a + b;

          useEffect(()=>{
           console.log(a, b, c)
          }, [a]);

          useEffect(()=>{
           console.log(a, b, c)
          }, [b]);

          useEffect(()=>{
           console.log(a, b, c)
          }, [c]);

          在一般的使用過程中,是不會有閉包問題的,如上代碼中,完全不會有閉包問題,和 deps 怎么寫沒有任何關(guān)系。

          2.2 延遲調(diào)用會存在閉包問題

          在延遲調(diào)用的場景下,一定會存在閉包問題。 什么是延遲調(diào)用?

          1. 使用 setTimeout、setInterval、Promise.then 等
          2. useEffect 的卸載函數(shù)
          const getUsername = () => {
            return new Promise((resolve, reject) => {
              setTimeout(() => {
                resolve('John');
              }, 3000);
            })
          }

          function Demo({
            const [count, setCount] = useState(0);

            // setTimeout 會造成閉包問題
            useEffect(() => {
              const timer = setTimeout(() => {
                console.log(count);
              }, 3000);
              return () => {
                clearTimeout(timer);
              }
            }, [])

            // setInterval 會造成閉包問題
            useEffect(() => {
              const timer = setInterval(() => {
                console.log(count);
              }, 3000);
              return () => {
                clearInterval(timer);
              }
            }, [])

            // Promise.then 會造成閉包問題
            useEffect(() => {
              getUsername().then(() => {
                console.log(count);
              });
            }, [])

            // useEffect 卸載函數(shù)會造成閉包問題
            useEffect(() => {
              return () => {
                console.log(count);
              }
            }, []);

            return (
              <button
                onClick={() =>
           setCount(c => c + 1)}
              >
                click
              </button>

            )
          }

          在以上示例代碼中,四種情況均會出現(xiàn)閉包問題,永遠輸出 0。這四種情況的根因都是一樣的,我們看一下代碼的執(zhí)行順序:

          1. 組件初始化,此時 count = 0
          2. 執(zhí)行 useEffect,此時 useEffect 的函數(shù)執(zhí)行,JS 引用鏈記錄了對 count=0 的引用關(guān)系
          3. 點擊 button,count 變化,但對之前的引用已經(jīng)無能為力了

          可以看到,閉包問題均是出現(xiàn)在延遲調(diào)用的場景下。解決辦法如下:

          const [count, setCount] = useState(0);

          // 通過 ref 來記憶最新的 count
          const countRef = useRef(count);
          countRef.current = count;

          useEffect(() => {
            const timer = setTimeout(() => {
              console.log(countRef.current)
            }, 3000);
            return () => {
              clearTimeout(timer);
            }
          }, [])

          ......

          通過 useRef 來保證任何時候訪問的 countRef.current 都是最新的,以解決閉包問題。

          到這里,我重申下我對 useEffect 的建議:

          1. 只有變化時,需要重新執(zhí)行 useEffect 的變量,才要放到 deps 中。而不是 useEffect 用到的變量都放到 deps 中。
          2. 在有延遲調(diào)用場景時,可以通過 ref 來解決閉包問題。

          3. 盡量不要用 useCallback

          我建議在項目中盡量不要用 useCallback,大部分場景下,不僅沒有提升性能,反而讓代碼可讀性變的很差。

          3.1 useCallback 大部分場景沒有提升性能

          useCallback 可以記住函數(shù),避免函數(shù)重復生成,這樣函數(shù)在傳遞給子組件時,可以避免子組件重復渲染,提高性能。

          const someFunc = useCallback(()=> {
             doSomething();
          }, []);

          return <ExpensiveComponent func={someFunc} />

          基于以上認知,很多同學(包括我自己)在寫代碼時,只要是個函數(shù),都加個 useCallback,是你么?反正我以前是。

          但我們要注意,提高性能還必須有另外一個條件,子組件必須使用了 shouldComponentUpdate 或者 React.memo 來忽略同樣的參數(shù)重復渲染。

          假如 ExpensiveComponent 組件只是一個普通組件,是沒有任何用的。比如下面這樣:

          const ExpensiveComponent = ({ func }) => {
            return (
              <div onClick={func}>
               hello
              </div>

            )
          }

          必須通過 React.memo 包裹 ExpensiveComponent ,才會避免參數(shù)不變的情況下的重復渲染,提高性能。

          const ExpensiveComponent = React.memo(({ func }) => {
            return (
              <div onClick={func}>
               hello
              </div>

            )
          })

          所以,useCallback 是要和 shouldComponentUpdate/React.memo 配套使用的,你用對了嗎?當然,我建議一般項目中不用考慮性能優(yōu)化的問題,也就是不要使用 useCallback 了,除非有個別非常復雜的組件,單獨使用即可。

          3.2 useCallback 讓代碼可讀性變差

          我看到過一些代碼,使用 useCallback 后,大概長這樣:

          const someFuncA = useCallback((d, g, x, y)=> {
             doSomething(a, b, c, d, g, x, y);
          }, [a, b, c]);

          const someFuncB = useCallback(()=> {
             someFuncA(d, g, x, y);
          }, [someFuncA, d, g, x, y]);

          useEffect(()=>{
            someFuncB();
          }, [someFuncB]);

          在上面的代碼中,變量依賴一層一層傳遞,最終要判斷具體哪些變量變化會觸發(fā) useEffect 執(zhí)行,是一件很頭疼的事情。

          我期望不要用 useCallback,直接裸寫函數(shù)就好:

          const someFuncA = (d, g, x, y)=> {
             doSomething(a, b, c, d, g, x, y);
          };

          const someFuncB = ()=> {
             someFuncA(d, g, x, y);
          };

          useEffect(()=>{
            someFuncB();
          }, [...]);

          在 useEffect 存在延遲調(diào)用的場景下,可能造成閉包問題,那通過咱們?nèi)f能的方法就能解決:

          const someFuncA = (d, g, x, y)=> {
             doSomething(a, b, c, d, g, x, y);
          };

          const someFuncB = ()=> {
             someFuncA(d, g, x, y);
          };

          + const someFuncBRef = useRef(someFuncB);
          + someFuncBRef.current = someFuncB;

          useEffect(()=>{
          +  setTimeout(()=>{
          +    someFuncBRef.current();
          +  }, 1000)
          }, [...]);

          對 useCallback 的建議就一句話:沒事別用 useCallback。

          4. useMemo 建議大量使用

          相較于 useCallback 而言,useMemo 的收益是顯而易見的。

          // 沒有使用 useMemo
          const memoizedValue = computeExpensiveValue(a, b);

          // 使用 useMemo
          const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

          如果沒有使用 useMemo,computeExpensiveValue 會在每一次渲染的時候執(zhí)行。如果使用了 useMemo,只有在 ab 變化時,才會執(zhí)行一次 computeExpensiveValue

          這筆賬大家應該都會算,所以我建議 useMemo 可以大量使用。

          當然也不是無節(jié)制的使用,在很簡單的基礎(chǔ)類型計算時,可能 useMemo 并不劃算。

          const a = 1;
          const b = 2;

          const c = useMemo(()=> a + b, [a, b]);

          比如上面的例子,請問計算 a+b 的消耗大?還是記錄 a/b ,并比較a/b 是否變化的消耗大?

          明顯 a+b 消耗更小。

          const a = 1;
          const b = 2;

          const c = a + b;

          這筆賬大家可以自己算,我建議簡單的基礎(chǔ)類型計算,就不要用 useMemo 了~

          5. useState 的正確使用姿勢

          useState 應該算最簡單的一個 Hooks,但在使用中,也有很多技巧可循,如果嚴格按照以下幾點,代碼可維護性直接翻倍。

          5.1 能用其他狀態(tài)計算出來就不用單獨聲明狀態(tài)

          一個 state 必須不能通過其它 state/props 直接計算出來,否則就不用定義 state。

          const SomeComponent = (props) => {
            const [a, setA] = useState(1);
            const [b, setB] = useState(2);
            
            const onClick = () => {
              const current = a + 1;
            
              setA(current);
              setB(current*2)
            }
            
            return (
              <div onClick={onClick}>
                 increment 
              </div>

            )
          }

          上面的示例中,變量 b 可以通過變量 a 計算出來,那就不要定義 b 了!

          const SomeComponent = (props) => {
            const [a, setA] = useState(1);
            
            const b = a*2;
            
            const onClick = () => {
              const current = a + 1;
            
              setA(current);
            }
            
            return (
              <div onClick={onClick}>
                 increment 
              </div>

            )
          }

          一般在項目中此類問題都比較隱晦,層層傳遞,在 Code Review 中很難一眼看出。如果能把變量定義清楚,那事情就成功了一半。

          5.2 保證數(shù)據(jù)源唯一

          在項目中同一個數(shù)據(jù),保證只存儲在一個地方。

          不要既存在 redux 中,又在組件中定義了一個 state 存儲。

          不要既存在父級組件中,又在當前組件中定義了一個 state 存儲。

          不要既存在 url query 中,又在組件中定義了一個 state 存儲。

          function SearchBox({ data }{
            const [searchKey, setSearchKey] = useState(getQuery('key'));
            
            const handleSearchChange = e => {
              const key = e.target.value;
              setSearchKey(key);
              history.push(`/movie-list?key=${key}`);
            }
            
            return (
                <input
                  value={searchKey}
                  placeholder="Search..."
                  onChange={handleSearchChange}
                />

            );
          }

          在上面的示例中,searchKey 存儲在兩個地方,既在 url query 上,又定義了一個 state。完全可以優(yōu)化成下面這樣:

          function SearchBox({ data }{
            const searchKey = parse(localtion.search)?.key;
            
            const handleSearchChange = e => {
              const key = e.target.value;
              history.push(`/movie-list?key=${key}`);
            }
            
            return (
                <input
                  value={searchKey}
                  placeholder="Search..."
                  onChange={handleSearchChange}
                />

            );
          }

          在實際項目開發(fā)中,此類問題也是比較隱晦,編碼時應注意。

          5.3 useState 適當合并

          項目中有木有寫過這樣的代碼:

          const [firstName, setFirstName] = useState();
          const [lastName, setLastName] = useState();
          const [school, setSchool] = useState();
          const [age, setAge] = useState();
          const [address, setAddress] = useState();

          const [weather, setWeather] = useState();
          const [room, setRoom] = useState();

          反正我最開始是寫過,useState 拆分過細,導致代碼中一大片 useState。

          我建議,同樣含義的變量可以合并成一個 state,代碼可讀性會提升很多:

          const [userInfo, setUserInfo] = useState({
            firstName,
            lastName,
            school,
            age,
            address
          });

          const [weather, setWeather] = useState();
          const [room, setRoom] = useState();

          當然這種方式我們在變更變量時,一定不要忘記帶上老的字段,比如我們只想修改 firstName

          setUserInfo(s=> ({
            ...s,
            fristName,
          }))

          其實如果是 React Class 組件,state 是會自動合并的:

          this.setState({
            firstName
          })

          在 Hooks 中,可以有這種用法嗎?其實是可以的,我們自己封裝一個 Hooks 就可以,比如 ahooks 的 useSetState[3],就封裝了類似的邏輯:

          const [userInfo, setUserInfo] = useSetState({
            firstName,
            lastName,
            school,
            age,
            address
          });

          // 自動合并
          setUserInfo({
            firstName
          })

          我自己在項目中大量使用了 useSetState 來代替 useState,來管理復雜類型的 state,媽媽更愛我了。

          六、總結(jié)

          作為資深的 React Hooks 用戶,我很認可 React Hooks 帶來的提效,這也是我這幾年完全擁抱 Hooks 的原因。同時我也越來越覺得 React Hooks 難駕馭,尤其隨著 React 18 的 concurrent mode 的到來,不知道會帶來什么坑。

          最后再給大家三個建議:

          1. 可以多使用別人封裝好的高級 Hooks 來提效,比如 ahooks[4] 庫(哈哈哈
          2. 可以多看看別人封裝好的 Hooks 源碼,加深對 React Hooks 理解,比如 ahooks[5] 庫(哈哈哈
          3. 可以關(guān)注下我的公眾號,我會經(jīng)常發(fā)布一些我自己寫的技術(shù)文章,以及轉(zhuǎn)發(fā)一些我認為比較好的文章,愛你喲(づ ̄3 ̄)づ╭?~

          Node 社群


          我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學習、共建。下方加 考拉 好友回復「Node」即可。


             “分享、點贊在看” 支持一波??

          參考資料

          [1]

          ahooks: https://github.com/alibaba/hooks

          [2]

          eslint-plugin-react-hooks: https://www.npmjs.com/package/eslint-plugin-react-hooks#installation

          [3]

          useSetState: https://ahooks.js.org/zh-CN/hooks/use-set-state

          [4]

          ahooks: https://github.com/alibaba/hooks

          [5]

          ahooks: https://github.com/alibaba/hooks

          瀏覽 66
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产抠逼视频 | 亚洲高清在线播放 | 无码精品一区二区三区四区五区六区 | av在线官网 | 中文字幕永久永久在线 |