<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 Hook 必須按順序、不能在條件語句中調(diào)用的枷鎖!

          共 10117字,需瀏覽 21分鐘

           ·

          2021-03-15 11:26

                                                  React 官網(wǎng)介紹了 Hook 的這樣一個限制:

          不要在循環(huán),條件或嵌套函數(shù)中調(diào)用 Hook, 確??偸窃谀愕?React 函數(shù)的最頂層以及任何 return 之前調(diào)用他們。遵守這條規(guī)則,你就能確保 Hook 在每一次渲染中都按照同樣的順序被調(diào)用。這讓 React 能夠在多次的 useStateuseEffect 調(diào)用之間保持 hook 狀態(tài)的正確。(如果你對此感到好奇,我們在下面會有更深入的解釋。)

          這個限制在開發(fā)中也確實會時常影響到我們的開發(fā)體驗,比如函數(shù)組件中出現(xiàn) if 語句提前 return 了,后面又出現(xiàn) Hook 調(diào)用的話,React 官方推的 eslint 規(guī)則也會給出警告。

          function App(){
            if (xxx) {
              return null;
            }

            // ? React Hook "useState" is called conditionally. 
            // React Hooks must be called in the exact same order in every component render.
            useState();
            
            return 'Hello'
          }

          其實是個挺常見的用法,很多時候滿足某個條件了我們就不希望組件繼續(xù)渲染下去。但由于這個限制的存在,我們只能把所有 Hook 調(diào)用提升到函數(shù)的頂部,增加額外開銷。

          由于 React 的源碼太復雜,接下來本文會以原理類似但精簡很多的 Preact[1] 的源碼為切入點來調(diào)試、講解。

          限制的原因

          這個限制并不是 React 團隊憑空造出來的,的確是由于 React Hook 的實現(xiàn)設(shè)計而不得已為之。

          以 Preact 的 Hook 的實現(xiàn)為例,它用數(shù)組和下標來實現(xiàn) Hook 的查找(React 使用鏈表,但是原理類似)。

          // 當前正在運行的組件
          let currentComponent

          // 當前 hook 的全局索引
          let currentIndex

          // 第一次調(diào)用 currentIndex 為 0
          useState('first'

          // 第二次調(diào)用 currentIndex 為 1
          useState('second')

          可以看出,每次 Hook 的調(diào)用都對應一個全局的 index 索引,通過這個索引去當前運行組件 currentComponent 上的 _hooks 數(shù)組中查找保存的值,也就是 Hook 返回的 [state, useState]

          那么假如條件調(diào)用的話,比如第一個 useState 只有 0.5 的概率被調(diào)用:

          // 當前正在運行的組件
          let currentComponent

          // 當前 hook 的全局索引
          let currentIndex

          // 第一次調(diào)用 currentIndex 為 0
          if (Math.random() > 0.5) {
            useState('first')
          }

          // 第二次調(diào)用 currentIndex 為 1
          useState('second')

          在 Preact 第一次渲染組件的時候,假設(shè) Math.random() 返回的隨機值是 0.6,那么第一個 Hook 會被執(zhí)行,此時組件上保存的 _hooks 狀態(tài)是:

          _hooks: [
            { value'first'updatefunction },
            
          { value: 'second'updatefunction },
          ]

          用圖來標識這個查找過程是這樣的:

          第一次渲染

          假設(shè)第二次渲染的時候,Math.random() 返回的隨機值是 0.3,此時只有第二個 useState 被執(zhí)行了,那么它對應的全局 currentIndex 會是 0,這時候去 _hooks[0] 中拿到的確是 first 所對應的狀態(tài),這就會造成渲染混亂。本應該渲染出 second 的地方渲染出了 first

          第二次渲染

          沒錯,本應該值為 second 的 value,莫名其妙的被指向了 first,渲染完全錯誤!

          以這個例子來看:

          export default function App({
            if (Math.random() > 0.5) {
              useState(10000)
            }
            const [value, setValue] = useState(0)

            return (
              <div>
                <button onClick={() => setValue(value + 1)}>+</button>
                {value}
              </div>

            )
          }

          結(jié)果是這樣:

          chaos

          破解限制

          有沒有辦法破解限制呢?

          如果要破解全局索引遞增導致的 bug,那么我們可以考慮換種方式存儲 Hook 狀態(tài)。

          如果不用下標存儲,是否可以考慮用一個全局唯一的 key 來保存 Hook,這樣不是就可以繞過下標導致的混亂了嗎?

          比如 useState 這個 API 改造成這樣:

          export default function App({
            if (Math.random() > 0.5) {
              useState(10000'key1');
            }
            const [value, setValue] = useState(0"key2");

            return (
              <div>
                <button onClick={() => setValue(value + 1)}>+</button>
                {value}
              </div>

            );
          }

          這樣,通過 _hooks['key'] 來查找,就無所謂前序的 Hook 出現(xiàn)的任何意外情況了。

          也就是說,原本的存儲方式是:

          _hooks: [
            { value'first'updatefunction },
            
          { value: 'second'updatefunction },
          ]

          改造后:

          _hooks: [
            key1: { value'first'updatefunction },
            key2
          { value: 'second'updatefunction },
          ]

          注意,數(shù)組本身就支持對象的 key 值特性,不需要改造 _hooks 的結(jié)構(gòu)。

          改造源碼

          來試著改造一下 Preact 源碼,它的 Hook 包的位置在 hooks/src/index.js[2] 下,找到 useState 方法:

          export function useState(initialState{
            currentHook = 1;
            return useReducer(invokeOrReturn, initialState, undefined);
          }

          它的底層調(diào)用了 useReducer,所以新增加一個 key 參數(shù)透傳下去:

          + export function useState(initialState, key) {
            currentHook = 1;
          + return useReducer(invokeOrReturn, initialState, undefined, key);
          }

          useReducer 原本是通過全局索引去獲取 Hook state:

          // 全局索引
          let currentIndex

          export function useReducer(reducer, initialState, init{
            const hookState = getHookState(currentIndex++, 2);
            hookState._reducer = reducer;

            return hookState._value;
          }

          改造成兼容版本,有 key 的時候優(yōu)先傳入 key 值:

          // 全局索引
          let currentIndex

          + export function useReducer(reducer, initialState, init, key) {
          +  const hookState = getHookState(key || currentIndex++, 2);
             hookState._reducer = reducer;

             return hookState._value;
          }

          最后改造一下 getHookState 方法:

          function getHookState(index, type) {
            const hooks =
              currentComponent.__hooks ||
              (currentComponent.__hooks = {
                _list: [],
                _pendingEffects: [],
              });

          // 傳入 key 值是 string 或 symbol 都可以
          +  if (typeof index !== 'number') {
          +    if (!hooks._list[index]) {
          +      hooks._list[index] = {};
          +    }
          +  } else {
              if (index >= hooks._list.length) {
                hooks._list.push({});
              }
            }
            // 這里天然支持 key 值取用的方式
            return hooks._list[index];
          }

          這里設(shè)計成傳入 key 值的時候,初始化就不往數(shù)組里 push 新狀態(tài),而是直接通過下標寫入即可,原本的取狀態(tài)的寫法 hooks._list[index] 本身就支持通過 key 從數(shù)組上取值,不用改動。

          至此,改造就完成了。

          來試試新用法:

          export default function App({
            if (Math.random() > 0.5) {
              useState(10000'key1');
            }
            const [value, setValue] = useState(0'key2');

            return (
              <div>
                <button onClick={() => setValue(value + 1)}>+</button>
                {value}
              </div>

            );
          }
          ok

          自動編譯

          事實上 React 團隊也考慮過給每次調(diào)用加一個 key 值的設(shè)計,在 Dan Abramov 的 為什么順序調(diào)用對 React Hooks 很重要?[3] 中已經(jīng)詳細解釋過這個提案。

          多重的缺陷導致這個提案被否決了,尤其是在遇到自定義 Hook 的時候,比如你提取了一個 useFormInput

          const valueKey = Symbol();
           
          function useFormInput({
            const [value, setValue] = useState(valueKey);
            return {
              value,
              onChange(e) {
                setValue(e.target.value);
              },
            };
          }

          然后在組件中多次調(diào)用它:

          function Form({
            // 使用 Symbol
            const name = useFormInput(); 
            // 又一次使用了同一個 Symbol
            const surname = useFormInput(); 
            // ...
            return (
              <>
                <input {...name} />
                <input {...surname} />
                {/* ... */}
              </>
              
            )
          }

          此時這個通過 key 尋找 Hook state 的方式就會發(fā)生沖突。

          但我的想法是,能不能借助 babel 插件的編譯能力,實現(xiàn)編譯期自動為每一次 Hook 調(diào)用都注入一個 key, 偽代碼如下:

          traverse(node) {
            if (isReactHookInvoking(node)) {
              addFunctionParameter(node, getUniqKey(node))
            }
          }

          生成這樣的代碼:

          function Form() {
          +  const name = useFormInput('key_1'); 
          +  const surname = useFormInput('key_2'); 
            // ...
            return (
              <>
                <input {...name} />
                <input {...surname} />
                {/* ... */}
              </>    
            )
          }

          + function useFormInput(key) {
          +  const [value, setValue] = useState(key);
            return {
              value,
              onChange(e) {
                setValue(e.target.value);
              },
            };
          }

          key 的生成策略可以是隨機值,也可以是注入一個 Symbol,這個無所謂,保證運行時期不會改變即可。也許有一些我沒有考慮周到的地方,對此有任何想法的同學都歡迎加我微信 sshsunlight[4] 討論,當然單純的交個朋友也沒問題,大佬或者萌新都歡迎。

          總結(jié)

          本文只是一篇探索性質(zhì)的文章:

          • 介紹 Hook 實現(xiàn)的大概原理以及限制
          • 探索出修改源碼機制繞過限制的方法

          其實本意是幫助大家更好的理解 Hook。

          我并不希望 React 取消掉這些限制,我覺得這也是設(shè)計的取舍。

          如果任何子函數(shù),任何條件表達式中都可以調(diào)用 Hook,代碼也會變得更加難以理解和維護。

          如果你真的希望更加靈活的使用類似的 Hook 能力,Vue3 底層響應式收集依賴的原理就可以完美的繞過這些限制,但更加靈活的同時也一定會無法避免的增加更多維護風險。

          最后





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

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

          2. 歡迎加我微信「huab119」拉你進技術(shù)群,長期交流學習...

            關(guān)注公眾號「前端勸退師」,持續(xù)為你推送精選好文,也可以加我為好友,隨時聊騷。



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




          參考資料

          [1]

          Preact: https://github.com/preactjs/preact

          [2]

          hooks/src/index.js: https://github.com/preactjs/preact/blob/master/hooks/src/index.js

          [3]

          為什么順序調(diào)用對 React Hooks 很重要?: https://overreacted.io/zh-hans/why-do-hooks-rely-on-call-order/#%E7%BC%BA%E9%99%B7-2-%E5%91%BD%E5%90%8D%E5%86%B2%E7%AA%81

          [4]

          sshsunlight: https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/017d568dc1d14cd883cc3238350a39ec~tplv-k3u1fbpfcp-watermark.image


          瀏覽 44
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  天天躁日日躁狠狠很躁 | 荫蒂高潮大荫蒂毛茸茸主播 | 人妻日逼网 | 成人在线中文免费视频 | 99热男人|