<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】1260- 聊聊我眼中的 React Hooks

          共 10000字,需瀏覽 20分鐘

           ·

          2022-03-16 02:22

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

          誠然,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'

          function*?fetch()?{
          ??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à)替代。

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

          function?TextInputWithFocusButton()?{
          ??const?inputEl?=?useRef(null);
          ??return?(
          ????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)?`a`?變化而執(zhí)行
          },?[b,?Date.now()])????????//?因?yàn)?Date.now()?每次都是新的值

          useStateEffect的理解也并不到位,因?yàn)閡seEffect實(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?(
          ??????
          ????)
          ??}
          }

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

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

          ??return?()
          }

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

          ??return?()
          }

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

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

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

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

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

          無限套娃?

          相比較未使用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?(
          ????<>
          ??????
          ??????
          ????
          ??)
          }

          從上面示例可以看到,當(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?(
          123
          )
          }
          //?--------------------------------------------------


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

          function?useABC()?{
          ??useEffect(()?=>?{
          ????(async?()?=>?{
          ??????await?Promise.all([fetchA(),?fetchB()])
          ??????await?postC()
          ????})()
          ??},?[])
          }
          //?--------------------------------------------------

          //?3.?傳統(tǒng)封裝
          function?App()?{
          ??useEffect(()?=>?{
          ????requestABC()
          ??},?[])
          ??return?(
          123
          )
          }

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

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

          如果自定義 Hooks 中使用到的useEffect和useState總次數(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?(
          ????

            ??????{Array.from({?length:?2e3?}).map((_,?i)?=>?({i}))}
            ????

          ??)
          }

          //?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?(
          {user?.name}
          )
          }

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

          這是一個(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()?//?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ì)吧。



          瀏覽 38
          點(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>
                  日韩videos | 大香蕉伊人综合 | 午夜精品久久久久久久 | 男人天堂亚洲努力打造 | 一级黄色电影A片 |