<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 進(jìn)階必備:從函數(shù)式組件看 Hooks 設(shè)計(jì)原理(收藏?。?/h1>

          共 15992字,需瀏覽 32分鐘

           ·

          2021-06-19 13:53


          從函數(shù)式說起

          React 在現(xiàn)有的三大主流框架中是非?!昂瘮?shù)式”的語言,小到 setState,render 函數(shù)的設(shè)計(jì),大到函數(shù)組件,周邊組件 Redux 等,都蘊(yùn)含了一定的函數(shù)式風(fēng)格。因此要了解 React,我們就需要了解一定的函數(shù)式編程。

          函數(shù)式編程作為聲明式編程的一種形式,與命令式編程相對,來源于范疇論,最早是為了解決數(shù)學(xué)問題而誕生。范疇論認(rèn)為,同一個(gè)范疇的所有成員,就是不同狀態(tài)的"變形",通過態(tài)射,一個(gè)成員可以變形成另一個(gè)成員。

          我們可以把物件理解為集合,態(tài)射理解為函數(shù),通過函數(shù),來規(guī)定范疇中成員之間的關(guān)系,函數(shù)扮演管道的角色,一個(gè)值進(jìn)去,一個(gè)新值出來,沒有其他副作用,也就是所謂的 y = f(x)。

          函數(shù)式風(fēng)格包含了多種特性,典型的如函數(shù)一等公民、純函數(shù)、副作用、柯里化、組合等,這里我們主要以基礎(chǔ)的純函數(shù)和副作用做進(jìn)一步解釋。

          純函數(shù)

          純函數(shù) ——輸入輸出數(shù)據(jù)流全是顯式的。

          顯式的意思是,函數(shù)與外界交換數(shù)據(jù)只有一個(gè)唯一渠道——參數(shù)和返回值;函數(shù)從函數(shù)外部接受的所有輸入信息都通過參數(shù)傳遞到該函數(shù)內(nèi)部;函數(shù)輸出到函數(shù)外部的所有信息都通過返回值傳遞到該函數(shù)外部。

          相同的輸入,永遠(yuǎn)得到相同的輸出,而且沒有任何副作用,與環(huán)境變量無關(guān),可以在任意地方調(diào)用。

           //splice是不純的函數(shù)  
           let arr = [1,2,3,4,5];  
           arr.splice(0,3);  //[1,2,3]   
           arr.splice(0,3);  //[4,5]    
           
           //slice是純函數(shù)   
           arr = [1,2,3,4,5];  
           arr.slice(0,3);  //[1,2,3]   
           arr.slice(0,3);  //[1,2,3] 

          副作用

          在計(jì)算機(jī)科學(xué)中,函數(shù)副作用指當(dāng)調(diào)用函數(shù)時(shí),除了返回函數(shù)值之外,還對主調(diào)用函數(shù)產(chǎn)生附加的影響。例如修改全局變量(函數(shù)外的變量),修改參數(shù)或改變外部存儲(chǔ)。

          典型的副作用:

          • 發(fā)送一個(gè)http請求
          • new Date() / Math.random();
          • console.log / IO
          • DOM查詢 (外部數(shù)據(jù))

          然而,只有純函數(shù)而無副作用的程序,在程序運(yùn)行完畢后,不留一絲痕跡,僅僅只是空耗 CPU 而已,因此,副作用是必要的。在函數(shù)式語言中,為了保證函數(shù)的盡可能純粹性,副作用使用函子進(jìn)行統(tǒng)一管理。這里我們不去深究函子的原理,我們需要記住的是:盡可能保持函數(shù)的純粹性,將副作用收攏統(tǒng)一管理。

          追溯歷史 - Hook 誕生背景

          React 組件

          React 組件根據(jù)書寫形式分為函數(shù)組件和類組件。

          class ComponentA extends React.Component {

              constructor(props) {

                  super(props);

                  this.state = { displayContent'Hello World' }

              }

              render() {

                  return <div>{this.state.displayContent}</div>

              }

          }
          function ComponentFunctionA = (props) => <div>{props.displayContent}</div>

          函數(shù)組件定位為展示組件,自身無狀態(tài),無生命周期。

          類組件定位為容器組件,自身管理狀態(tài)與生命周期。

          問題

          函數(shù)組件想管理狀態(tài)、生命周期

          隨著需求變更,在未作出良好設(shè)計(jì)的情況下。常會(huì)出現(xiàn)本是函數(shù)的組件處于功能內(nèi)聚性等因素想管理狀態(tài)的情況。

          解決方案:

          • 改為類組件
          • 提升狀態(tài)至上層容器

          然而,改為類組件需要一定的人力成本;而數(shù)據(jù)提升至上層容器,既有可能會(huì)增加 React 組件層級結(jié)構(gòu),又喪失了組件功能的內(nèi)聚性,都不能認(rèn)為是良好的解決方案。

          類組件生命周期帶來邏輯分離

          class DemoA extends React.PureComponent {

              constructor(props) {

                  super(props);

                  this.listener = () => {/* do something */};

              }

              componentDidMount() {

                  document.addEventListener('click'this.listener);

              }

              componentWillUnmount() {

                  document.removeEventListener('click'this.listener);

              }

          }

          這里只是一個(gè)簡單的例子,展示了由于生命周期的存在,需要我們將一個(gè)事件的監(jiān)聽和取消邏輯分別置于不同生命周期內(nèi)。一旦邏輯復(fù)雜,極易導(dǎo)致遺漏這類對事件,數(shù)據(jù)的清理工作,從而造成內(nèi)存泄漏甚至邏輯錯(cuò)誤。

          邏輯抽象復(fù)用

          方案優(yōu)點(diǎn)缺點(diǎn)
          Mixin使用簡單方便 靈活維護(hù)性差容易 mixin 覆蓋
          HOC解決了class不能使用 mixin 問題擁有額外組件層級屬性來源不定,容易屬性覆蓋
          Render Props明確來源,解決了屬性覆蓋問題額外組件層級可讀性差
          Hook消除額外組件層級可讀性高,適用邏輯抽象數(shù)據(jù)來源輸出明確依賴管理閉包問題

          class 本身的問題,比如不能很好的壓縮,在熱重載時(shí)會(huì)出現(xiàn)不穩(wěn)定的情況

          解決問題 - Hook 設(shè)計(jì)

          通過 Hook 方式,React 為函數(shù)組件引入了可管理副作用的 useState、useEffect、useMemo 等 Hook,以保證函數(shù)盡可能純粹的基礎(chǔ)上,有效解決了上述幾個(gè)之前組件開發(fā)的痛點(diǎn)。

          這篇文章中以 useState 的數(shù)據(jù)存儲(chǔ)和 useEffect 的副作用管理為例子展開。

          由于函數(shù)組件有著純函數(shù)的特點(diǎn),本身不負(fù)責(zé)數(shù)據(jù)存儲(chǔ)和副作用處理。因此我們首先要解決的問題就是數(shù)據(jù)的存儲(chǔ)和副作用管理。

          useState

          在 JavaScript 中,解決數(shù)據(jù)存儲(chǔ)主要有以下幾個(gè)方案。

          • class 成員變量
          • 全局狀態(tài)
          • Dom
          • localStorage 等本地存儲(chǔ)方案
          • 閉包

          其中,類的成員變量是 class 采用的數(shù)據(jù)存儲(chǔ)方案;

          考慮到盡可能規(guī)避副作用的影響,我們排除全局狀態(tài)、本地存儲(chǔ)和 DOM 的方案;相對而言,閉包可以滿足我們數(shù)據(jù)存儲(chǔ)和可靠性的要求。

          DEMO 演化

          參考 React.useState 的使用方案,應(yīng)該返回一個(gè) state 數(shù)據(jù)字段和一個(gè)更新 state 的 dispatch。

          function Demo ({

              const [count, setCount] = useState(0)

              return <div onClick={() => { setCount(count++); }}>{count}<div>

          }

          根據(jù)閉包的定義和使用的返回值,我們可以很輕易的定義出以下方法:

          var useState = (initState) => {

              let data = initState;

              const dispatch = (newData) => {

                  data = newData;

              }

              return [data, dispatch];

          }

          在初始化階段,我們可以驗(yàn)證上面的基礎(chǔ) useState 可以運(yùn)行。然而在每次渲染的過程中,函數(shù)都會(huì)被重新調(diào)用而重新初始化,這并不是我們期望的。因此我們需要一個(gè)數(shù)據(jù)結(jié)構(gòu)對每次執(zhí)行的 state 進(jìn)行存儲(chǔ),同時(shí)還需要區(qū)分初始化和更新狀態(tài)的不同執(zhí)行方式。

          type Hook {

              memorizedState: any;

          }

          var useState = (initState) => {

              // 根據(jù)不同生命周期判斷

              if (mounted) {

                  mountedState(initState);

              }

              if (updated) {

                  updatedState(initState);

              }

          }

          var mountedState = (initState) => {

              const hook = createNewHook();

              // 初始化渲染

              hook.memoizedState = initalState;

              return [hook.memorizedState, dispatchAction]

          }



          var createNewHook = () => {

              return {

                  memorizedStatenull

              }

          }



          function dispatchAction(action){

            // 使用數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)所有的更新行為,以便在 rerender 流程中計(jì)算最新的狀態(tài)值

            storeUpdateActions(action);

            // 執(zhí)行 fiber 的渲染

            scheduleWork();

          }



          // 第一次之后每一次執(zhí)行 useState 時(shí)實(shí)際調(diào)用的方法

          function updateState(initialState){

              // 獲取當(dāng)前正在工作中的 hook

              const hook = updateWorkInProgressHook();

              // 根據(jù) dispatchAction 中存儲(chǔ)的更新行為計(jì)算出新的狀態(tài)值,并返回給組件

              updateMemorizedState();

              return [hook.memoizedState, dispatchAction];

          }

          到這里我們有兩個(gè)問題

          • 對于同一個(gè) state,在 mounted 和 updated 的不同狀態(tài)下,hook 是如何共享的
          • dispatchAction 中的 storeUpdateActions 和 updateState 中的 updateMemorizedState 是如何運(yùn)作的

          針對第一個(gè)問題,對于一個(gè)組件而言,Hook 是相對于組件存在的。因此,React 組件存儲(chǔ)的 ReactNode 十分適合該場景,在當(dāng)前版本下,我們將其掛載于 FiberNode 節(jié)點(diǎn)下。

          type FiberNode {

              memorizedState: any;

          }

          而針對第二個(gè)問題, 需要我們考慮一些復(fù)雜場景問題。

          在我們實(shí)際場景中,普遍存在著一個(gè)更新周期中多次調(diào)用的 re-render 行為

          以一個(gè)例子描述:

          function Demo ({

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

              return <div onClick={() => {

                  setCount(count++);

                  setCount(count++)

                  setCount(count++)

              }}>{count}<div>

          }

          實(shí)際上組件不會(huì)渲染3次,而是根據(jù)最后的狀態(tài)渲染。這意味著在調(diào)用 dispatch 更新的時(shí)候,我們并不是直接進(jìn)行更新邏輯,而是將其存儲(chǔ)進(jìn)行update時(shí)統(tǒng)一的調(diào)度更新,根據(jù)執(zhí)行的有序性,我們采用隊(duì)列存儲(chǔ)一個(gè) hook 的多次調(diào)用。

          type Queue {

              last: Update,

              dispatch: any,

              lastRenderedState: any

          }

          type Update {

              action: any,

              next: Update

          }

          type Hook {

              memorizedState: any,

              queue: Queue;

          }

          function mountState(initState{

              const hook = mountWorkInProgressHook();

              hook.memorizedState = initState;



              const queue = (hook.queue = {

                  lastnull,

                  dispatchnull,

                  lastRenderedStatenull

              });

              // 閉包綁定 queue,實(shí)現(xiàn)共享

              const dispatch = dispatchAction.bind(null, queue);

              queue.dispatch = dispatch;

              return [hook.memorizedState, dispatch]

          }

          function dispatchAction(queue, action{

              const update = {

                  action,

                  nextnull,

              };

              // 處理隊(duì)列更新

              let last = queue.last;

              if (last === null) {

                  update.next = update;

              } else {

                  // ... 更新循環(huán)鏈表

              }

              // 執(zhí)行 fiber 的渲染

              scheduleWork();

          }

          function updateState(initialState){

              // 獲取當(dāng)前正在工作中的 hook 

              const hook = updateWorkInProgressHook();

              // 根據(jù) dispatchAction 中存儲(chǔ)的更新行為計(jì)算出新的狀態(tài)值,并返回給組件 

              (function doReducerWork()

                  let newState = null

                  do

                      // 循環(huán)鏈表,執(zhí)行每一次更新 

                  }while(...) 

                  hook.memoizedState = newState; 

              })(); 

              return [hook.memoizedState, hook.queue.dispatch]; 

          }

          此外,在真實(shí)的應(yīng)用場景中,我們會(huì)根據(jù)邏輯進(jìn)行狀態(tài)分割。需要在一個(gè)組件內(nèi)多次使用一個(gè) Hook,因此需要記錄所有使用的 Hook 信息。這方面,與之前存儲(chǔ)同一組更新相同的 Hook 多次調(diào)用相同,可采用鏈表形式進(jìn)行存儲(chǔ)。

          type Hook = {

              memoizedState: any,                     // 上一次完整更新之后的最終狀態(tài)值

              queue: UpdateQueue<any, any> | null,    // 更新隊(duì)列

              next: any                               // 下一個(gè) hook

          }

          這里我們可以看一個(gè)例子:

          const Demo = () => {

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

              const [time, setTime] = useState(Date.now());



              return <div onClick={() => {

                  setCount(count++);

                  setTime(Date.now());

              }}>{count}-{time}</div>


          }

          在 ReactNode 中存儲(chǔ)的節(jié)點(diǎn)形式:

          const fiber = {

              //...

              memoizedState: {

                  memoizedState0

                  queue: {

                      last: {

                          action1

                      },  

                      dispatch: dispatch,

                      lastRenderedState0

                  },

                  next: {

                      memoizedState1603594106044,

                      queue: {

                          // ...

                      },

                      nextnull

                  }

              },

              //...

          }

          整個(gè)鏈表在 mounted 的時(shí)候構(gòu)建,在 update 時(shí)按照順序執(zhí)行。因此不能在條件循環(huán)等場景下使用。

          useEffect

          有了 useState 設(shè)計(jì)經(jīng)驗(yàn),useEffect 可以同比借鑒。在mount時(shí)創(chuàng)建一個(gè) hook 對象,新建一個(gè) effectQueue,以單向鏈表的方式存儲(chǔ)每一個(gè) effect,將 effectQueue 綁定在 fiberNode 上,并在完成渲染之后依次執(zhí)行該隊(duì)列中存儲(chǔ)的 effect 函數(shù)。

          type EffectQueue{

            lastEffect: Effect

          }

          type FiberNode{

            memoizedState: any,  // 用來存放某個(gè)組件內(nèi)所有的 Hook 狀態(tài)

            updateQueue: EffectQueue  

          }

          type Effect {

            create: any;

            destory: any;

            deps: Array;

            next: any;

          }

          與 useState 不同的一點(diǎn)是,useEffect 擁有一個(gè) deps 依賴數(shù)組。當(dāng)依賴數(shù)組變更的時(shí)候,一個(gè)新的副作用函數(shù)會(huì)被追加至鏈尾。

          function useEffect(fn, dependencies{

            if (mounted) {

              mounteEffect(fn, dependencies)        

            }

            if (updated) {

              updateEffect(fn, dependencies)

            }

          }



          function mountEffect(fn, deps{

            const hook = mountWorkInProgressHook();

            hook.memorizedState = pushEffect(xxxTag, fn, deps)

          }



          function updateEffect(fn, deps{

            const hook = updateWorkInProgressHook();

            const nextDeps = deps === undefined ? null : deps;



            // 依賴改變則觸發(fā)銷毀重置

            if(currentHook!==null){

              const prevEffect = currentHook.memoizedState;

              const destroy = prevEffect.destroy;

              if (nextDeps!== null){

                  if(areHookInputsEqual(deps, prevEffect.deps)){

                     pushEffect(xxxTag, create, destroy, deps);

                     return;

                  }

                }  

             } 

             hook.memoizedState = pushEffect(xxxTag, create, deps);

          }



          function pushEffect(tag, create, destroy, deps{

            const effect = {

              create,

              destory,

              deps,

              nextnull

            };

            // 構(gòu)建 effect 隊(duì)列

            const updateQueue = fiberNode.updateQueue = fiberNode.updateQueue || newUpdateQueue();

            if (updateQueue.lastEffect) {

              const firstEffect = lastEffect.next;

              lastEffect.next = effect;

              effect.next = firstEffect;

              updateQueue.lastEffect = effect;

            } else {

              updateQueue.lastEffect = effect.next = effect;

            }

            return effect;

          }

          我們可以看到,在 useEffect 階段,實(shí)際并沒有對 effect 進(jìn)行執(zhí)行,僅僅是構(gòu)建一條 effect 執(zhí)行存儲(chǔ)鏈表,而真正 create 和 destroy 的執(zhí)行,在于 commit 階段,該部分內(nèi)容不在本次分享范圍,感興趣的小伙伴可以自行了解

          相關(guān)鏈接:ReactFiberCommitWork[1]

          總結(jié)

          通過查找定位問題 -> 得出需求 -> 實(shí)現(xiàn)設(shè)計(jì) -> 設(shè)計(jì)演進(jìn)的步驟,我們一步步了解了 Hook 設(shè)計(jì)的初衷和設(shè)計(jì)的一步步完善,在日常工作中,我們也應(yīng)該借鑒這種思維模式,完善自身對于業(yè)務(wù)痛點(diǎn)的認(rèn)識以真正采取相應(yīng)的解決措施

          參考資料

          [1]

          ReactFiberCommitWork: https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberCommitWork.new.js#L375


          最后



          如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個(gè)小忙:

          1. 點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)

          2. 歡迎加我微信「 sherlocked_93 」拉你進(jìn)技術(shù)群,長期交流學(xué)習(xí)...

          3. 關(guān)注公眾號「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。


          點(diǎn)個(gè)在看支持我吧,轉(zhuǎn)發(fā)就更好了



          瀏覽 91
          點(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>
                  国产一区二区三区无码 | 中文天堂在线视频 | 欧美在线观看网址 | 午夜色婷婷 | 国产成人大香蕉在线免费 |