<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 夠優(yōu)雅嗎?

          共 17154字,需瀏覽 35分鐘

           ·

          2022-11-01 13:41

          時(shí)至今日,React Hooks 已在 React 生態(tài)中大放異彩,席卷了幾乎所有的 React 應(yīng)用。而其又與 Function Component 以及 Fiber 架構(gòu)幾近天作之合,在當(dāng)下,我們好像毫無拒絕它的道理。

          大廠技術(shù)  高級(jí)前端  Node進(jìn)階

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

          回復(fù)1,加入高級(jí)Node交流群

          誠然,Hooks 解決了 React Mixins 這個(gè)老大難的問題,但從它各種奇怪的使用體驗(yàn)上來說,我認(rèn)為現(xiàn)階段的 Hooks 并不是一個(gè)好的抽象。

          紅臉太常見,也來唱個(gè)黑臉,本文將站在一個(gè)「挑刺兒」的視角,聊聊我眼中的 React Hooks ~

          「奇怪的」規(guī)矩

          React 官方制定了一些 Hooks 書寫規(guī)范用來規(guī)避 Bug,但這也恰恰暴露了它存在的問題。

          命名

          Hooks 并非普通函數(shù),我們一般用use開頭命名,以便與其他函數(shù)區(qū)分。

          但相應(yīng)地,這也破壞了函數(shù)命名的語義。固定的use前綴使 Hooks 很難命名,你既為useGetState這樣的命名感到困惑,也無法理解useTitle到底是怎么個(gè)use法兒。

          相比較而言,以_開頭的私有成員變量和$尾綴的流,則沒有類似的困擾。當(dāng)然,這只是使用習(xí)慣上的差異,并不是什么大問題。

          調(diào)用時(shí)序

          在使用useState的時(shí)候,你有沒有過這樣的疑惑:useState雖然每次render() 都會(huì)調(diào)用,但卻可以為我保持住 State,如果我寫了很多個(gè),那它怎么知道我想要的是什么 State 呢?

          const [name, setName] = useState('xiaoming')
          console.log('some sentences')
          const [age, setAge] = useState(18)

          兩次useState只有參數(shù)上的區(qū)別,而且也沒有語義上的區(qū)分(我們僅僅是給返回值賦予了語義),站在 useState的視角,React 怎么知道我什么時(shí)候想要name而什么時(shí)候又想要age的呢?

          以上面的示例代碼來看,為什么第 1 行的useState會(huì)返回字符串name,而第 3 行會(huì)返回?cái)?shù)字age呢? 畢竟看起來,我們只是「平平無奇」地調(diào)用了兩次useState而已。

          答案是「時(shí)序」。useState的調(diào)用時(shí)序決定了結(jié)果,也就是,第一次的useState「保存」了 name 的狀態(tài),而第二次「保存」了age的狀態(tài)。

          // Class Component 中通過字面量聲明與更新 State,無一致性問題
          this.setState({
            name: 'xiaoming',  // State 字面量 `name`,`age`
            age: 18,
          })

          React 簡單粗暴地用「時(shí)序」決定了這一切(背后的數(shù)據(jù)結(jié)構(gòu)是鏈表),這也導(dǎo)致 Hooks 對調(diào)用時(shí)序的嚴(yán)格要求。也就是要避免所有的分支結(jié)構(gòu),不能讓 Hooks 「時(shí)有時(shí)無」。

          // ? 典型錯(cuò)誤
          if (some) {
            const [name, setName] = useState('xiaoming')
          }

          這種要求完全依賴開發(fā)者的經(jīng)驗(yàn)抑或是 Lint,而站在一般第三方 Lib 的角度看,這種要求調(diào)用時(shí)序的 API 設(shè)計(jì)是極為罕見的,非常反直覺。

          最理想的 API 封裝應(yīng)當(dāng)是給開發(fā)者認(rèn)知負(fù)擔(dān)最小的。好比封裝一個(gè)純函數(shù)add(),不論開發(fā)者是在什么環(huán)境調(diào)用、在多么深的層級(jí)調(diào)用、用什么樣的調(diào)用時(shí)序,只要傳入的參數(shù)符合要求,它就可以正常運(yùn)作,簡單而純粹。

          function add(a: number, b: number) {
            return a + b
          }

          function outer() {
            const m = 123;
            setTimeout(() => {
              request('xx').then((n) => {
                const result = add(m, n)         // 符合直覺的調(diào)用:無環(huán)境要求
              })
            }, 1e3)
          }

          可以說「React 確實(shí)沒辦法讓 Hooks 不要求環(huán)境」,但也不能否認(rèn)這種方式的怪異。

          類似的情況在redux-saga里也有,開發(fā)者很容易寫出下面這種「符合直覺」的代碼,而且怎么也「看」不出有問題。

          import { call } from 'redux-saga/effects'

          functionfetch() {
            setTimeout(function* () {
              const user = yield call(fetchUser)
              console.log('hi', user)                  // 不會(huì)執(zhí)行到這兒
            }, 1e3)
          }

          yield call()在 Generator 里調(diào)用,看起來真的很「合理」。但實(shí)際上,function*需要 Generator 執(zhí)行環(huán)境,而call也需要redux-saga的執(zhí)行環(huán)境。雙重要求之下,實(shí)例代碼自然無法正常運(yùn)行。

          useRef 的「排除萬難」

          從本義上來說,useRef其實(shí)是 Class Component 時(shí)代React.createRef()的等價(jià)替代。

          官方文檔[1]中最開始的示例代碼可以佐證這一點(diǎn)(如下所示,有刪減):

          function TextInputWithFocusButton() {
            const inputEl = useRef(null);
            return (
              <input ref={inputEl} type="text" />
            );
          }

          但因?yàn)槠鋵?shí)現(xiàn)特殊,也常作他用。

          React Hooks 源碼中,useRef僅在 Mount 時(shí)期初始化對象,而 Update 時(shí)期返回 Mount 時(shí)期的結(jié)果(memoizedState)。這意味著一次完整的生命周期中,useRef 保留的引用始終不會(huì)改變。

          而這一特點(diǎn)卻讓它成為了 Hooks 閉包救星

          「遇事不決,useRef !」(useRef存在許多濫用的情況,本文不多贅述)

          每一個(gè) Function 的執(zhí)行都有與之相應(yīng)的 Scope,對于面向?qū)ο髞碚f,this引用即是連接了所有 Scope 的 Context(當(dāng)然前提是在同一個(gè) Class 下)。

          class Runner {
            runCount = 0

            run() {
              console.log('run')
              this.runCount += 1
            }

            xrun() {
              this.run()
              this.run()
              this.run()
            }

            output() {
              this.xrun()
              // 即便是「間接調(diào)用」`run`,這里「仍然」能獲取 `run` 的執(zhí)行信息
              console.log(this.runCount) // 3
            }
          }

          在 React Hooks 中,每一次的 Render 由彼時(shí)的 State 決定,Render 完成 Context 即刷新。優(yōu)雅的 UI 渲染,干凈而利落。

          useRef多少違背了設(shè)計(jì)者的初衷, useRef可以橫跨多次 Render 生成的 Scope,它能保留下已執(zhí)行的渲染邏輯,卻也能使已渲染的 Context 得不到釋放,威力無窮卻也作惡多端。

          而如果說 this引用是面向?qū)ο笾凶钪饕母弊饔茫敲?useRef亦同。從這一點(diǎn)來說,擁有 useRef寫法的 Function Component 注定難以達(dá)成「函數(shù)式」。

          小心使用

          有缺陷的生命周期

          構(gòu)造時(shí)

          Class Component 和 Function Component 之間還有一個(gè)很大的「Bug」,Class Component 僅實(shí)例化一次后續(xù)僅執(zhí)行 render() ,而 Function Component 卻是在不斷執(zhí)行自身。

          這導(dǎo)致 Function Component 相較 Class Component 實(shí)際缺失了對應(yīng)的constructor構(gòu)造時(shí)。當(dāng)然如果你有辦法只讓 Function 里的某段邏輯只執(zhí)行一遍,倒是也可以模擬出constructor。

          // 比如使用 useRef 來構(gòu)造
          function useConstructor(callback) {
            const init = useRef(true)
            if (init.current) {
              callback()
              init.current = false
            }
          }

          生命周期而言, constructor 不能類同 useEffect ,如果實(shí)際節(jié)點(diǎn)渲染時(shí)長較長,二者會(huì)有很大時(shí)差。

          也就是說,Class Component 和 Function Component 的生命周期 API 并不能完全一一對應(yīng),這是一個(gè)很引發(fā)錯(cuò)誤的地方。

          設(shè)計(jì)混亂的 useEffect

          在了解useEffect的基本用法后,加上對其字面意思的理解(監(jiān)聽副作用),你會(huì)誤以為它等同于 Watcher。

          useEffect(() => {
            // watch 到 `a` 的變化
            doSomething4A()
          }, [a])

          但很快你就會(huì)發(fā)現(xiàn)不對勁,如果變量a未能觸發(fā) re-render,監(jiān)聽并不會(huì)生效。也就是說,實(shí)際還是應(yīng)該用于監(jiān)聽 State 的變化,即useStateEffect。但參數(shù)deps卻并未限制僅輸入 State。如果不是為了某些特殊動(dòng)作,很難不讓人認(rèn)為是設(shè)計(jì)缺陷。

          const [a] = useState(0)
          const [b] = useState(0)

          useEffect(() => {
              // 假定此處為 `a` 的監(jiān)聽
          }, [a])

          useEffect(() => {
              // 假定此處為 `b` 的監(jiān)聽
            // 實(shí)際即便 `b` 未變化也并未監(jiān)聽 `a`,但此處仍然因?yàn)闀?huì)因?yàn)?nbsp;`a` 變化而執(zhí)行
          }, [b, Date.now()])        // 因?yàn)?nbsp;Date.now() 每次都是新的值

          useStateEffect的理解也并不到位,因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(255, 93, 108);">useEffect實(shí)際還負(fù)責(zé)了 Mount 的監(jiān)聽,你需要用「空依賴」來區(qū)分 Mount 和 Update。

          useEffect(onMount, [])

          單一 API 支持的能力越多,也意味著其設(shè)計(jì)越混亂。復(fù)雜的功能不僅考驗(yàn)開發(fā)者的記憶,也難于理解,更容易因錯(cuò)誤理解而引發(fā)故障。

          useCallback

          性能問題?

          在 Class Component 中我們常常把函數(shù)綁在this上,保持其的唯一引用,以減少子組件不必要的重渲染。

          class App {
            constructor() {
              // 方法一
              this.onClick = this.onClick.bind(this)
            }
            onClick() {
              console.log('I am `onClick`')
            }

            // 方法二
            onChange = () => {}

            render() {
              return (
                <Sub onClick={this.onClick} onChange={this.onChange} />
              )
            }
          }

          在 Function Component 中對應(yīng)的方案即 useCallback

          // ? 有效優(yōu)化
          function App() {
            const onClick = useCallback(() => {
              console.log('I am `onClick`')
            }, [])

            return (<Sub onClick={onClick} />)
          }

          // ? 錯(cuò)誤示范,`onClick` 在每次 Render 中都是全新的,<Sub> 會(huì)因此重渲染
          function App() {
            // ... some states
            const onClick = () => {
              console.log('I am `onClick`')
            }

            return (<Sub onClick={onClick} />)
          }

          useCallback可以在多次重渲染中仍然保持函數(shù)的引用, 第2行的onClick也始終是同一個(gè),從而避免了子組件<Sub>的重渲染。

          useCallback源碼其實(shí)也很簡單:

          Mount 時(shí)期僅保存 callback 及其依賴數(shù)組

          Update 時(shí)期判斷如果依賴數(shù)組一致,則返回上次的 callback

          順便再看看useMemo 的實(shí)現(xiàn),其實(shí)它與 useCallback 的區(qū)別僅僅是多一步 Invoke:

          無限套娃?[2]

          相比較未使用useCallback帶來的性能問題,真正麻煩的是useCallback帶來的引用依賴問題。

          // 當(dāng)你決定引入 `useCallback` 來解決重復(fù)渲染問題
          function App() {
            // 請求 A 所需要的參數(shù)
            const [a1, setA1] = useState('')
            const [a2, setA2] = useState('')
            // 請求 B 所需要的參數(shù)
            const [b1, setB1] = useState('')
            const [b2, setB2] = useState('')

            // 請求 A,并處理返回結(jié)果
            const reqA = useCallback(() => {
              requestA(a1, a2)
            }, [a1, a2])

            // 請求 A、B,并處理返回結(jié)果
            const reqB = useCallback(() => {
              reqA()                                           // `reqA`的引用始終是最開始的那個(gè),
              requestB(b1, b2)                       // 當(dāng)`a1`,`a2`變化后`reqB`中的`reqA`其實(shí)是過時(shí)的。
            }, [b1, b2])                   // 當(dāng)然,把`reqA`加到`reqB`的依賴數(shù)組里不就好了?
                                                                       // 但你在調(diào)用`reqA`這個(gè)函數(shù)的時(shí)候,
                                                                           // 你怎么知道「應(yīng)該」要加到依賴數(shù)組里呢?
            return (
              <>
                <Comp onClick={reqA}></Comp>
                <Comp onClick={reqB}></Comp>
              </>
            )
          }

          從上面示例可以看到,當(dāng)useCallback之前存在依賴關(guān)系時(shí),它們的引用維護(hù)也變得復(fù)雜。調(diào)用某個(gè)函數(shù)時(shí)要小心翼翼,你需要考慮它有沒有引用過時(shí)的問題,如有遺漏又沒有將其加入依賴數(shù)組,就會(huì)產(chǎn)生 Bug。

          Use-Universal

          Hooks 百花齊放的時(shí)期誕生了許多工具庫,僅ahooks 就有 62 個(gè)自定義 Hooks,真可謂「萬物皆可 use」~ 真的有必要封裝這么多 Hooks 嗎?又或者說我們真的需要這么多 Hooks 嗎?

          合理封裝?

          盡管在 React 文檔中,官方也建議封裝自定義 Hooks 提高邏輯的復(fù)用性。但我覺得這也要看情況,并不是所有的生命周期都有必要封裝成 Hooks。

          // 1. 封裝前
          function App() {
            useEffect(() => {           // `useEffect` 參數(shù)不能是 async function
              (async () => {
                await Promise.all([fetchA(), fetchB()])
                await postC()
              })()
            }, [])
            return (<div>123</div>)
          }
          // --------------------------------------------------

          // 2. 自定義 Hooks
          function App() {
            useABC()
            return (<div>123</div>)
          }

          function useABC() {
            useEffect(() => {
              (async () => {
                await Promise.all([fetchA(), fetchB()])
                await postC()
              })()
            }, [])
          }
          // --------------------------------------------------
          // 3. 傳統(tǒng)封裝
          function App() {
            useEffect(() => {
              requestABC()
            }, [])
            return (<div>123</div>)
          }

          async function requestABC() {
            await Promise.all([fetchA(), fetchB()])
            await postC()
          }

          在上面的代碼中,對生命周期中的邏輯封裝為 HookuseABC反而使其耦合了生命周期回調(diào),降低了復(fù)用性。即便我們的封裝中不包含任何 Hooks,在調(diào)用時(shí)也僅僅是包一層useEffect而已,不算費(fèi)事,而且讓這段邏輯也可以在 Hooks 以外的地方使用。

          如果自定義 Hooks 中使用到的useEffectuseState總次數(shù)不超過 2 次,真的應(yīng)該想一想這個(gè) Hook 的必要性了,是否可以不封裝。

          簡單來說,Hook 要么「掛靠生命周期」要么「處理 State」,否則就沒必要。

          重復(fù)調(diào)用

          Hook 調(diào)用很「反直覺」的就是它會(huì)隨重渲染而不停調(diào)用,這要求 Hook 開發(fā)者要對這種反復(fù)調(diào)用有一定預(yù)期。

          正如上文示例,對請求的封裝,很容易依賴useEffect,畢竟掛靠了生命周期就能確定請求不會(huì)反復(fù)調(diào)

          function useFetchUser(userInfo) {
            const [user, setUser] = useState(null)
            useEffect(() => {
              fetch(userInfo).then(setUser)
            }, [])

            return user
          }

          但,useEffect真的合適嗎?這個(gè)時(shí)機(jī)如果是DidMount,那執(zhí)行的時(shí)機(jī)還是比較晚的,畢竟如果渲染結(jié)構(gòu)復(fù)雜、層級(jí)過深,DidMount就會(huì)很遲。

          比如,ul中渲染了 2000 個(gè) li

          function App() {
            const start = Date.now()

            useEffect(() => {
              console.log('elapsed:', Date.now() - start, 'ms')
            }, [])

            return (
              <ul>
                {Array.from({ length: 2e3 }).map((_, i) => (<li key={i}>{i}</li>))}
              </ul>
            )
          }

          // output
          // elapsed: 242 ms

          那不掛靠生命周期,而使用狀態(tài)驅(qū)動(dòng)呢?似乎是個(gè)好主意,如果狀態(tài)有變更,就重新獲取數(shù)據(jù),好像很合理。

          useEffect(() => {
            fetch(userInfo).then(setUser)
          }, [userInfo])                   // 請求參數(shù)變化時(shí),重新獲取數(shù)據(jù)

          但初次執(zhí)行時(shí)機(jī)仍然不理想,還是在DidMount。

          let start = 0
          let f = false

          function App() {
            const [id, setId] = useState('123')
            const renderStart = Date.now()

            useEffect(() => {
              const now = Date.now()
              console.log('elapsed from start:', now - start, 'ms')
              console.log('elapsed from render:', now - renderStart, 'ms')
            }, [id])                       // 此處監(jiān)聽 `id` 的變化
            if (!f) {
              f = true
              start = Date.now()
              setTimeout(() => {
                setId('456')
              }, 10)
            }

            return null
          }

          // output
          // elapsed from start: 57 ms
          // elapsed from render: 57 ms
          // elapsed from start: 67 ms
          // elapsed from render: 1 ms

          這也是上文為什么說useEffect設(shè)計(jì)混亂,你把它當(dāng)做 State Watcher 的時(shí)候,其實(shí)它還暗含了「初次執(zhí)行在DidMount」的邏輯。從字面意思Effect來看,這個(gè)邏輯才是副作用吧。。。

          狀態(tài)驅(qū)動(dòng)的封裝除了調(diào)用時(shí)機(jī)以外,其實(shí)還有別的問題:

          function App() {
            const user = useFetchUser({          // 乍一看似乎沒什么問題
              name: 'zhang',
              age: 20,
            })

            return (<div>{user?.name}</div>)
          }

          實(shí)際上,組件重渲染會(huì)導(dǎo)致請求入?yún)⒅匦掠?jì)算 -> 字面量聲明的對象每次都是全新的 -> useFetchUser因此不停請求 -> 請求變更了 Hook 內(nèi)的 State user -> 外層組件<App>重渲染。

          這是一個(gè)死循環(huán)!

          當(dāng)然,你可以用Immutable來解決同一參數(shù)重復(fù)請求的問題。

          useEffect(() => {
            // xxxx
          }, [ Immutable.Map(userInfo) ])

          但總的看來,封裝 Hooks 遠(yuǎn)遠(yuǎn)不止是變更了你代碼的組織形式而已。比如做數(shù)據(jù)請求,你可能因此而走上狀態(tài)驅(qū)動(dòng)的道路,同時(shí),你也要解決狀態(tài)驅(qū)動(dòng)隨之帶來的新麻煩。

          為了 Mixin ?

          其實(shí),Mixin 的能力也并非 Hooks 一家獨(dú)占,我們完全可以使用 Decorator 封裝一套 Mixin 機(jī)制。也就是說, Hooks 不能依仗 Mixin 能力去力排眾議。

          const HelloMixin = {
            componentDidMount() {
              console.log('Hello,')
            }
          }

          function mixin(Mixin) {
            return function (constructor) {
              return class extends constructor {
                componentDidMount() {
                  Mixin.componentDidMount()
                  super.componentDidMount()
                }
              }
            }
          }

          @mixin(HelloMixin)
          class Test extends React.PureComponent {
            componentDidMount() {
              console.log('I am Test')
            }

            render() {
              return null
            }
          }

          render(<Test />) // output: Hello, \n I am Test

          不過 Hooks 的組裝能力更強(qiáng)一些,也容易嵌套使用。但需要警惕層數(shù)較深的 Hooks,很可能在某個(gè)你不知道的角落就潛伏著一個(gè)有隱患的useEffect。

          小結(jié)

          • 本文沒有鼓吹 Class Component 拒絕使用 React Hooks 的意思,反而是希望通過細(xì)致地比對二者,從而更深入理解 Hooks。
          • React Hooks 的各種奇怪之處,也正是潛在癥結(jié)之所在。
          • 在 Hooks 之前,F(xiàn)unction Component 都是 Stateless 的,小巧、可靠但功能有限。Hooks 為 Function Component 賦予了 State 能力并提供了生命周期,使 Function Component 的大規(guī)模使用成為了可能。
          • Hooks 的「優(yōu)雅」來自向函數(shù)式的致敬,但useRef 的濫用讓 Hooks 離「優(yōu)雅」相去甚遠(yuǎn)。
          • 大規(guī)模實(shí)踐 React Hooks 仍然有諸多問題,無論是從語義理解抑或是封裝的必要性。
          • 創(chuàng)新不易,期待 React 官方之后會(huì)有更好的設(shè)計(jì)吧。

          原文鏈接:https://zhuanlan.zhihu.com/p/455317250?utm_source=ZHShareTargetIDMore&utm_medium=social&utm_oi=28389876432896

          參考資料

          [1]

          官方文檔: https://link.zhihu.com/?target=https%3A//reactjs.org/docs/hooks-reference.html%23useref

          [2]

          ?: https://link.zhihu.com/?target=https%3A//www.alt-codes.net/check-mark-symbols.php

          Node 社群



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



          如果你覺得這篇內(nèi)容對你有幫助,我想請你幫我2個(gè)小忙:

          1. 點(diǎn)個(gè)「在看」,讓更多人也能看到這篇文章
          2. 訂閱官方博客 www.inode.club 讓我們一起成長

          點(diǎn)贊和在看就是最大的支持??

          瀏覽 133
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(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>
                  日韩性爱一区 | 在线观看日韩 | 亚洲色播免费视频 | 小泽与黑人| 搞逼网址 |