制定專屬本身的 React Hooks

Hooks 是 16.7.0-alpha 新加入的新特性,目的解決狀態邏輯複用,使無狀態組件擁有了許多狀態組件的能力,如自更新能力(useState)、訪問ref(useRef)、上下文context(useContext)、更高級的setState(useReducer)及週期方法(useEffect/useLayoutEffect)及緩存(useMemo、useCallback)。其底層實現沒有太多變更,總體更接近函數式語法,邏輯內聚,高階封裝這兩大特色,讓你同時領悟到 Hooks 的強大與優雅。html

若是你已經厭倦寫諸如修改網頁標題,判斷用戶離線狀態,監聽頁面大小,用戶手機狀態(電池、螺旋儀...),說明你已經不甘心作一個重複勞動的開發者 ,那麼自定義hooks很是適合你。只想關注Custom Hooks,F 傳送!!!!react

在閱讀本文以前,建議unLearning,也就是忘記你以前學會的「React」 ,它已經不是那個「它」了,不然只會給你帶來「誤導」。ios

ps: 爲了更好的閱讀體驗,- 表示刪減代碼, + 表示新增代碼,* 表示修改行git

本文custom Hooks repogithub

我的Blogtypescript

useState

看這篇解析以前,咱們已經知道本身的水平,豈能像新手同樣先看api? 固然是要先從源碼入手。shell

alt

function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}
複製代碼

WTF ??? npm

alt

回到正題,作人怎麼能手高眼低呢?redux

高手,固然是要先從源碼入手。api

咱們先看下官方的api

useState 用來定義組件數據變量。傳入一個初始值,獲得一個數組,前者爲值,後者是一個dispatch函數,經過dispatch函數能夠去更新該值。

const [value, updateVal] = useState(0)
value // 0
updateVal(1)
value // 1
複製代碼

也能夠傳入一個同步函數。

// ⚠️ Math.random() 每次 render 都會調用Math.random()
const [value, updateVal] = useState(Math.random());
// 只會執行一次
const [value, updateVal] = useState(() => Math.random());
複製代碼

useState僅在函數組件第一次執行初始化。在組件存在期間始終返回最新的值。不會再次去執行初始化函數。看到這,是否是以爲和閉包同樣?

指向問題

// Index.js
class Index extends Component {
  componentDidMount() {
    setTimeout(() => console.log('classComponent', this.state.value), 1000);
    this.setState({ value: 5 });
  }

  render() {
    return null;
  }
}

// App.js
function App () {
  const [value, updateVal] = useState(0);
  useEffect(() => {
    setTimeout(() => console.log('FunctionComponent', value), 1000);
    updateVal(5);
  }, []);
  return (
    <Index /> ); } // classComponent 5 // FunctionComponent 0 複製代碼

若是你還不瞭解 useEffect,能夠暫時把上面 useEffect 暫時當作是 componentWillMount 。目的是一秒鐘後打印當前的value值。

前者經過 this 能夠訪問到最新的值,而函數組件因爲閉包的緣由,打印的時候訪問的仍是更新前的值。這種狀況能夠經過useRef解決,若是你還不瞭解useRef

const num = useRef(null);
useEffect(() => {
    setTimeout(() => console.log(num.current), 1000); // 2
    updateVal((pre) => {
      const newPre = pre * 2;
      num.current = newPre;
      return newPre;
    });
}, []);
複製代碼

可是updateVal 執行的時機沒法保證(畢竟在整個週期的最後);還有個比較low的方案 —— 就是 useStatedispatch 函數 。

useEffect(() => {
    setTimeout(() => updateVal((pre) => {
      console.log('FunctionComponent', pre);
      return pre;
    }), 1000);
    updateVal(5);
  }, []);
// classComponent 5
// FunctionComponent 5
複製代碼

效果是能實現,可是因爲 dispatch set 新值會觸發一次 re-render。 因此這個方案不建議使用。後面會有封裝的hooks達到目的。

根做用域順序聲明

不能嵌套在 if 或者 for 中聲明

以前看過很多hooks的文章,都說hooks是以數組的形式存儲的,因此纔會出現指向問題。但在後來實踐發現並不是如此(連官方也這麼誤導我).

React 如何將 Hook 調用與組件相關聯?
React 跟蹤當前渲染組件。 因爲 Hooks 規則,咱們知道 Hook 只能從 React 組件調用(或自定義 Hooks 也只能從 React 組件中調用)。
每一個組件都有一個 「內存單元」 的內部列表。它們只是 JavaScript 對象,咱們能夠在其中放置一些數據。當調用 useState() 這樣的Hook 時,它讀取當前單元格(或在第一次呈現時初始化它),而後將指針移動到下一個單元格。這就是多個 useState() 調用各自獲取獨立本地狀態的方式。

其實是以一種單向循環鏈表。相似A.next === B => B.next === C 。

alt
剖析 引用

const [state1,setState1] = useState(1)
const [state2,setState2] = useState(2)
const [state3,setState3] = useState(3)
複製代碼

每一個FunctionalComponent都會有個對應的Fiber對象,

function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;  // ReactElement[$$typeof]
  this.type = null;         // ReactElement.type
  this.stateNode = null;

  // ...others
  this.ref = null;
  this.memoizedState = null;
  // ...others
}
複製代碼

在其中調用的useState 會有個 Hook 對象。

export type Hook = {
  memoizedState: any,

  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,

  next: Hook | null, // 指向下一個hook節點
};

複製代碼

其餘的問題不用太關注,只須要知道當在第一次執行到useState的時候,會對應 Fiber 對象上的 memoizedState,這個屬性原來設計來是用來存儲 ClassComponentstate 的,由於在 ClassComponentstate 是一整個對象,因此能夠和memoizedState 一一對應。

