<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>

          你真的用對(duì) useEffect 了嗎?

          共 43133字,需瀏覽 87分鐘

           ·

          2021-06-11 12:45


          點(diǎn)擊上方 前端瓶子君,關(guān)注公眾號(hào)

          回復(fù)算法,加入前端編程面試算法每日一題群


          最近在公司搬磚的過程中遇到了一個(gè)bug,頁面加載的時(shí)候會(huì)閃現(xiàn)一下,找了很久才發(fā)現(xiàn)是useeffect的依賴項(xiàng)的問題,所以打算寫篇文章總結(jié)一下,希望對(duì)看到文章的你也有所幫助。

          1.什么是useEffect?

          該 Hook 接收一個(gè)包含命令式、且可能有副作用代碼的函數(shù)。
          在函數(shù)組件主體內(nèi)(這里指在 React 渲染階段)改變 DOM、添加訂閱、設(shè)置定時(shí)器、記錄日志以及執(zhí)行其他包含副作用的操作都是不被允許的,因?yàn)檫@可能會(huì)產(chǎn)生莫名其妙的 bug 并破壞 UI 的一致性。使用 useEffect 完成副作用操作。賦值給 useEffect 的函數(shù)會(huì)在組件渲染到屏幕之后執(zhí)行。你可以把 effect 看作從 React 的純函數(shù)式世界通往命令式世界的逃生通道。(官方文檔)

          這么一看你也許會(huì)有點(diǎn)不明白...
          看下面這個(gè)例子:

          import React, { useState, useEffect } from 'react';

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

            useEffect(() => {
              document.title = `You clicked ${count} times`;
            });

            return (
              <div>
                <p>You clicked {count} times</p>
                <button onClick={() => setCount(count + 1)}>
                  Click me
                </button>
              </div>

            );
          }
          復(fù)制代碼

          useEffect 做了什么?
          通過使用這個(gè) Hook,你可以告訴 React 組件需要在渲染后執(zhí)行某些操作。React 會(huì)保存你傳遞的函數(shù)(我們將它稱之為 “effect”),并且在執(zhí)行 DOM 更新之后調(diào)用它。在這個(gè) effect 中,我們?cè)O(shè)置了 document 的 title 屬性,不過我們也可以執(zhí)行數(shù)據(jù)獲取或調(diào)用其他命令式的 API。

          為什么在組件內(nèi)部調(diào)用 useEffect?
          將 useEffect 放在組件內(nèi)部讓我們可以在 effect 中直接訪問 count state 變量(或其他 props)。我們不需要特殊的 API 來讀取它 —— 它已經(jīng)保存在函數(shù)作用域中。Hook 使用了 JavaScript 的閉包機(jī)制,而不用在 JavaScript 已經(jīng)提供了解決方案的情況下,還引入特定的 React API。

          useEffect 會(huì)在每次渲染后都執(zhí)行嗎?
          是的,默認(rèn)情況下,它在第一次渲染之后和每次更新之后都會(huì)執(zhí)行。(我們稍后會(huì)談到如何控制它。)你可能會(huì)更容易接受 effect 發(fā)生在“渲染之后”這種概念,不用再去考慮“掛載”還是“更新”。React 保證了每次運(yùn)行 effect 的同時(shí),DOM 都已經(jīng)更新完畢 如果你熟悉 React class 的生命周期函數(shù),你可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個(gè)函數(shù)的組合。

          2.如何使用useEffect

          2.1實(shí)現(xiàn)componentDidMount 的功能

          useEffect的第二個(gè)參數(shù)為一個(gè)空數(shù)組,初始化調(diào)用一次之后不再執(zhí)行,相當(dāng)于componentDidMount。

          function Demo ({
            useEffect(() => {
              console.log('hello world')
            }, [])
            return (
              <div>
                hello world
              </div>

            )
          }
          // 等價(jià)于
          class Demo extends Component {
            componentDidMount() {
              console.log('hello world')
            }
            render() {
              return (
                <div>
                  hello world
                </div>

              );
            }
          }
          復(fù)制代碼

          2.2實(shí)現(xiàn)組合 componentDidMount componentDidUpdate 的功能

          當(dāng)useEffect沒有第二個(gè)參數(shù)時(shí),組件的初始化和更新都會(huì)執(zhí)行。

          class Example extends React.Component {
            constructor(props) {
              super(props);
              this.state = {
                count0
              };
            }

            componentDidMount() {
              document.title = `You clicked ${this.state.count} times`;
            }

            componentDidUpdate() {
              document.title = `You clicked ${this.state.count} times`;
            }

            render() {
              return (
                <div>
                  <p>You clicked {this.state.count} times</p>
                  <button onClick={() => this.setState({ count: this.state.count + 1 })}>
                    Click me
                  </button>
                </div>

              );
            }
          }
          // 等價(jià)于
          import React, { useState, useEffect } from 'react';

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

            useEffect(() => {
              document.title = `You clicked ${count} times`;
            });

            return (
              <div>
                <p>You clicked {count} times</p>
                <button onClick={() => setCount(count + 1)}>
                  Click me
                </button>
              </div>

            );
          }

          復(fù)制代碼

          2.3實(shí)現(xiàn)組合 componentDidMount componentWillUnmount 的功能

          useEffect返回一個(gè)函數(shù),這個(gè)函數(shù)會(huì)在組件卸載時(shí)執(zhí)行。

          class Example extends Component {
            constructor (props) {
              super(props);
              this.state = {
                count0
              }
            }
            componentDidMount() {
              this.id = setInterval(() => {
                this.setState({countthis.state.count + 1})
              }, 1000);
            }
            componentWillUnmount() {
              clearInterval(this.id)
            }
            render() { 
              return <h1>{this.state.count}</h1>;
            }
          }
          // 等價(jià)于
          function Example({
            const [count, setCount] = useState(0);

            useEffect(() => {
              const id = setInterval(() => {
                setCount(c => c + 1);
              }, 1000);
              return () => clearInterval(id);
            }, []);

            return <h1>hello world</h1>
          }
          復(fù)制代碼

          3.useEffect使用的坑

          3.1 無限循環(huán)

          當(dāng)useEffect的第二個(gè)參數(shù)傳數(shù)組傳一個(gè)依賴項(xiàng),當(dāng)依賴項(xiàng)的值發(fā)生變化,都會(huì)觸發(fā)useEffect執(zhí)行。
          請(qǐng)看下面的例子:

          App組件顯示了一個(gè)項(xiàng)目列表,狀態(tài)和狀態(tài)更新函數(shù)來自與useState這個(gè)hooks,通過調(diào)用useState,來創(chuàng)建App組件的內(nèi)部狀態(tài)。初始狀態(tài)是一個(gè)object,其中的hits為一個(gè)空數(shù)組,目前還沒有請(qǐng)求后端的接口。

          import React, { useState } from 'react';
           
          function App({
            const [data, setData] = useState({ hits: [] });
           
            return (
              <ul>
                {data.hits.map(item => (
                  <li key={item.objectID}>
                    <a href={item.url}>{item.title}</a>
                  </li>
                ))}
              </ul>

            );
          }
           
          export default App;
          復(fù)制代碼

          為了獲取后端提供的數(shù)據(jù),接下來將使用axios來發(fā)起請(qǐng)求,同樣也可以使用fetch,這里會(huì)使用useEffect來隔離副作用。

          import React, { useState, useEffect } from 'react';
          import axios from 'axios';
           
          function App({
            const [data, setData] = useState({ hits: [] });
           
            useEffect(async () => {
              const result = await axios(
                'http://localhost/api/v1/search?query=redux',
              );
           
              setData(result.data);
            });
           
            return (
              <ul>
                {data.hits.map(item => (
                  <li key={item.objectID}>
                    <a href={item.url}>{item.title}</a>
                  </li>
                ))}
              </ul>

            );
          }
           
          export default App;
          復(fù)制代碼

          在useEffect中,不僅會(huì)請(qǐng)求后端的數(shù)據(jù),還會(huì)通過調(diào)用setData來更新本地的狀態(tài),這樣會(huì)觸發(fā)view的更新。

          但是,運(yùn)行這個(gè)程序的時(shí)候,會(huì)出現(xiàn)無限循環(huán)的情況。useEffect在組件mount時(shí)執(zhí)行,但也會(huì)在組件更新時(shí)執(zhí)行。因?yàn)槲覀冊(cè)诿看握?qǐng)求數(shù)據(jù)之后都會(huì)設(shè)置本地的狀態(tài),所以組件會(huì)更新,因此useEffect會(huì)再次執(zhí)行,因此出現(xiàn)了無限循環(huán)的情況。我們只想在組件mount時(shí)請(qǐng)求數(shù)據(jù)。我們可以傳遞一個(gè)空數(shù)組作為useEffect的第二個(gè)參數(shù),這樣就能避免在組件更新執(zhí)行useEffect,只會(huì)在組件mount時(shí)執(zhí)行。

          import React, { useState, useEffect } from 'react';
          import axios from 'axios';
           
          function App({
            const [data, setData] = useState({ hits: [] });
           
            useEffect(async () => {
              const result = await axios(
                'http://localhost/api/v1/search?query=redux',
              );
           
              setData(result.data);
            }, []);
           
            return (
              <ul>
                {data.hits.map(item => (
                  <li key={item.objectID}>
                    <a href={item.url}>{item.title}</a>
                  </li>
                ))}
              </ul>

            );
          }
           
          export default App;
          復(fù)制代碼

          useEffect的第二個(gè)參數(shù)可用于定義其依賴的所有變量。如果其中一個(gè)變量發(fā)生變化,則useEffect會(huì)再次運(yùn)行。如果包含變量的數(shù)組為空,則在更新組件時(shí)useEffect不會(huì)再執(zhí)行,因?yàn)樗粫?huì)監(jiān)聽任何變量的變更。

          再看這個(gè)例子:
          業(yè)務(wù)場(chǎng)景:需要在頁面一開始時(shí)得到一個(gè)接口的返回值,取調(diào)用另一個(gè)接口。
          我的思路是,先設(shè)置這個(gè)接口的返回值為data=[], 等到數(shù)據(jù)是再去請(qǐng)求另一個(gè)接口,即data作為useEffect的第二個(gè)參數(shù)傳入。
          但是不知道為什么會(huì)造成死循環(huán),拿不到我們想要的結(jié)果。
          直到在官網(wǎng)看到這個(gè)例子:

          知道useEffect會(huì)比較前一次渲染和后一次渲染的值,然后我就在想,如果我所設(shè)置的data=[],那么即使我后一次渲染的data也為[],那么[]===[]為false,所以才會(huì)造成useEffect會(huì)一直不停的渲染,所以我把data的初始值改為undefined,試了一下果然可以。

          結(jié)論:useEffect的不作為componentDidUnmount的話,傳入第二個(gè)參數(shù)時(shí)一定注意:第二個(gè)參數(shù)不能為引用類型,引用類型比較不出來數(shù)據(jù)的變化,會(huì)造成死循環(huán)

          3.2使用async await 時(shí)的報(bào)錯(cuò)

          在代碼中,我們使用async / await從第三方API獲取數(shù)據(jù)。如果你對(duì)async/await熟悉的話,你會(huì)知道,每個(gè)async函數(shù)都會(huì)默認(rèn)返回一個(gè)隱式的promise。但是,useEffect不應(yīng)該返回任何內(nèi)容。這就是為什么會(huì)在控制臺(tái)日志中看到以下警告:

          Warning: useEffect function must return a cleanup function or nothing. Promises and useEffect(async () => …) are not supported, but you can call an async function inside an effect

          這就是為什么不能直接在useEffect中使用async函數(shù),因此,我們可以不直接調(diào)用async函數(shù),而是像下面這樣:

          function App({
            const [data, setData] = useState({ hits: [] });
           
            useEffect(() => {
              const fetchData = async () => {
                const result = await axios(
                  'http://localhost/api/v1/search?query=redux',
                );
           
                setData(result.data);
              };
           
              fetchData();
            }, []);
           
            return (
              <ul>
                {data.hits.map(item => (
                  <li key={item.objectID}>
                    <a href={item.url}>{item.title}</a>
                  </li>
                ))}
              </ul>

            );
          }
          復(fù)制代碼

          4.useEffect在實(shí)戰(zhàn)中的應(yīng)用

          4.1 響應(yīng)更新

          很多情況下,我們需要響應(yīng)用戶的輸入,然后再請(qǐng)求。這個(gè)時(shí)候我們會(huì)引入一個(gè)input框,監(jiān)聽query值的變化:

          import axios from 'axios';
           
          function App({
            const [data, setData] = useState({ hits: [] });
            const [query, setQuery] = useState('redux');
           
            useEffect(() => {
              const fetchData = async () => {
                const result = await axios(
                  'http://localhost/api/v1/search?query=redux',
                );
                setData(result.data);
              };
           
              fetchData();
            }, []);
           
            return (
              <Fragment>
                <input
                  type="text"
                  value={query}
                  onChange={event =>
           setQuery(event.target.value)}
                />
                <ul>
                  {data.hits.map(item => (
                    <li key={item.objectID}>
                      <a href={item.url}>{item.title}</a>
                    </li>
                  ))}
                </ul>
              </Fragment>

            );
          }
          復(fù)制代碼

          有個(gè)query值,已經(jīng)更新query的邏輯,還需要將這個(gè)query值傳遞給后臺(tái),這個(gè)操作會(huì)在useEffect中進(jìn)行
          前面我們說了,目前的useEffect只會(huì)在組件mount時(shí)執(zhí)行,并且useEffect的第二個(gè)參數(shù)是依賴的變量,一旦這個(gè)依賴的變量變動(dòng),useEffect就會(huì)重新執(zhí)行,所以我們需要添加query為useEffect的依賴:

          function App({
            const [data, setData] = useState({ hits: [] });
            const [query, setQuery] = useState('redux');
           
            useEffect(() => {
              const fetchData = async () => {
                const result = await axios(
                  `http://localhost/api/v1/search?query=${query}`,
                );
           
                setData(result.data);
              };
           
              fetchData();
            }, [query]);
           
            return (
              ...
            );
          }
          復(fù)制代碼

          一旦更改了query值,就可以重新獲取數(shù)據(jù)。但這會(huì)帶來另一個(gè)問題:query的任何一次變動(dòng)都會(huì)請(qǐng)求后端,這樣會(huì)帶來比較大的訪問壓力。這個(gè)時(shí)候我們需要引入一個(gè)按鈕,點(diǎn)擊這個(gè)按鈕再發(fā)起請(qǐng)求。

          function App({
           const [data, setData] = useState({ hits: [] });
           const [query, setQuery] = useState('redux');
           const [search, setSearch] = useState('');

           useEffect(() => {
             const fetchData = async () => {
               const result = await axios(
                 `http://localhost/api/v1/search?query=${query}`,
               );

               setData(result.data);
             };

             fetchData();
           }, [query]);

           return (
             <Fragment>
               <input
                 type="text"
                 value={query}
                 onChange={event =>
           setQuery(event.target.value)}
               />
               <button type="button" onClick={() => setSearch(query)}>
                 Search
               </button>

               <ul>
                 {data.hits.map(item => (
                   <li key={item.objectID}>
                     <a href={item.url}>{item.title}</a>
                   </li>
                 ))}
               </ul>
             </Fragment>

           );
          }
          復(fù)制代碼

          可以看到上面我們添加了一個(gè)新的按鈕,然后創(chuàng)建新的組件state:search。每次點(diǎn)擊按鈕時(shí),會(huì)把search的值設(shè)置為query,這個(gè)時(shí)候我們需要修改useEffect中的依賴項(xiàng)為search,這樣每次點(diǎn)擊按鈕,search值變更,useEffect就會(huì)重新執(zhí)行,避免不必要的變更:

          function App({
            const [data, setData] = useState({ hits: [] });
            const [query, setQuery] = useState('redux');
            const [search, setSearch] = useState('redux');
           
            useEffect(() => {
              const fetchData = async () => {
                const result = await axios(
                  `http://localhost/api/v1/search?query=${search}`,
                );
           
                setData(result.data);
              };
           
              fetchData();
            }, [search]);
           
            return (
              ...
            );
          }
           
          export default App;
          復(fù)制代碼

          此外,search state的初始狀態(tài)設(shè)置為與query state 相同的狀態(tài),因?yàn)榻M件首先會(huì)在mount時(shí)獲取數(shù)據(jù)。所以簡單點(diǎn),直接將的要請(qǐng)求的后端URL設(shè)置為search state的初始值。

          function App({
            const [data, setData] = useState({ hits: [] });
            const [query, setQuery] = useState('redux');
            const [url, setUrl] = useState(
              'http://localhost/api/v1/search?query=redux',
            );
           
            useEffect(() => {
              const fetchData = async () => {
                const result = await axios(url);
           
                setData(result.data);
              };
           
              fetchData();
            }, [url]);
           
            return (
              <Fragment>
                <input
                  type="text"
                  value={query}
                  onChange={event =>
           setQuery(event.target.value)}
                />
                <button
                  type="button"
                  onClick={() =>

                    setUrl(`http://localhost/api/v1/search?query=${query}`)
                  }
                >
                  Search
                </button>
              <ul>
                  {data.hits.map(item => (
                    <li key={item.objectID}>
                      <a href={item.url}>{item.title}</a>
                    </li>
                  ))}
                </ul>
              </Fragment>

            );
          }
          復(fù)制代碼

          4.2 如何處理Loading和Error

          良好的用戶體驗(yàn)是需要在請(qǐng)求后端數(shù)據(jù),數(shù)據(jù)還沒有返回時(shí)展現(xiàn)loading的狀態(tài),因此,我們還需要添加一個(gè)loading的state

          import React, { Fragment, useState, useEffect } from 'react';
          import axios from 'axios';
           
          function App({
            const [data, setData] = useState({ hits: [] });
            const [query, setQuery] = useState('redux');
            const [url, setUrl] = useState(
              'http://hn.algolia.com/api/v1/search?query=redux',
            );
            const [isLoading, setIsLoading] = useState(false);
           
            useEffect(() => {
              const fetchData = async () => {
                setIsLoading(true);
           
                const result = await axios(url);
           
                setData(result.data);
                setIsLoading(false);
              };
           
              fetchData();
            }, [url]);
            return (
              <Fragment>
                <input
                  type="text"
                  value={query}
                  onChange={event =>
           setQuery(event.target.value)}
                />
                <button
                  type="button"
                  onClick={() =>

                    setUrl(`http://localhost/api/v1/search?query=${query}`)
                  }
                >
                  Search
                </button>
           
                {isLoading ? (
                  <div>Loading ...</div>
                ) : (
                  <ul>
                    {data.hits.map(item => (
                      <li key={item.objectID}>
                        <a href={item.url}>{item.title}</a>
                      </li>
                    ))}
                  </ul>
                )}
              </Fragment>

            );
          }
          復(fù)制代碼

          在useEffect中,請(qǐng)求數(shù)據(jù)前將loading置為true,在請(qǐng)求完成后,將loading置為false。我們可以看到useEffect的依賴數(shù)據(jù)中并沒有添加loading,這是因?yàn)椋覀儾恍枰賚oading變更時(shí)重新調(diào)用useEffect。請(qǐng)記住:只有某個(gè)變量更新后,需要重新執(zhí)行useEffect的情況,才需要將該變量添加到useEffect的依賴數(shù)組中。

          loading處理完成后,還需要處理錯(cuò)誤,這里的邏輯是一樣的,使用useState來創(chuàng)建一個(gè)新的state,然后在useEffect中特定的位置來更新這個(gè)state。由于我們使用了async/await,可以使用一個(gè)大大的try-catch:

          import React, { Fragment, useState, useEffect } from 'react';
          import axios from 'axios';
           
          function App({
            const [data, setData] = useState({ hits: [] });
            const [query, setQuery] = useState('redux');
            const [url, setUrl] = useState(
              'http://localhost/api/v1/search?query=redux',
            );
            const [isLoading, setIsLoading] = useState(false);
            const [isError, setIsError] = useState(false);
           
            useEffect(() => {
              const fetchData = async () => {
                setIsError(false);
                setIsLoading(true);
           
                try {
                  const result = await axios(url);
           
                  setData(result.data);
                } catch (error) {
                  setIsError(true);
                }
           
                setIsLoading(false);
              };
           
              fetchData();
            }, [url]);
            return (......)

          復(fù)制代碼

          每次useEffect執(zhí)行時(shí),將會(huì)重置error;在出現(xiàn)錯(cuò)誤的時(shí)候,將error置為true;在正常請(qǐng)求完成后,將error置為false。

          4.3 處理表單

          通常,我們不僅會(huì)用到上面的輸入框和按鈕,更多的時(shí)候是一張表單,所以也可以在表單中使用useEffect來處理數(shù)據(jù)請(qǐng)求,邏輯是相同的:

          function App({
            ...
           
            return (
              <Fragment>
                <form
                  onSubmit={() =>

                    setUrl(`http://localhost/api/v1/search?query=${query}`)
                  }
                >
                  <input
                    type="text"
                    value={query}
                    onChange={event =>
           setQuery(event.target.value)}
                  />
                  <button type="submit">Search</button>
                </form>
           
                {isError && <div>Something went wrong ...</div>}
           
                ...
              </Fragment>

            );
          }
          復(fù)制代碼

          上面的例子中,提交表單的時(shí)候,會(huì)觸發(fā)頁面刷新;就像通常的做法那樣,還需要阻止默認(rèn)事件,來阻止頁面的刷新。

          function App({
            ...
           
            const doFetch = () => {
              setUrl(`http://localhost/api/v1/search?query=${query}`);
            };
           
            return (
              <Fragment>
                <form onSubmit={event => {
                  doFetch();
           
                  event.preventDefault();
                }}>
                  <input
                    type="text"
                    value={query}
                    onChange={event =>
           setQuery(event.target.value)}
                  />
                  <button type="submit">Search</button>
                </form>
           
                {isError && <div>Something went wrong ...</div>}
           
                ...
              </Fragment>

            );
          }
          復(fù)制代碼

          4.4 自定義hooks

          我們可以看到上面的組件,添加了一系列hooks和邏輯之后,已經(jīng)變得非常的龐大。那這時(shí)候我們?cè)趺刺幚砟兀縣ooks的一個(gè)非常的優(yōu)勢(shì),就是能夠很方便的提取自定義的hooks。這個(gè)時(shí)候,我們就能把上面的一大堆邏輯抽取到一個(gè)單獨(dú)的hooks中,方便復(fù)用和解耦。

          function useHackerNewsApi = () => {
            const [data, setData] = useState({ hits: [] });
            const [url, setUrl] = useState(
              'http://localhost/api/v1/search?query=redux',
            );
            const [isLoading, setIsLoading] = useState(false);
            const [isError, setIsError] = useState(false);
           
            useEffect(() => {
              const fetchData = async () => {
                setIsError(false);
                setIsLoading(true);
           
                try {
                  const result = await axios(url);
           
                  setData(result.data);
                } catch (error) {
                  setIsError(true);
                }
           
                setIsLoading(false);
              };
           
              fetchData();
            }, [url]);
           
            const doFetch = () => {
              setUrl(`http://localhost/api/v1/search?query=${query}`);
            };
           
            return { data, isLoading, isError, doFetch };
          }
          復(fù)制代碼

          在自定義的hooks抽離完成后,引入到組件中。

          function App({
            const [query, setQuery] = useState('redux');
            const { data, isLoading, isError, doFetch } = useHackerNewsApi();
           
            return (
              <Fragment>
                ...
              </Fragment>

            );
          }
          復(fù)制代碼

          然后我們需要在form組件中設(shè)定初始的后端URL

          const useHackerNewsApi = () => {
            ...
           
            useEffect(
              ...
            );
           
            const doFetch = url => {
              setUrl(url);
            };
           
            return { data, isLoading, isError, doFetch };
          };
           
          function App({
            const [query, setQuery] = useState('redux');
            const { data, isLoading, isError, doFetch } = useHackerNewsApi();
           
            return (
              <Fragment>
                <form
                  onSubmit={event =>
           {
                    doFetch(
                      `http://localhost/api/v1/search?query=${query}`,
                    );
           
                    event.preventDefault();
                  }}
                >
                  <input
                    type="text"
                    value={query}
                    onChange={event =>
           setQuery(event.target.value)}
                  />
                  <button type="submit">Search</button>
                </form>
           
                ...
              </Fragment>

            );
          }
          復(fù)制代碼

          4.5使用useReducer整合邏輯

          到目前為止,我們已經(jīng)使用了各種state hooks來管理數(shù)據(jù),包括loading、error、data等狀態(tài)。但是我們可以看到,這三個(gè)有關(guān)聯(lián)的狀態(tài)確是分散的,它們通過分離的useState來創(chuàng)建,為了有關(guān)聯(lián)的狀態(tài)整合到一起,我們需要用到useReducer。

          如果你寫過redux,那么將會(huì)對(duì)useReducer非常的熟悉,可以把它理解為一個(gè)輕量額redux。useReducer 返回一個(gè)狀態(tài)對(duì)象和一個(gè)可以改變狀態(tài)對(duì)象的dispatch函數(shù)。跟redux類似的,dispatch函數(shù)接受action作為參數(shù),action包含type和payload屬性。我們看一個(gè)簡單的例子吧:

          import React, {
            Fragment,
            useState,
            useEffect,
            useReducer,
          from 'react';
          import axios from 'axios';
           
          const dataFetchReducer = (state, action) => {
            ...
          };
           
          const useDataApi = (initialUrl, initialData) => {
            const [url, setUrl] = useState(initialUrl);
           
            const [state, dispatch] = useReducer(dataFetchReducer, {
              isLoadingfalse,
              isErrorfalse,
              data: initialData,
            });
           
            ...
          };
          復(fù)制代碼

          useReducer將reducer函數(shù)和初始狀態(tài)對(duì)象作為參數(shù)。在我們的例子中,data,loading和error狀態(tài)的初始值與useState創(chuàng)建時(shí)一致,但它們已經(jīng)整合到一個(gè)由useReducer創(chuàng)建對(duì)象,而不是多個(gè)useState創(chuàng)建的狀態(tài)。

          const dataFetchReducer = (state, action) => {
            ...
          };
           
          const useDataApi = (initialUrl, initialData) => {
            const [url, setUrl] = useState(initialUrl);
           
            const [state, dispatch] = useReducer(dataFetchReducer, {
              isLoadingfalse,
              isErrorfalse,
              data: initialData,
            });
           
            useEffect(() => {
              const fetchData = async () => {
                dispatch({ type'FETCH_INIT' });
           
                try {
                  const result = await axios(url);
           
                  dispatch({ type'FETCH_SUCCESS'payload: result.data });
                } catch (error) {
                  dispatch({ type'FETCH_FAILURE' });
                }
              };
           
              fetchData();
            }, [url]);
           
            ...
          };
          復(fù)制代碼

          在獲取數(shù)據(jù)時(shí),可以調(diào)用dispatch函數(shù),將信息發(fā)送給reducer。使用dispatch函數(shù)發(fā)送的參數(shù)為object,具有type屬性和可選payload的屬性。type屬性告訴reducer需要應(yīng)用哪個(gè)狀態(tài)轉(zhuǎn)換,并且reducer可以使用payload來創(chuàng)建新的狀態(tài)。在這里,我們只有三個(gè)狀態(tài)轉(zhuǎn)換:發(fā)起請(qǐng)求,請(qǐng)求成功,請(qǐng)求失敗。

          在自定義hooks的末尾,state像以前一樣返回,但是因?yàn)槲覀兡玫降氖且粋€(gè)狀態(tài)對(duì)象,而不是以前那種分離的狀態(tài),所以需要將狀態(tài)對(duì)象解構(gòu)之后再返回。這樣,調(diào)用useDataApi自定義hooks的人仍然可以訪問data,isLoading 和 isError這三個(gè)狀態(tài)。

          const useDataApi = (initialUrl, initialData) => {
            const [url, setUrl] = useState(initialUrl);
           
            const [state, dispatch] = useReducer(dataFetchReducer, {
              isLoadingfalse,
              isErrorfalse,
              data: initialData,
            });
           
            ...
           
            const doFetch = url => {
              setUrl(url);
            };
           
            return { ...state, doFetch };
          }; 
          復(fù)制代碼

          接下來添加reducer函數(shù)的實(shí)現(xiàn)。它需要三種不同的狀態(tài)轉(zhuǎn)換FETCH_INIT,F(xiàn)ETCH_SUCCESS和FETCH_FAILURE。每個(gè)狀態(tài)轉(zhuǎn)換都需要返回一個(gè)新的狀態(tài)對(duì)象。讓我們看看如何使用switch case語句實(shí)現(xiàn)它:

          switch (action.type) {
              case 'FETCH_INIT':
                return {
                  ...state,
                  isLoadingtrue,
                  isErrorfalse
                };
              case 'FETCH_SUCCESS':
                return {
                  ...state,
                  isLoadingfalse,
                  isErrorfalse,
                  data: action.payload,
                };
              case 'FETCH_FAILURE':
                return {
                  ...state,
                  isLoadingfalse,
                  isErrortrue,
                };
              default:
                throw new Error();
            }
          };
          復(fù)制代碼

          4.6取消數(shù)據(jù)請(qǐng)求

          React中的一種很常見的問題是:如果在組件中發(fā)送一個(gè)請(qǐng)求,在請(qǐng)求還沒有返回的時(shí)候卸載了組件,這個(gè)時(shí)候還會(huì)嘗試設(shè)置這個(gè)狀態(tài),會(huì)報(bào)錯(cuò)。我們需要在hooks中處理這種情況,可以看下是怎樣處理的:

          const useDataApi = (initialUrl, initialData) => {
            const [url, setUrl] = useState(initialUrl);
           
            const [state, dispatch] = useReducer(dataFetchReducer, {
              isLoadingfalse,
              isErrorfalse,
              data: initialData,
            });
           
            useEffect(() => {
              let didCancel = false;
              const fetchData = async () => {
                dispatch({ type'FETCH_INIT' });
                try {
                  const result = await axios(url);
                  if (!didCancel) {
                    dispatch({ type'FETCH_SUCCESS'payload: result.data });
                  }
                } catch (error) {
                  if (!didCancel) {
                    dispatch({ type'FETCH_FAILURE' });
                  }
                }
              };
            fetchData();
              return () => {
                didCancel = true;
              };
            }, [url]);
           
            const doFetch = url => {
              setUrl(url);
            };
           
            return { ...state, doFetch };
          };
          復(fù)制代碼

          我們可以看到這里新增了一個(gè)didCancel變量,如果這個(gè)變量為true,不會(huì)再發(fā)送dispatch,也不會(huì)再執(zhí)行設(shè)置狀態(tài)這個(gè)動(dòng)作。這里我們?cè)趗seEffe的返回函數(shù)中將didCancel置為true,在卸載組件時(shí)會(huì)自動(dòng)調(diào)用這段邏輯。也就避免了再卸載的組件上設(shè)置狀態(tài)。

          5.useEffect 與 useLayoutEffect

          1.png
          • useEffect 在全部渲染完畢后才會(huì)執(zhí)行
          • useLayoutEffect 會(huì)在 瀏覽器 layout 之后,painting 之前執(zhí)行
          • 其函數(shù)簽名與 useEffect 相同,但它會(huì)在所有的 DOM 變更之后同步調(diào)用 effect
          • 可以使用它來讀取 DOM 布局并同步觸發(fā)重渲染
          • 在瀏覽器執(zhí)行繪制之前 useLayoutEffect 內(nèi)部的更新計(jì)劃將被同步刷新
          • 盡可能使用標(biāo)準(zhǔn)的 useEffect 以避免阻塞視圖更新
          function LayoutEffect({
             const [color, setColor] = useState('red');
             useLayoutEffect(() => {
                 alert(color);
             });
             useEffect(() => {
                 console.log('color', color);
             });
             return (
                 <>
                     <div id="myDiv" style={{ background: color }}>顏色</div>
                     <button onClick={() => setColor('red')}>紅</button>
                     <button onClick={() => setColor('yellow')}>黃</button>
                     <button onClick={() => setColor('blue')}>藍(lán)</button>
                 </>

             );
          }
          復(fù)制代碼

          useEffect優(yōu)勢(shì)
          useEffect 在渲染結(jié)束時(shí)執(zhí)行,所以不會(huì)阻塞瀏覽器渲染進(jìn)程,所以使用 Function Component 寫的項(xiàng)目一般都有用更好的性能。
          自然符合 React Fiber 的理念,因?yàn)?Fiber 會(huì)根據(jù)情況暫停或插隊(duì)執(zhí)行不同組件的 Render,如果代碼遵循了 Capture Value 的特性,在 Fiber 環(huán)境下會(huì)保證值的安全訪問,同時(shí)弱化生命周期也能解決中斷執(zhí)行時(shí)帶來的問題。
          useEffect 不會(huì)在服務(wù)端渲染時(shí)執(zhí)行。由于在 DOM 執(zhí)行完畢后才執(zhí)行,所以能保證拿到狀態(tài)生效后的 DOM 屬性。

          6.useEffect源碼解析

          首先我們要牢記 effect hook 的一些屬性:

          • 它們?cè)阡秩緯r(shí)被創(chuàng)建,但是在瀏覽器繪制后運(yùn)行。
          • 如果給出了銷毀指令,它們將在下一次繪制前被銷毀。
          • 它們會(huì)按照定義的順序被運(yùn)行。

          于是就應(yīng)該有另一個(gè)隊(duì)列來保存這些 effect hook,并且還要能夠在繪制后被定位到。通常來說,應(yīng)該是 fiber 保存包含了 effect 節(jié)點(diǎn)的隊(duì)列。每個(gè) effect 節(jié)點(diǎn)都是一個(gè)不同的類型,并能在適當(dāng)?shù)臓顟B(tài)下被定位到:

          在修改之前調(diào)用 getSnapshotBeforeUpdate() 實(shí)例。

          運(yùn)行所有插入、更新、刪除和 ref 的卸載。

          運(yùn)行所有生命周期函數(shù)和 ref 回調(diào)函數(shù)。生命周期函數(shù)會(huì)在一個(gè)獨(dú)立的通道中運(yùn)行,所以整個(gè)組件樹中所有的替換、更新、刪除都會(huì)被調(diào)用。這個(gè)過程還會(huì)觸發(fā)任何特定于渲染器的初始 effect hook。

          useEffect() hook 調(diào)度的 effect —— 也被稱為“被動(dòng) effect”,它基于這部分代碼。

          hook effect 將會(huì)被保存在 fiber 一個(gè)稱為 updateQueue 的屬性上,每個(gè) effect 節(jié)點(diǎn)都有如下的結(jié)構(gòu):

          • tag —— 一個(gè)二進(jìn)制數(shù)字,它控制了 effect 節(jié)點(diǎn)的行為
          • create —— 繪制之后運(yùn)行的回調(diào)函數(shù)
          • destroy —— 它是 create() 返回的回調(diào)函數(shù),將會(huì)在初始渲染前運(yùn)行
          • inputs —— 一個(gè)集合,該集合中的值將會(huì)決定一個(gè) effect 節(jié)點(diǎn)是否應(yīng)該被銷毀或者重新創(chuàng)建
          • next —— 它指向下一個(gè)定義在函數(shù)組件中的 effect 節(jié)點(diǎn)

          除了 tag 屬性,其他的屬性都很簡明易懂。如果你對(duì) hook 很了解,你應(yīng)該知道,React 提供了一些特殊的 effect hook:比如 useMutationEffect() 和 useLayoutEffect()。這兩個(gè) effect hook 內(nèi)部都使用了 useEffect(),實(shí)際上這就意味著它們創(chuàng)建了 effect hook,但是卻使用了不同的 tag 屬性值。這個(gè) tag 屬性值是由二進(jìn)制的值組合而成(詳見源碼):

          const NoEffect = /*             */ 0b00000000;
          const UnmountSnapshot = /*      */ 0b00000010;
          const UnmountMutation = /*      */ 0b00000100;
          const MountMutation = /*        */ 0b00001000;
          const UnmountLayout = /*        */ 0b00010000;
          const MountLayout = /*          */ 0b00100000;
          const MountPassive = /*         */ 0b01000000;
          const UnmountPassive = /*       */ 0b10000000;
          復(fù)制代碼

          復(fù)制代碼React 支持的 hook effect 類型 這些二進(jìn)制值中最常用的情景是使用管道符號(hào)(|)連接,將比特相加到單個(gè)某值上。然后我們就可以使用符號(hào)(&)檢查某個(gè) tag 屬性是否能觸發(fā)一個(gè)特定的行為。如果結(jié)果是非零的,就表示可以。

          const effectTag = MountPassive | UnmountPassive
          assert(effectTag, 0b11000000)
          assert(effectTag & MountPassive, 0b10000000)
          復(fù)制代碼

          復(fù)制代碼如何使用 React 的二進(jìn)制設(shè)計(jì)模式的示例 這里是 React 支持的 hook effect,以及它們的 tag 屬性(詳見源碼):

          • Default effect?——?UnmountPassive | MountPassive.
          • Mutation effect?——?UnmountSnapshot | MountMutation.
          • Layout effect?——?UnmountMutation | MountLayout.

          以及這里是 React 如何檢查行為觸發(fā)的(詳見源碼):

          if ((effect.tag & unmountTag) !== NoHookEffect) {
            // Unmount
          }
          if ((effect.tag & mountTag) !== NoHookEffect) {
            // Mount
          }
          復(fù)制代碼

          源碼節(jié)選 所以,基于我們剛才學(xué)習(xí)的關(guān)于 effect hook 的知識(shí),我們可以實(shí)際操作,從外部向 fiber 插入一些 effect:

          function injectEffect(fiber{
            const lastEffect = fiber.updateQueue.lastEffect

            const destroyEffect = () => {
              console.log('on destroy')
            }

            const createEffect = () => {
              console.log('on create')

              return destroy
            }

            const injectedEffect = {
              tag0b11000000,
              next: lastEffect.next,
              create: createEffect,
              destroy: destroyEffect,
              inputs: [createEffect],
            }

            lastEffect.next = injectedEffect
          }

          const ParentComponent = (
            <ChildComponent ref={injectEffect} />
          )
          復(fù)制代碼

          這就是我對(duì)于useEffect的一點(diǎn)總結(jié),筆者很菜,如果有錯(cuò)誤歡迎指正。
          參考文章:
          juejin.cn/post/684490… blog.csdn.net/sinat_17775…

          關(guān)于本文

          作者:Angus安格斯

          https://juejin.cn/post/6952509261519781918

          最后

          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會(huì)很認(rèn)真的解答喲!
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對(duì)你有幫助,在看」是最大的支持
           》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持


          瀏覽 75
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  操逼观看| 亚洲天堂黄片 | 国产精品久久在线视频 | 亚洲最大视频网站 | 水蜜桃视频网址 |