可是在 Hooks 中,React並不知道咱們調用了幾回 useState,因此在保存 state 這件事情上,React 想出了一個比較有意思的方案,

Fiber.memoizedState === hook1
state1 === hook1.memoizedState
hook1.next === hook2
state2 === hook2.memoizedState
hook2.next === hook3
state3 === hook2.memoizedState
複製代碼

每一個在 FunctionalComponent 中調用的 useState 都會有一個對應的 Hook 對象,他們按照執行的順序以相似單向循環鏈表的數據格式存放在 Fiber.memoizedState 上。

若是出現下面這種邏輯

if(false) {
    const [status,setStatus] = useState(false)
}
// or
let times = 10 // 某次邏輯修改了times
for(let i = 0;i < times; i++) {
    const status = useState(false)
}
複製代碼

會致使某次 re-render 後,少了某個 hook ,next 指向錯誤,好比 hook1.next 指向了 hook3 形成數據混亂,沒法達到預想效果。

useEffect

生命週期的階段性方法,相似setState(state, cb)中的cb,執行時機位於整個更新週期的最後。

話很少少,先上源碼。

///////// useEffect
export function useEffect( create: () => (() => void) | void, inputs: Array<mixed> | void | null, ) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, inputs);
}
//...省略
function mountEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null, ): void {
 return mountEffectImpl(
    UpdateEffect | PassiveEffect,
    UnmountPassive | MountPassive,
    create,
    deps,
  );
}

複製代碼

alt

咳,這個方法可謂是hooks裏最重要的一個hooks。若是把useState當作 HTML+CSS,那 useEffect 就是 JS

useEffect(fn, deps?:any[])fn 執行函數,deps 依賴。與 useState 相似,fn 在初始化時執行一次。然後續執行則依賴於 deps 的變化,若是 re-render 後執行該 effects 發現這次 deps 與上次不一樣,就會觸發執行。

ps: React 內部使用 Object.is 對 deps 進行淺比較。

剛開始脫離 classComponent 轉而使用 hooks 時曾覺得它在 render 前執行,其實否則。

默認狀況下,效果在每次完成渲染後運行

useEffect(() => {
    // 僅在初始化時(首次render後)執行
}, []);

useEffect(() => {
    // 每次render後執行
});
複製代碼

fn 可返回一個清理函數,大多數運用於 addEventListenerremoveEventListener

useEffect(() => {
    // 首次render後執行
    return () => {
        // 組件卸載前執行
    }
},[]);

useEffect(() => {
    (function fn() => { /*dst...*/ })()
    // 每次render後執行
    return () => {
        // 從第二次開始,先運行此清理函數,再執行fn
    }
});

複製代碼

清理函數的執行時機能夠理解爲若是該 Effect 句柄執行過,則下次優先執行清理函數,以防止內存泄漏,最後一次執行時機在組件卸載後。

若是非要形容對應哪一個生命週期,我更以爲像 componentDidUpdate

不要在 useEffect 中操做DOM。好比使用 requestAnimationFrame 添加幾萬個節點。會有意想不到的驚喜。

eg:

const Message = ({ boxRef, children }) => {
  const msgRef = React.useRef(null);
  React.useEffect(() => {
    const rect = boxRef.current.getBoundingClientRect(); // 獲取尺寸
    msgRef.current.style.top = `${rect.height + rect.top}px`; // 放到盒子下方
  }, [boxRef]);

  return (
    <span ref={msgRef} className="msgA"> {children} </span>
  );
};
const App = () => {
  const [show, setShow] = React.useState(false);
  const boxRef = React.useRef(null);

  return (
    <div> <div ref={boxRef} className="box" onClick={() => setShow(prev => !prev)}> Click me A </div> {show && <Message boxRef={boxRef}>useEffect</Message>} </div>
  );
};

複製代碼

目的很簡單,將 Message 組件 顯示時放置到div下,但實際運行結果時會發現有一瞬間跳動效果。

alt

當把Message組件內的 useEffect 換成useLayoutEffect就正常了。

Edit charming-surf-wz9fk

緣由是雖然useEffect在瀏覽器繪製後執行,也表明着它會在新渲染以前觸發。須要執行新的渲染以前它會先刷新現有的effects。

什麼?你不信?

alt
e.g:

const [val, updateVal] = useState(0)
useEffect(() => { // hooks1
    updateVal(2);
}, []);
useEffect(() => { // hooks2
    console.log(val);// ---- 0
});
複製代碼

在 render後,先執行hook1 updateVal(2) 觸發了 re-render,但在此以前須要先刷新現有的 effects,因此hooks2 val 打印出來的仍是 0 ,而後再次觸發 render 渲染後的 effects hooks2纔打印出 2

依賴於閉包

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

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`);
    }, 3000);
  });

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

你以爲快速的連續點擊5次,彈出來的會是什麼?

alt
與classComponent不一樣,它訪問的而是this。而不是閉包。

componentDidUpdate() {
    setTimeout(() => {
      console.log(`You clicked ${this.state.count} times`);
    }, 3000);
  }
複製代碼

alt

在定時器裏執行的事件,徹底依賴於閉包。可能你不認同,可是事實確是如此。

關於依賴項不要對React撒謊

function App(){
   const [tagType, setTagType] = useState()
    async function request () {
        const result = await Api.getShopTag({
            tagType
        })
    }
    useEffect(()=>{
        setTagType('hot')
        request()
    },[])
    return null
}
複製代碼

request 函數依賴於 tagType,可是Effects沒有依賴於tagType,當tagType改變時,requesttagType 的值仍然是 hot。 你可能只是想掛載的請求它,可是 如今只須要記住:若是你設置了依賴項,effect中用到的全部組件內的值都要包含在依賴中。這包括 props,state,函數 — 組件內的任何東西。

後話

在組件年內請求數據時,常常會這麼寫

function App(){
    async function request () {
        // ...
        setList(result)
        setLoaded(false)
    }
    useEffect(()=>{
        request()
    },[])
    return null
}
複製代碼

在正常狀況下訪問固然沒問題,當組件體積龐大或者請求速度慢時,你會收到「驚喜」。

alt
意思是還沒請求完畢你就去到別的頁面,致使effects內的 setList/setLoaded 無從下手送溫暖。 這也是閉包的弊端 —— 沒法及時銷燬。還有一個解決方案是 AbortController

其實搞定這兩個api就能完成80%的業務了。符合二八定律,即20%的功能完成80%的業務。封裝自定義hooks大多數也須要它們。

useLayoutEffect

useLayoutEffect 名字與 useEffect 相差了一個 Layout。 顧名思義,它們的區別就是執行時機不同,表示在 Layout 後觸發。即 render 後。

源碼以下:

alt

簽名與 useEffect 相同,但在全部 DOM 變化後同步觸發。 使用它來從 DOM 讀取佈局並同步從新渲染。 在瀏覽器有機會繪製以前,將在 useLayoutEffect 內部計劃的更新將同步刷新。

官方解釋它會阻塞渲染,因此在不操做dom的狀況用 useEffect ,以免阻止視覺更新。

執行時機 render > useLayoutEffect > useEffect > setState(useState) > 清理effects > render(第二遍) > ...

而useLayoutEffect內setState的執行機制和useEffect不同。雖然最後都執行了合併策略。在mount和update的階段也是不同的。甚至函數組件頂部申明useState的順序都會致使執行結果不一致。

相對於組件 mount,在 update 觸發 Hooks 的順序更讓人容易理解一些。

requestAnimationFrame將任務「打碎」,執行的時機在於重繪後,也就是useLayoutEffect執行事後。

ps:若是你只是改變數據,首選useEffect,由於它不會阻塞渲染。這是優勢也是缺點,不阻塞(表明異步),固然也保證不了順序。而涉及到 DOM 操做的建議使用useLayoutEffect

useReducer

內置hook,看名字就知道和redux有關。使用方法和redux相同。

const reducer = (state,action) => {
    let backups = { ...state }
    switch(action.type){
        case 'any': // ... ; break;
    }
    return backups
}
const initial = { nowTimes: Date.now() }
function App () {
    const [val, dispatch] = useReducer(reducer,initial);
    return null
}
複製代碼

經過 useState 手動一個實現 useReducer

function useMyReducer(reducer, initialState, init) {
  const compatible = init ? init(initialState) : initialState;
  const [state, setState] = useState(compatible);
  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }

  return [state, dispatch];
}
複製代碼

第三個參數相似於 reduxinitialState,用於設置初始State。不管是否命中 reducer,每次 dispatch 都將觸發 re-render。

若是你想用它代替 Redux 可能仍是缺乏點什麼。有一個明顯的問題,這裏定義的state是和組件綁定的,和 useState 同樣,沒法和其餘組件共享數據。可是經過 useContext 能夠達到目的。

useContext

useContext(context: React.createContext)

// Context/index.js
const ContextStore = React.createContext()

// App.js

function Todo() {
  const value = React.useContext(ContextStore);
  return (
    <React.Fragment>
      {
        JSON.stringify(value, null, 4)
      }
    </React.Fragment>
  );
}

function App() {
  return (
    <ContextStore.Provider value={store}>
      <Todo />
    </ContextStore.Provider>
  );
}
複製代碼

經過使用方法發現,配合 useReducer 能夠在組件樹頂層使用 Context.Provider 生產/改變數據,在子組件使用 useContext 消費數據。

const myContext = React.createContext();
const ContextProvider = () => {
    const products = useReducer(productsReducer, { count: 0 });
    const order = useReducer(orderReducer, { order: [] });
    const store = {
        product: products,
        order: order // [ order,deOrder ]
    }
    return (
        <myContext.Provider value={store}>
          <Todo />
        </myContext.Provider>
    );
};

const Todo = () => {
    const { product, order } = useContext(myContext)
    return (
        <React.Fragment>
            {
                JSON.stringify(state, null, 4)
            }
            <button onClick={product.dispatch}> product dispatch </button>
        </React.Fragment>
    )
}
複製代碼

弊端是當數據量變大時,整個應用會變得「十分臃腫」而且性能差勁。這有個很不錯的實現 iostore

useMemo

譯文備忘錄,若是更貼切點我想應該叫緩存,useCache? 但後來想一想也對,叫備忘錄也沒錯,畢竟是狀態邏輯複用。

useMemoreselect庫功能相同,都是依賴於傳入的值,有固定的輸入就必定有固定的輸出。沒必要從新去計算,優化性能。在依賴不改變的狀況下避免從新去計算浪費性能。

可是reselect用起來太繁瑣了。useMemo相對簡單的多

const memoDate = useMemo(()=>{
   return new Date().toLocalString() 
},[])
複製代碼

useMemo的第二個參數與useEffect功能相同,當依賴發生變化纔會進行從新計算。memoDate在組件內將始終不變。

依賴項

可能你寫過這樣的代碼

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

const request = useMemo(() => {
    return aysnc () => {
        let result = await Api.getMainShopTag({
            startNum: count,
            size: 10
        })
        setCount(result.count)
    }
}, []);
return <button onClick={request} type="button"> request </button>
複製代碼

簡單的一個分頁標籤請求,開始一切正常,當請求第二次的時候發現 count 仍爲0。useMemo 緩存了函數,天然也緩存了函數內變量的指向。因此須要在deps內添加函數內須要依賴的參數。

若是你對這一切還不熟悉,react-hooks 針對 eslint 推出一款插件 eslint-plugin-react-hooks,它能夠自動幫你修復依賴項,而且提供優化支持。在制定自定義hooks的時候,嚴格遵照準則。

npm i eslint-plugin-react-hooks -D

複製代碼
// .eslintrc
{
    // other ...
    "plugins": [
        "html","react","react-hooks"
    ],
    "rules":{
        "react-hooks/rules-of-hooks": "error",
        "react-hooks/exhaustive-deps": "warn"
    }
}
複製代碼

useCallback

useCallback 是 useMemo的變體。二者做用相同,你能夠理解爲前者更偏向於函數緩存。在定義一些不依賴於當前組件的屬性變量方法時,能夠儘可能採用 useCallback 緩存。避免組件每次render前再次申明。

useCallback(fn, deps) === useMemo(() => fn, deps))
複製代碼

好比上面的代碼你能夠簡化成

const request = useCallback(aysnc () => {
        let result = await Api.getMainShopTag({
            startNum: count,
            size: 10
        })
        setCount(result.count)
    }, [count]);
複製代碼

關於memo與callback

useCallback與useMemo須要慎重使用。不少人覺得二者是爲了解決建立函數帶來的性能問題,其實否則。

上菜:

const forgetPwd = () => {
    const sendSmsCode = () => { /*...*/ }
}

const forgetPwd = () => {
    const sendSmsCode = useMemo(()=>{ /*...*/ }, [A,B])
}

複製代碼

上面的例子中二者效果是同樣的,不管如何 sendSmsCode 都會被建立。只不由於後者須要比對依賴而耗費了稍微一點點的性能(蚊子再小也是肉),那可能會有疑問,爲何使用了緩存性能反而愈來愈差。

<button onClick={() => {}}>Search</button>
// 第二次render
<button onClick={() => {}}>Search</button>
複製代碼

兩次渲染 inline 函數永遠不會相等,與memo的概念背道而馳。這是沒有意義的diff。只會浪費時間,而組件也絕對不會被memo優化。

useCallback 實際上緩存的是 inline callback 的實例,配合React.memo可以起到避免沒必要要的渲染。二者缺一個性能都會變差。當你的函數組件 UI 容器內有任何一個 inline 函數,任何優化都是沒有意義的。

useRef

解決的問題是組件數據狀態沒法保存,有以下代碼

function Interval() {
  const [time, setTime] = useState(Date.now());
  let intervalId;
  const start = () => {
    intervalId = setInterval(() => {
      setTime(Date.now());
    }, 500);
  };
  const clear = () => {
    clearInterval(intervalId);
  };
  return (
    <div> <button onClick={start} type="button">start</button> <button onClick={clear} type="button">clear</button> </div>
  );
}
複製代碼

看起來很正常的一段邏輯,可是啓動定時器後,發現沒法關閉定時器了。

這是由於啓動定時器後,setTime(Date.now()) 更新值後,函數組件被 re-render。此時intervalId已經被從新聲明瞭。因此清除不了以前的定時器。

函數組件沒有被實例化,意味着沒法使用this、沒有內部的組件屬性變量。須要避免其每次被從新聲明。

const [intervalId, setIntervalId] = useState(null)
const start = () => {
    setIntervalId(
        setInterval(() => setTime(Date.now()), 500)
    )
};
複製代碼

難道必須所有使用 useState 儲存狀態麼?前面提到過,每次執行 setIntervalId 句柄都會觸發一次 re-render,即便沒有在視圖裏沒有用到。

能夠用 useRef 處理組件屬性。改造組件

// ... other
let intervalId = useRef(null);
const start = () => {
    intervalId.current = setInterval(() => {
        setTime(Date.now());
    }, 500);
};
const clear = () => {
    clearInterval(intervalId.current);
};

複製代碼

使用 useRef 最好的理由是不會觸發 re-render 。源碼:

function mountRef<T>(initialValue: T): {current: T} {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  if (__DEV__) {
    Object.seal(ref);
  }
  hook.memoizedState = ref;
  return ref;
}

複製代碼

那怎麼理解useRef?

你能夠把它當作是一個盒子。能夠聽任何數據(甚至組件) —— 海納百川有容乃大。在盒子中的東西(current)會被隔離,且值將會被深拷貝,不會被外界所幹擾、同時也不會響應。你能夠從新經過 ref.current 去賦值。而且不會觸發 re-render 與 useEffect 。經過 .current 獲取的值始終都是最新的。

const info = useRef({ status: false });

const focus = () => {
    // 始終都是最新的
    setTimeout(() => console.log(info.current), 1000); // {status: true}
    info.current.status = true;
}

const input = useRef();
useEffect(() => {
    //能夠訪問元素上的方法
    input.current.focus()
}, [])

useEffect(() => {
    // info改變不會觸發
}, [info.current.status])

return <input ref={input} type="text" onFocus={focus} /> 複製代碼

useImperativeHandle

雖然經過useRef能夠訪問本組件屬性。但若是父元素想操做子組件就顯得較無能無力。在 classComponent 你可使用this.chilren去訪問子組件方法。函數組件就沒有這項特權了,畢竟沒有被實例化,官方提供useImperativeHandle(原useImperativeMethods)向父組件暴露組件方法。額外的是須要配合 forwardRef 轉發該節點使用,官方的例子已經極爲清楚了:

// FancyInput.js
function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    blurs: () =>{
        // dst...
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

//App.js
function App(){
    const fancyInputRef = useRef()

    useEffect(()=>{
        fancyInputRef.current.focus()
    },[])
    return (
        <React.Fragment>
            <FancyInput ref={fancyInputRef} />
        </React.Fragment>
    )
}

複製代碼

useDebugValue

這個就屬於輔助標識了。在某個custom Hook 使用,標識該鉤子是 custom hook。 好比申明一個 useMount

const useMount = (mount) => {
  useEffect(() => {
    mount && mount();
  }, []);
  useDebugValue('it"s my Custom hook',fit => `${fit} !` );
};
// App.js
const App = () => {
  useMount(() => { /*dst...*/});
  return (
    <Provider> <Count /> <IncrementButton /> </Provider>
  );
};

複製代碼

alt
該Api只會在ReactDevTools啓用情況下才會加載,藍色表明 Custom Hook 名稱,好比use Mount。紅色爲描述。

自定義鉤子

實現 setState 回調

首先實現一個簡陋版本

// App.js
function App() {
  const [value, updateVal] = useState(0);
  const init = useRef(0);

  const doSomething = () => {
    updateVal(pre => pre + 1);
  };

  useEffect(() => {
    init.current += 1
    if (init.current > 1) {
      // callback
      console.log('has changed', value);
    }
  }, [value]);

  return <button onClick={doSomething}>{value}</button>;
}
複製代碼

這或許不能稱爲callback,監聽可能更符合一點,能夠封裝成hooks

// hooks.js
const useStateWithCb = (initialState, callback) => {
  const [state, setState] = useState(()=>initialState);
  
  useEffect(() => callback(state), [state, callback]); //每次state更新的時候都會去執行該回調
  return [state, setState];
}

function App () {
    const [value, updataVal ] = useStateWithCb(0, () => {
        console.log(value)
    })
}
複製代碼

這樣初始化也會執行,相應能夠參照上文給useStateWithCb加上 useRef 來避免初始化執行。這裏就再也不演示了。還一種方法是利用 useLayoutEffect 也能達成相同的效果。

不過這樣作的弊端是不能動態的傳入回調。叔可忍嬸嬸不能忍,既然返回的updateVal不能改,那就劫持它。因而就用到 Proxyapply 攔截函數調用。

apply 函數有3個參數

  • target 調用的目標對象(函數)。
  • thisArg 調用上下文對象。
  • argumentsList 被調用時的參數數組。
// hooks.js
const useStateWithCb = (initialVal) => {
  const [state, setState] = useState(()=>initialVal);
  const hijackSetState = useMemo(()=>new Proxy(setState, { // 這裏作一層緩存,確保不會再次建立
    apply(target, thisArg, argumentsList) { // 攔截
      const args = Array.prototype.slice.call(argumentsList);
      if (args.length > 1 && typeof args.slice(-1)[0] === 'function') { // 若是尾參數爲函數
        fn.current = args.pop(); // 賦值給 fn.current
      }
      return target.apply(thisArg, args);
    }
  }),[]);
  useEffect(() => {
      typeof fn.current === 'function' && fn.current(state);
  }, [state]);
  return [state, hijackSetState];
};

// App.js
import { useStateWithCb } from 'hooks'
function App() {
  const [value, updateVal] = useStateWithCb(0);

  const doSomething = () => {
    updateVal.current(pre => pre + 1, (newVal) => {
      console.log(newVal); // 1
    });
  };

  return <button onClick={doSomething} type="button">{value}</button>;
}
複製代碼

設置相同值

效果是能實現,但設置相同值,Object.is 會將其斷定 unChanged

//App.js
const [value, updateVal] = useStateWithCb(2);

const doSomething = () => {
    updateVal(2, (newVal) => {
      console.log(newVal); // 值未改變,觸發不了newVal
    });
};
複製代碼

因爲Effect 依賴於 state ,對於基本類型數據直接比較的值,因此觸發不了 Effect 的回調函數,而Object,Array,Map,Symbol等直接比較內存地址的卻能夠。能夠對基本類型作特別處理。

// utils.js

// 判斷原始類型數據 undefined,number,boolean,string, null,NaN
const isOriginal = o => typeof o === 'object' ? !o : typeof o !== 'function';
// 建立隨機數
const random = () => Math.random().toString(36).split('').join('.')
// 函數判斷
const isFunction = fn => Object.prototype.toString.call(fn) === "[object Function]"

const useStateWithCb = (initialVal) => {
  const [state, setState] = useState(()=>initialVal);
  const fn = useRef(null);
+ const [r, sr] = useState(()=>random()); // 建立隨機數句柄
  const hijackSetState = useMemo(() => new Proxy(setState, {
    apply(target, thisArg, argumentsList) {
      const args = Array.prototype.slice.call(argumentsList);
+     if (isOriginal(args[0]) && args[0] === state) { // 原始類型 && 等與自身
+       sr(random()); // 若是是原始類型則更新隨機數
+     }
      if (args.length > 1 && typeof args.slice(-1)[0] === 'function') {
        fn.current = args.pop();
      }
      return target(...args);
    }
  }),[]);

  useEffect(() => {
    isFunction(fn.current) && fn.current(state);
+ }, [state, r]); // 增長 隨機數 做爲依賴

  return [state, hijackSetState];
};
複製代碼

代碼簡潔,說明思路便可。

回調內再次賦值

// App.js
const doSomething = () => {
    updateVal(pre => `${pre}2`, (firstVal) => {
      console.log(firstVal); // 1
      updateVal(pre => `${pre}3`, (secondVal) => {
        console.log(secondVal); // 循環調用
      });
    });
};
複製代碼

這種狀況也能想獲得是因爲每次調用的都是上一次的更新器致使死循環。

那就把最新的更新器傳給callback。

// hooks.js
function useStateWithCb (initialVal){
    //...other
    useEffect(() => {
-    isFunction(fn.current) && fn.current(state);
+    isFunction(fn.current) && fn.current(state, hijackSetState);
    }, [state, r, hijackSetState]);
   // ...other
}

// index.js
const doSomething = () => {
    const [value, updateVal] = useStateWithCb(1);
    updateVal(pre => `${pre}2`, (firstVal, firstSetter) => {
      console.log(firstVal); // 12
      firstSetter(pre => `${pre}3`, (secondVal, secondSetter) => {
        console.log(secondVal); // 123
        secondSetter(pre => `${pre}4`, (thirdVal, thirdSetter) => {
          console.log(thirdVal); // 1234
        });
      });
    });
};
複製代碼
完整版
const useStateWithCb = (initialVal) => {
  const [state, setState] = useState(()=>initialVal);
  const fn = useRef(null);
  const [r, sr] = useState(()=>random());
  const hijackSetState = useMemo(() => new Proxy(setState, {
    apply(target, thisArg, argumentsList) {
      const args = Array.prototype.slice.call(argumentsList);
      if (isOriginal(args[0]) && args[0] === state) {
        sr(random());
      }
      if (
        args.length > 1
          && typeof args.slice(-1)[0] === 'function'
      ) {
        fn.current = args.pop();
      }
      return target(...args);
    },
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }), []);

  useEffect(() => {
    typeof fn.current === 'function' && fn.current(state, hijackSetState);
  }, [state, r, hijackSetState]);

  return [state, hijackSetState];
};
複製代碼
有興趣的能夠安裝 `npm i like-hooks -S`。使用方法和上文一致。

Edit serverless-morning-r2svr

useLifeCycles

或許不須要單獨拿出來。但仍是忍不住湊字數。

alt

const useLifeCycles = (mount, unMount) => {
  useEffect(() => {
    mount && mount();
    return () => {
      unMount && unMount();
    };
  }, []);
};
複製代碼

useRequestAnimationFrame

RequestAnimationFrame使用的頻率很高,理所固然將其封裝成一個hooks;名字太長可不是個好事。

/** * useRaf useRequestAnimationFrame * @param callback 回調函數 * @param startRun 當即執行 */
const useRaf = (callback, startRun = true) => {
  const requestRef = useRef(); // 儲存RequestAnimationFrame返回的id
  const previousTimeRef = useRef(null); // 每次耗時間隔

  const animate = useCallback((time) => {
    if (previousTimeRef.current !== undefined) {
      const deltaTime = time - previousTimeRef.current; // 耗時間隔
      callback(deltaTime);
    }
    previousTimeRef.current = time;
    requestRef.current = requestAnimationFrame(animate);
  }, [callback]);

  useEffect(() => {
    requestRef.current = requestAnimationFrame(animate);
    return () => cancelAnimationFrame(requestRef.current);
  }, []);

  const stopRaf = useCallback(() => {
    if(startRun) cancelAnimationFrame(requestRef.current);
    requestRef.current = null;
  }, [animate]);

  const restartRaf = useCallback(() => {
    if (requestRef.current === null) {
      requestAnimationFrame(animate);
    }
  }, [animate]);

  return [restartRaf, stopRaf];
};

// App.js
const App = () => {
  const [count, setCounter] = useState(0);
  const run = () => {
    setCounter(pre => pre + 1);
  };
  const [start, stop] = useRaf(() => run());
  return (
    <div> <button type="button" onClick={start}>開始</button> <button type="button" onClick={stop}>暫停</button> <h1>{count}</h1> </div>
  );
};
複製代碼

Edit serverless-morning-r2svr

該hook接受一個函數做爲幀變更的callback,callback接受一個參數,做爲距離上次performance.now() 的間隔耗時,一般爲16ms上下(意義不大,但可爲低配置用戶啓用優化方案)。hook返回兩個控制器,一個用來重啓,固然不會將數據重置,另外一個用來暫停。

ps:不要用來操做DOM,若是非得操做,建議改爲useLayoutEffect

有了這個hook,相信你就可以輕輕鬆鬆作出秒錶、倒計時、數字逐幀變更等酷炫組件了。

usePrevious

利用 useRef 保存上一次的值,在Effect裏第一次取值會拿到 undefined 的狀況。有時候還須要去判斷,這裏利用Symbol 判斷,首次返回該值。固然你也能夠不考慮這種狀況(第二個參數爲false)。

export const usePrevious = (value) => {
  const r = useRef(Math.random().toString(36)) // 利用隨機數建立全局惟一的id
  const ref = useRef(Symbol.for(r.current));

  useEffect(() => {
    ref.current = value;
  });
  return Symbol.for(r.current) === ref.current ? value : ref.current;
};
複製代碼

Edit serverless-morning-r2svr

useEventListener

不想去頻繁寫原生event事件,將其封裝成hooks。

export function useEventListener(eventName, handler, target = window) {
  const memoHandler = useRef();
  useEffect(() => {
    memoHandler.current = handler;
  }, [handler]);

  useEffect(() => {
    const eventListener = event => memoHandler.current(event);
    const targetEl =
      "current" in target && typeof target.current === "object"
        ? target.current
        : target;
    targetEl.addEventListener(eventName, eventListener);
    return () => {
      targetEl.removeEventListener(eventName, eventListener);
    };
  }, [eventName, target]);
}
複製代碼

因爲React-DOM爲IE9+,不考慮 attachEvent。函數組件只能經過ref訪問元素,增長.current判斷防止報錯。

Edit serverless-morning-r2svr

useDebounce

防抖都不會陌生

/** * 防抖函數執行 * @param {*} fn 被防抖函數 * @param {number} [ms=300] 間隔 */
export const useDebounce = (fn, args, ms = 300 ) => {
  
  const pendingInput = useRef(true);
  
  useEffect(() => {
    let savedHandlerId;
    if (pendingInput.current) {
      pendingInput.current = false;
    } else {
      savedHandlerId = setTimeout(fn, ms);
    }
    return () => clearTimeout(savedHandlerId);
  }, [fn, ms, args]);
};
複製代碼

Edit serverless-morning-r2svr

useThrottle

節流有更加簡單的第三方實現

const throttled = useRef(throttle((newValue) => {
    // dst...
}, 1000))
useEffect(() => throttled.current(value), [value])
複製代碼

可是入鄉隨俗,仍是要實現一個。

/* * 節流函數,等電梯,電梯15秒一輪,進人不重置。 * @param {*} fn 被節流函數 * @param {*} args 依賴更新參數 * @param {number} [timing=300] 節流閥時間 * @returns 節流值 */
const useThrottle = (fn, args, timing = 300) => {
  const [state, setState] = useState(() => fn(...args));
  const timeout = useRef(null);
  const lastArgs = useRef(null); // 最近一次參數
  const hasChanged = useRef(false); // 是否有更新
  useEffect(() => {
    if (!timeout.current) {
      const timeoutHandler = () => {
        if (hasChanged.current) { // 有更新,當即更新並再啓動一次,不然放棄更新
          hasChanged.current = false;
          setState(() => fn(...lastArgs.current));
          timeout.current = setTimeout(timeoutHandler, timing);
        } else {
          timeout.current = undefined;
        }
      };
      timeout.current = setTimeout(timeoutHandler, timing);
    } else {
      lastArgs.current = args; // 更新最新參數
      hasChanged.current = true; // 有更新任務
    }
  }, [...args, fn, timing]);
  return state;
};
複製代碼

使用方法

const throttledValue = useThrottle(value => value, [val], 1000);
複製代碼

Edit serverless-morning-r2svr

useImtArray

製做一個 ImmutableArray

/** * 經過二次封裝數組,達到相似ImmutableArray效果 * @param {*} initial * @returns */
const useImtArray = (initial = []) => {
  const [value, setValue] = useState(()=>{
    if(!Array.isArray(initial)) {
      throw new Error('useImtArray argument Expectations are arrays. Actually, they are' + Object.prototype.toString.call(initial))
    }
    return initial
  });
  return {
    value,
    push: useCallback(val => setValue(v => [...v, val]), []),
    pop: useCallback(() => setValue(arr => arr.slice(0, arr.length - 1)), []),
    shift: useCallback(() => setValue(arr => arr.slice(1, arr.length)),[]),
    unshift: useCallback(val => setValue(v => [val, ...v]), []),
    clear: useCallback(() => setValue(() => []), []),
    removeByVal: useCallback(val => setValue(arr => arr.filter(v => v !== val)),[]),
    removeByIdx: useCallback(index => setValue(arr =>
          arr.filter((v, idx) => parseInt(index, 10) !== idx),
        ), []),
  };
};
複製代碼

Edit serverless-morning-r2svr

usePromise

Promise固然也少不了。

/** * 簡化Promise * @param {*} fn Promise函數 * @param {*} [args=[]] 依賴更新參數 * @returns loading:加載狀態,value:成功狀態的值,error:失敗狀態的值 */
const usePromise = (fn, args = []) => {
  const [state, setState] = useState({});
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const memoPromise = useCallback(() => fn(), args);

  useEffect(() => {
    let pending = true; // 防止屢次觸發
    setState(newestState => ({ ...newestState, loading: true }));
    Promise.resolve(memoPromise())
      .then(value => {
        if (pending) {
          setState({
            loading: false,
            value,
          });
        }
      })
      .catch(error => {
        if (pending) {
          setState({
            loading: false,
            error,
          });
        }
      });

    return () => {
      pending = false;
    };
  }, [memoPromise]);

  return state;
};


// App.js
const request = () => new Promise((resolve,reject)=>{
  setTimeout(()=>{
    if(Math.random() > 0.5 ){
      resolve('Success')
    }else{
      reject('Fail')
    }
  },2000)
})
function App(){
  const { value, loading, error} = usePromise(request)
  return (
    <div>{loading? <span>Loading...</span> : result:<span>{error||value}</span>}</div>
  )
}
複製代碼

Edit serverless-morning-r2svr

useGetter

經過 Object.definedProperty 可以簡單的去監聽讀取屬性

import { clone, isPlainObject } from '../utils';
/** * 監聽對象屬性被讀取 * @param {*} watcher 監聽對象 * @param {*} fn 回調 */
const useGetter = (watcher, fn) => {
  if (!isPlainObject(watcher)) {
    throw new Error(
      `Expectation is the object, the actual result ${Object.prototype.toString.call( watcher, )}`,
    );
  }
  const value = useMemo(() => watcher, [watcher]);
  const cloneVal = useMemo(() => clone(watcher), [watcher]);
  const cb = useRef(fn);

  Object.keys(cloneVal).forEach(name => {
    Object.defineProperty(value, name, {
      get() {
        if (typeof cb.current === 'function')
          cb.current(name, cloneVal);
        return cloneVal[name];
      },
    });
  });
};
複製代碼

Edit serverless-morning-r2svr

useLockBodyScroll

這個鉤子偶然看到的,針對防止遮罩滾動穿透。原地址

/** * 鎖定body滾動條,多用於modal,後臺 */
const useLockBodyScroll = () => {
  useLayoutEffect(() => {
    const originalStyle = window.getComputedStyle(document.body)
      .overflow;
    document.body.style.overflow = 'hidden';
    return () => {
      document.body.style.overflow = originalStyle;
    };
  }, []);
};
複製代碼

Edit serverless-morning-r2svr

useTheme

你甚至能夠本身切換主題配色,就像這樣

/** * 更換主題 * @param {*} theme 主題數據 */
const useTheme = theme => {
  useLayoutEffect(() => {
    for (const key in theme) {
      document.documentElement.style.setProperty(
        `--${key}`,
        theme[key],
      );
    }
  }, [theme]);
};
複製代碼

Edit serverless-morning-r2svr

useInput

寫input的時候你還在手動 onChange 麼?

/** * auto Input Hooks * @param {*} initial Input初始值 * @returns InputProps clear清空 replace(arg:any|Function) bind 綁定Input */
function useInput(initial) {
  const [value, setValue] = useState(initial);
  function onChange(event) {
    setValue(event.currentTarget.value);
  }
  const clear = () => {
    setValue('');
  };
  const replace = arg => {
    setValue(pre => (typeof arg === 'function' ? arg(pre) : arg));
  };
  return {
    bind: {
      value,
      onChange,
    },
    value,
    clear,
    replace,
  };
}

function Input() {
  let userName = useInput("Seven"); // {clear,replace,bind:{value,onChange}}
  return <input {...userName.bind} />; } 複製代碼

Edit serverless-morning-r2svr

useDragger

一個極簡的拖拽hook,稍加改造。

/** * 拖拽元素 * @param {*} el 目標元素 * @returns x,y偏移量 pageX,pageY 元素左上角位置 */
function useDraggable(el) {
  const [{ dx, dy }, setOffset] = useState({ dx: 0, dy: 0 });
  const [{ pageX, pageY }, setPageOffset] = useState({
    pageX: 0,
    pageY: 0,
  });
  useEffect(() => {
   const { top, left } = el.current.getBoundingClientRect();
    setPageOffset({ pageX: top, pageY: left });
    const handleMouseDown = event => {
      const startX = event.pageX - dx;
      const startY = event.pageY - dy;
      const handleMouseMove = e => {
        const newDx = e.pageX - startX;
        const newDy = e.pageY - startY;
        setOffset({ dx: newDx, dy: newDy });
      };
      document.addEventListener('mousemove', handleMouseMove);
      document.addEventListener('mouseup', () => {
          document.removeEventListener('mousemove', handleMouseMove);
        },{ once: true });
    };
    el.current.addEventListener('mousedown', handleMouseDown);
    return () => {
      el.current.removeEventListener('mousedown', handleMouseDown);
    };
  }, [dx, dy, el]);

  useEffect(() => {
    el.current.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
  }, [dx, dy, el]);

  return { x: dx, y: dy, pageX, pageY };
}
複製代碼

Edit serverless-morning-r2svr

生命週期:從類組件到函數組件的過渡

截至目前 react 最新版本爲 16.9 ,從圖例中,探索各生命週期的實現方案。雖然沒有理由再去使用 LifyCycle 了,可是瞭解下仍是能夠的。

alt

componentDidMount 與 componentWillUnmount

因爲是函數組件,沒有被實例化,就沒有一套完整的 LifeCycle 。componentWillMountcomponentDidMount 只有順序之分,放在組件頂部。

function App (){
    // 函數組件頂部
    const [value, setValue] = useState(0)
    useEffect(() => {
        console.log('componentDidMount');
        return () => {
          console.log('componentWillUnMount');
        };
    }, []);
    // other
}
複製代碼

forceUpdate

經過更新一個無關的state閉包變量強制更新

const [updateDep,setUpdateDep] = useState(0)
function forceUpdate() {
    setUpdateDep((updateDep) => updateDep + 1 )
}
複製代碼

getSnapshotBeforeUpdate

render後渲染dom以前調用,固然是 useLayoutEffect。效果有待驗證。

const SnapshotRef = useRef()
useLayoutEffect(()=>{
    SnapshotRef.current = //...
})
複製代碼

componentDidUpdate

利用上文setState回調的例子,不一樣的是 componentDidUpdate 依賴的是全部值,因此沒有deps。結合 useRefuseEffect 實現,componentDidUpdate 執行時機爲組件第二次開始render,只須要判斷執行render次數是否大於1便可。時機晚於 useLayoutEffect。以即可以拿到最新的 SnapshotRef.current

let updateRender = useRef(0)

useEffect(() => {
  updateRender.current++ 
  if(updateRender.current > 1){
      // componentDidUpdate
      // get SnapshotRef.current do some thing
  }
})
複製代碼

shouldComponentUpdate

在class裏,PureComponent 替代自動shouldComponentUpdate,而在函數組件裏,固然是memo,能將一個組件完美優化工做量可不會小。

可是有時候咱們就單單想控制某個組件不更新。也是能夠作到的

const ShouldUpdateCertainCpt = useMemo(() => (
    <div>Never updated</div>
), [])
    
return (
    <ShouldUpdateCertainCpt /> ) 複製代碼

後話

仍是那句話,入鄉隨俗,React hooks 確實是革命性的變更,不能把 hooks 當作是 ClassComponent LifeCycle 的進化版,應該稱之爲重作版,於前者來講對新手也不太友好。把底層機制經過 effects 暴露給開發者確實是個明智之舉。若是仍然想着用Hooks去實現LifeCycle 那麼爲何不用 react 的「老版本」呢?

更有有意思的Hooks

本文有部分Hook都出自react-usehook-guide 思路去開發的。相信掌握了hooks,你離成功剩下的只差一個Idea了。

Hooks 描述
React Use hooks 工具庫
useHistory 管理歷史記錄棧
useScript 動態添加腳本
useAuth 用戶狀態
useWhyDidYouUpdate hook版Why-Did-You-Update
useDarkMode 切換夜間模式

參考文獻

若是你還以爲不錯,star一下也是不錯的like-hooks

相關文章
相關標籤/搜索