你真的懂React Hook嗎?(結合源碼解析)

前言

  1. 讀這篇文章的前提是你已經對React Hook有所瞭解的狀況下,若是你尚未了解,請先移步官網學習一下。
    1. 最好不要去網上看別人的總結之類的,無非就是超的官網的,並且這樣會讓你的認知從一開始就走偏。
  2. 這篇文章主要是探究Hook的動機,使用中的一些疑問;
    1. 使用的話React官網已經講得很詳細了,這裏就很少贅述了。
  3. 有須要看接下來的疑難點的夥伴歡迎直接跳過探究直接看具體的疑問;

探究

主要從3個方面研究React Hookjavascript

根據黃金思惟圈(What、How、Why)java

What

什麼是Hook?react

打開Google翻譯,獲得的解釋:鉤、鉤子ios

再看看React官網的解釋:They let you use state and other React features without writing a class.(它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。)ajax

因此,結合一下。我我的的理解是這樣的:對於函數式的組件,能夠用鉤子(Hook)將想要的外部功能給「鉤」進來。redux

在React Hook出來以前,函數式組件都是無狀態的組件,最多就是根據props來加一些判斷的邏輯;而在React Hook出來以後就能夠在函數式組件裏面加入狀態(useState),類生命週期(useEffect),甚至是一些本身的複用邏輯(自定義Hook)等等這些外部的功能。axios

How

怎麼使用Hook?api

你們一塊兒看一下官網的一個例子。數組

題目:顯示一個計數器。當你點擊按鈕,計數器的值就會增長。瀏覽器

Class組件

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div>
    );
  }
}
複製代碼

React Hook

import React, { useState } from 'react';

function Example() {
  // 聲明一個叫 "count" 的 state 變量 const [count, setCount] = useState(0);
  return (
    <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div>
  );
}
複製代碼

這樣就算是完成了一個最簡單的React Hook 實踐,關於一些官方提供的Hook晚點會介紹。

Why

  • 爲何會有Hooks?
  • Hook能解決什麼問題?

作任何一件事情我以爲都應該理清這個兩個問題,這樣的話就會事半功倍。

咱們先看看React官方是怎麼解釋「Why」的

  1. 在組件之間複用狀態邏輯很難
  2. 複雜組件變得難以理解
  3. 難以理解的 class

本人我的認爲第三點是來湊數的....

爲何這麼說?

由於React用了這麼久了基本都是在使用Class組件,這個是在以前,哪怕是如今學習React的必經之路吧!因此,這點我接下來就會跳過了😂

在組件之間服用狀態邏輯很難

其實高階組件或者說是props都是很好的解決了複雜的聚合業務邏輯,那爲何說在組件之間服用狀態邏輯很難呢?

其實道理很是簡單。

舉個簡單的例子,方便你們理解。

場景:有 請求A,請求B,請求C,請求D。他們的請求都有相互依賴關係好比,發請求B的時候必須拿到請求A的結果中的某個值,而請求C也必須拿到請求B的結果中的某個值。以此類推請求D。

Promise出來以前是怎麼作的呢?

$.ajax({
    type:"post",
    success: function(){//成功回調
        //再次異步請求
        $.ajax({
            type:"post",
            url:"...",
            success:function(){//成功回調
              //再次異步請求
              $.ajax({
                  type:"post",
                  url:"...",
                  success:function(){
                      .......//如此循環
                  }
              })
          }
        })
    }
})
複製代碼

這還只是3層,若是是100層呢?那看起來就很是的難受了!

Promise較好的解決了這個問題

new Promise(f1)
 .then(f2)
 .then(f3)
 .then(f4)
 .then(f5)
 .then(f5)
…………
複製代碼

而後是async/await。這裏就不展開了,有興趣的能夠本身去了解一下。

結論

之因此這麼大費周章的講是爲了解釋,React中的高階組件(HOC)。他的邏輯其實和回調地獄相似,一個兩個其實都還算優雅或者說舒服,一旦多了的話。。。

export default withHover(
  withTheme(
    withAuth(
      withRepos(Profile)
    )
  )
)

// 就會變成這樣,不夠優雅
<WithHover>
  <WithTheme hovering={false}>
    <WithAuth hovering={false} theme='dark'>
      <WithRepos hovering={false} theme='dark' authed={true}>
        <Profile 
          id='JavaScript'
          loading={true} 
          repos={[]}
          authed={true}
          theme='dark'
          hovering={false}
        />
      </WithRepos>
    </WithAuth>
  <WithTheme>
</WithHover>
複製代碼

並且每一個高階組件的邏輯複用咱們可能還要一個個去研讀。

複雜組件變得難以理解

其實,這點很是好理解。舉一個很是簡單常見的例子你們就會明白了。

場景:假如我有一個子組件Child,他的功能是這樣的:父組建會給一個id,在組件建立的時候獲取一下有關信息,在id改變的時候再從新獲取。

Class組件

componentDidMount () {
    this.fetch(this.props.id)
 }
componentDidUpdate (prevProps) {
  if (prevProps.id !== this.props.id) {
    this.fetch(this.props.id)
  }
}
fetch = id => {
  this.setState({ loading: true })
  fetchInfo(id)
    .then(info => this.setState({
    info,
    loading: false
  }))
}
複製代碼

React Hook:

const fetch = id => {
  this.setState({ loading: true })
  fetchInfo(id)
    .then(info => this.setState({
    info,
    loading: false
  }))
}
useEffect(() => {
  fetch(this.props.id)
}, [this.props.id])
複製代碼

結論

簡單的說一下他的優勢吧。

  1. 複用代碼更加簡單(須要什麼就「鉤」進來)
  2. 清爽的代碼風格,一目瞭然。(useState支持數組和對象,能夠清晰的定義特殊的字段等等)
  3. 代碼量更少(能夠看一下我以前的子父組建的例子)
  4. 更願意去寫一些小組件複用(我我的喜歡React就是由於他的組件寫起來很是的順手!ps:沒有貶低其餘框架的意思。。)
  5. 其實我我的認爲React Hook在宣揚一個觀念「按需加載」。

useState

使用

簡單的使用在上面的探究-How裏面有介紹,更多的在React官網也有介紹。

請回答如下代碼的運行結果

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

  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }

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

你猜alert會彈出什麼呢?會是5嗎?— 這個值是alert的時候counter的實時狀態。或者會是3嗎?— 這個值是我點擊時候的狀態。

分割線

來本身 試試吧!

答案是

3

這是爲何呢?function組建到底是若是工做的呢?

咱們發現count在每一次函數調用中都是一個常量值。值得強調的是 — 咱們的組件函數每次渲染都會被調用,可是每一次調用中count值都是常量,而且它被賦予了當前渲染中的狀態值。

這並非React特有的,普通的函數也有相似的行爲:

function sayHi(person) {
  const name = person.name;  setTimeout(() => {
    alert('Hello, ' + name);
  }, 3000);
}

let someone = {name: 'Dan'};
sayHi(someone);

someone = {name: 'Yuzhi'};
sayHi(someone);

someone = {name: 'Dominic'};
sayHi(someone);
複製代碼

這個例子中, 外層的someone會被賦值不少次(就像在React中,當前的組件狀態會改變同樣)。**而後,在sayHi函數中,局部常量name會和某次調用中的person關聯。**由於這個常量是局部的,因此每一次調用都是相互獨立的。結果就是,當定時器回調觸發的時候,每個alert都會彈出它擁有的name

這就解釋了咱們的事件處理函數如何捕獲了點擊時候的count值。若是咱們應用相同的替換原理,每一次渲染「看到」的是它本身的count

// During first render
function Counter() {
  const count = 0; // Returned by useState() // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }
  // ...
}

// After a click, our function is called again
function Counter() {
  const count = 1; // Returned by useState() // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }
  // ...
}

// After another click, our function is called again
function Counter() {
  const count = 2; // Returned by useState() // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }
  // ...
}
複製代碼

因此實際上,每一次渲染都有一個「新版本」的handleAlertClick。每個版本的handleAlertClick「記住」 了它本身的 count

// During first render
function Counter() {
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + 0);    }, 3000);
  }
  // ...
  <button onClick={handleAlertClick} /> // The one with 0 inside // ...
}

// After a click, our function is called again
function Counter() {
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + 1);    }, 3000);
  }
  // ...
  <button onClick={handleAlertClick} /> // The one with 1 inside // ...
}

// After another click, our function is called again
function Counter() {
  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + 2);    }, 3000);
  }
  // ...
  <button onClick={handleAlertClick} /> // The one with 2 inside // ...
}
複製代碼

這就是爲何在這個demo中中,事件處理函數「屬於」某一次特定的渲染,當你點擊的時候,它會使用那次渲染中counter的狀態值。

**在任意一次渲染中,props和state是始終保持不變的。**若是props和state在不一樣的渲染中是相互獨立的,那麼使用到它們的任何值也是獨立的(包括事件處理函數)。它們都「屬於」一次特定的渲染。即使是事件處理中的異步函數調用「看到」的也是此次渲染中的count值。

請回答如下代碼的運行結果

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

  const addCount = () => {
    setCount(count+1)
    setCount(count+2)
    setCount(count+3)
    setCount(count+4)
    setCount(count+5)
  }

  console.log(count)
  
  return (
    <div> <button onClick={addCount}> Click me </button> </div>
  );
}
複製代碼

分割線

答案是

5

爲何呢?

useState的更新到底是如何工做的呢?

咱們進入ReactHooks.js來看看,發現useState的實現居然異常簡單,只有短短兩行

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

其實能夠這樣理解useState,useState其實就是useReducer的一個語法糖;可是這個不在這個問題的討論範圍內;

好,收回來。

其實咱們在const [xx, setXx] = useState(xx)的時候就生成一個隊列,咱們暫時叫它爲queue;全部這一輪運行讀取到的state都被放到一個鏈表的隊列裏面去,而後再用do-while循環,每次都是拿到最新的值,可是不是Object.assgin的形式,而是直接賦值。話很少說直接源碼。

function updateReducer(reducer, initialArg, init) {
// 獲取初始化時的 hook
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  // 開始渲染更新
  if (numberOfReRenders > 0) {
    const dispatch = queue.dispatch;
    if (renderPhaseUpdates !== null) {
      // 獲取Hook對象上的 queue,內部存有本次更新的一系列數據
      const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
      if (firstRenderPhaseUpdate !== undefined) {
        renderPhaseUpdates.delete(queue);
        let newState = hook.memoizedState;
        let update = firstRenderPhaseUpdate;
        // 獲取更新後的state
        do {
          const action = update.action;
          // 此時的reducer是basicStateReducer,直接返回action的值
          // 注意,這裏是等於號因此
          /** * * setObj({ a: 1, b: 1, c: 1 }) * setObj({ a: 2, b: 2 }) * setObj({ a: 3 }) * * 到最後也只有只有{ a: 3 },而b和c全沒了 * **/
          newState = reducer(newState, action);
          update = update.next;
        } while (update !== null);
        // 對 更新hook.memoized 
        hook.memoizedState = newState;
        // 返回新的 state,及更新 hook 的 dispatch 方法
        return [newState, dispatch];
      }
    }
  }
複製代碼

useEffect/useLayoutEffect

提示

在學習useEffect這個Hook的時候,淡化你知道的「生命週期」這個概念。

useEffect和useLayoutEffect兩兄弟的區別是什麼?

執行的時機不一樣

那麼具體哪裏不一樣呢?

其實在初始化useEffect和useLayoutEffect是沒有區別的,他們真正的區別在於初始化以後;

舉個很是形象的例子🌰:

除了初始化以後的一輪更新:

瀏覽器:我要繪製了!

React:等等,我有一個哥們臨時有事要處理,他是:useLayoutEffect

useLayoutEffect執行....

React:好了,你能夠開始繪製了~@瀏覽器

瀏覽器:好的

瀏覽器更新UI...

瀏覽器:我更新好了。你有什麼事要作的嗎?@React

React:有的,useEffect你上

useEffect執行....

可能有點廢話了。其實區別就是

useLayoutEffect()

瀏覽器繪製

useEffect()

這樣其實你們也能很直接的看到弊端了。那就是useLayoutEffect若是有大量的計算的話,那樣可能會阻塞UI更新,或者說UI渲染。因此仍是要謹慎使用。

通常來講他們沒有什麼太大的區別的,若是真的要使用useLayoutEffect的話要謹慎一些。否則可能會致使UI渲染阻塞之類的問題。

可是,也不是沒有使用場景。

好比下面的這個代碼就很須要useLayoutEffect

function App() {
  const [count, setCount] = useState(0);
  
  useLayoutEffect(() => {
    if (count === 0) {
      const randomNum = 10 + Math.random()*200
      setCount(10 + Math.random()*200);
    }
  }, [count]);

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

// 我是分割線

function App() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    if (count === 0) {
      const randomNum = 10 + Math.random()*200
      setCount(10 + Math.random()*200);
    }
  }, [count]);

  return (
      <div onClick={() => setCount(0)}>{count}</div>
  );
}
複製代碼

其實明白的同窗一下就看出來了,若是使用useEffect的話會出現閃爍,會先回到0而後再更新新的隨機數。而反觀useLayoutEffect則不會,他會很天然的過渡。

總結:

useLayoutEffect的使用場景爲:有一箇中間狀態但願隱藏的時候再使用。

大部分狀況下useEffect能夠適用於99%的場景。

useEffect的錯誤事例。看看有沒有你

function SearchResults() {
  const [query, setQuery] = useState('react');

  // Imagine this function is also long
  function getFetchUrl() {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }

  // Imagine this function is also long
  async function fetchData() {
    const result = await axios(getFetchUrl());
    setData(result.data);
  }

  useEffect(() => {
    fetchData();
  }, []);

  // ...
}
複製代碼

爲何錯了?

不難看出上面代碼的意思是。想要模仿componentDidMount的生命週期,在頁面或者組件加載以後發送一個請求。咋一看好像沒有什麼問題(實際在運行的過程當中也沒有什麼問題,在寫這篇文章以前我也是這麼作的。)

可是你們能夠想象一下,若是這個函數組件,是如今的5倍大,這個didMount裏面調用的請求,將來依賴的東西你均可以100%的察覺到嗎?

我以爲難!不免會有疏忽。到時候可能就會出現state或者props讀取錯誤的狀況。由於每一次render的state和props都是獨立的。

那麼,該如何解決呢?

有一個很土的辦法,直接把函數扔到useEffect裏面去

function SearchResults() {
  // ...
  useEffect(() => {
    // We moved these functions inside! 
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query=react';
    }
    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }
    fetchData();
  }, []); // ✅ Deps are OK
  // ...
}
複製代碼

那高級點的辦法呢?

function SearchResults() {
  // ✅ Preserves identity when its own deps are the same
  const getFetchUrl = useCallback((query) => {
    return 'https://hn.algolia.com/api/v1/search?query=' + query;
  }, []);  // ✅ Callback deps are OK

  useEffect(() => {
    const url = getFetchUrl('react');
    // ... Fetch data and do something ...
  }, [getFetchUrl]); // ✅ Effect deps are OK

  // ...
}
複製代碼

將函數用useCallback包裹,這樣的話咱們只須要作useEffect的依賴裏面寫上咱們的函數,而後在useCallback裏面寫上咱們的依賴。

咱們都知道,每一次effect都是全新的state和props,那我要如何得到上一輪更新的state呢?

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

  const prevCountRef = useRef();
  useEffect(() => {
    prevCountRef.current = count;
  });
  const prevCount = prevCountRef.current;

  return <h1>Now: {count}, before: {prevCount}</h1>;
}
複製代碼

其實很好理解,若是看了前面useEffect和useLayoutEffect區別的同窗一下就能夠知道這個的實現原理。

首先,在一切都更新以後,而後會會執行useEffect內部的回調函數,將prevCount給賦值,因爲沒有觸發渲染,因此只是單純的賦值。這樣就看起來prevCount的值永遠都慢一步。

總結

其實在學習useEffect的時候。應該忘記你對React的一些知識。好比生命週期,在函數組件裏面沒有生命週期這個概念了。

每一次的render他都有本身的state和props。state和props更應該被看做一個常量,哪怕是const bar = xx這樣的常量。這樣理解起來useEffect這個反作用其實會更加順暢,也不容易進入他的「陷阱」

useRef

前言

爲何我把useRef單獨拎出來講,不把他和useImperativeHandle放在一塊兒講,由於

(官網原話)它建立的是一個普通 Javascript 對象。而 useRef() 和自建一個 {current: ...} 對象的惟一區別是,useRef 會在每次渲染時返回同一個 ref 對象。

記住useRef不僅僅用於獲取DOM節點和組件實例,還有一個巧妙的用法就是做爲容器保留可變變量,能夠這樣說:沒法自如地使用useRef會讓你失去hook將近一半的能力

useRef 與 createRef 的區別

useRef 僅能用在 FunctionComponent,createRef 僅能用在 ClassComponent。

useRef 僅能用在 FunctionComponent,createRef 僅能用在 ClassComponent。

第一句話是顯然的,由於 Hooks 不能用在 ClassComponent。

第二句話的緣由是,createRef 並無 Hooks 的效果,其值會隨着 FunctionComponent 重複執行而不斷被初始化:

function App() {
  // 錯誤用法,永遠也拿不到 ref
  const valueRef = React.createRef();
  return <div ref={valueRef} />;
}
複製代碼
複製代碼

上述 valueRef 會隨着 App 函數的 Render 而重複初始化,這也是 Hooks 的獨特之處,雖然用在普通函數中,但在 React 引擎中會獲得超出普通函數的表現,好比初始化僅執行一次,或者引用不變

爲何 createRef 能夠在 ClassComponent 正常運行呢?這是由於 ClassComponent 分離了生命週期,使例如 componentDidMount 等初始化時機僅執行一次。

如何解決每次render帶來類閉包問題?

首先,題目怎麼理解?題目

若是咱們但願他alert的時候能夠獲取到最新的值的話,可使用useRef來解決

const Counter = () => {
  const [count, setCount] = useState<number>(0)
  const countRef = useRef<number>(count)

  useEffect(() => {
    countRef.current = count
  })

  const handleCount = () => {
    setTimeout(() => {
      alert('current count: ' + countRef.current)
    }, 3000);
  }

  //...
}

export default Counter
複製代碼

useMomo/useCallack/memo

memo沒有回調函數的話是怎麼淺比較的?

先來看看memo沒有回調函數的時候他作了什麼。

memo它是一個高階組件(HOC)他與React.PureComponent十分類似。除了使用的地方不一樣(Class組件和Function組件)以外幾乎一致。

Memo內部和PureComponent同樣使用Object.is用於前對比,若是傳入的props內存地址不變的話,那就不會渲染了(或者說複用最近的一次渲染)。

下面能夠看源碼事例

function updateMemoComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps: any, updateExpirationTime, renderExpirationTime: ExpirationTime, ): null | Fiber {

  /* ...省略...*/

  // 判斷更新的過時時間是否小於渲染的過時時間
  if (updateExpirationTime < renderExpirationTime) {
    const prevProps = currentChild.memoizedProps;

    // 若是自定義了compare函數,則採用自定義的compare函數,不然採用官方的shallowEqual(淺比較)函數。(下面有解析)
    let compare = Component.compare;
    compare = compare !== null ? compare : shallowEqual;

    /** * 1. 判斷當前 props 與 nextProps 是否相等; * 2. 判斷即將渲染組件的引用是否與workInProgress Fiber中的引用是否一致; * * 只有二者都爲真,纔會退出渲染。 */
    if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
      // 若是都爲真,則退出渲染
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }
  }

  /* ...省略...*/

複製代碼

shallowEqual(淺比較)

// 用原型鏈的方法
const hasOwn = Object.prototype.hasOwnProperty

// 這個函數其實是Object.is()的polyfill
function is(x, y) {
  if (x === y) {
    return x !== 0 || y !== 0 || 1 / x === 1 / y
  } else {
    return x !== x && y !== y
  }
}

export default function shallowEqual(objA, objB) {
  // 首先對基本數據類型的比較
  if (is(objA, objB)) return true
  // 因爲Obejct.is()能夠對基本數據類型作一個精確的比較, 因此若是不等
  // 只有一種狀況是誤判的,那就是object,因此在判斷兩個對象都不是object
  // 以後,就能夠返回false了
  if (typeof objA !== 'object' || objA === null ||
      typeof objB !== 'object' || objB === null) {
    return false
  }

  // 過濾掉基本數據類型以後,就是對對象的比較了
  // 首先拿出key值,對key的長度進行對比

  const keysA = Object.keys(objA)
  const keysB = Object.keys(objB)

  // 長度不等直接返回false
  if (keysA.length !== keysB.length) return false
  // key相等的狀況下,在去循環比較
  for (let i = 0; i < keysA.length; i++) {
  // key值相等的時候
  // 借用原型鏈上真正的 hasOwnProperty 方法,判斷ObjB裏面是否有A的key的key值
  // 屬性的順序不影響結果也就是{name:'daisy', age:'24'} 跟{age:'24',name:'daisy' }是同樣的
  // 最後,對對象的value進行一個基本數據類型的比較,返回結果
    if (!hasOwn.call(objB, keysA[i]) ||
        !is(objA[keysA[i]], objB[keysA[i]])) {
      return false
    }
  }

  return truea
}
複製代碼

由源碼能夠知道,加入沒有傳一個比較的回調函數會使用官方的淺比較。具體的能夠看註釋

memo的回調函數

咱們都知道react的生命週期中有一個shouldComponentUpdate。在這個函數中返回true的話就表明本次render須要執行,而返回false就能夠跳過本次的render。

而memo正好相反,返回true表示本次跳過,返回false就表示本次須要執行render。

具體怎麼用呢?

你們能夠本身運行一下,看看效果。必定要本身試一下,否則很容易和shouldComponentUpdate弄混了。學習仍是要本身動手才行。

function ChangeLog({w = ''}){
  // 省略
  console.log('====render====')
}
export default memo(ChangeLog, (prevProps, nextProps) => {
  if (prevProps.w !== nextProps.w) {
    return false
  }
  return true
});
複製代碼

useMemo/useCallback

前言

之因此把這useMomo/useCallack兩兄弟和在一塊兒。是由於他們其實十分類似。

一個是緩存變量(useMemo),一個是緩存函數(useCallback)。

其實這麼一說就清晰不少了。

具體的使用和useEffect同樣,都是第一個參數爲調用時的回調函數,第二個參數是調用判斷所監聽的值(能夠是變量,也能夠是函數)

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const memoizedValue = useCallback(() => computeExpensiveValue(a, b), [a, b]);
複製代碼

useMemo() 返回的是一個 memoized 值,只有當依賴項(好比上面的 a,b 發生變化的時候,纔會從新計算這個 memoized 值)

memoized 值不變的狀況下,不會從新觸發渲染邏輯。

提及渲染邏輯,須要記住的是 useMemo() 是在 render 期間執行的,因此不能進行一些額外的副操做,好比網絡請求等。

若是沒有提供依賴數組(上面的 [a,b])則每次都會從新計算 memoized 值,也就會 re-redner

useCallback也是同樣的,這裏就很少贅述了。

useReducer/useContext

前言

幫他們兩兄弟和在一塊兒說主要說由於他們兩兄弟在通常狀況下是能夠與Redux一戰的。

可是!!

可是啊,可是若是你須要中間價,或者說須要「時間旅行」,又或者臨時須要跨頁面級的數據共享,那你仍是須要redux來解決的。不過基本上的場景咱們使用useReducer和useContext就能夠完美的替代redux了。

怎麼作?

其實以前因爲要起一個新項目,可是忽然發現有一個爺爺組件的值爲須要通知給孫子組件,而後孫子組件可能會用掉回調函數調用爺爺組件的方法。那個時候其實已經用Hook寫了一半了,懶得加Redux了,又不想一層層傳props下去。怎麼辦?

經過了解,我知道了useReducer/useContext剛恰好能夠解決個人需求。

const TodosDispatch = React.createContext(null);

const initialState = { bar: null };

function reducer(state, action) {
  switch (action.type) {
    case 'setCenter':
      return { ...state, bar: action.bar };
    default:
      return state
  }
}

// 雖然這個是自組件,可是哪怕是曾曾曾孫子組件均可以直接用useContext拿到dispatch
function DeepChild(props) {
  // 若是咱們想要執行一個 action,咱們能夠從 context 中獲取 dispatch。
  const dispatch = useContext(TodosDispatch);

  function handleClick() {
    dispatch({ type: 'add', text: 'hello' });
  }

  return (
    <button onClick={handleClick}>Add todo</button>
  );
}

function TodosApp() {
  // 提示:`dispatch` 不會在從新渲染之間變化
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <TodosDispatch.Provider value={dispatch}>
      <DeepTree todos={todos} />
    </TodosDispatch.Provider>
  );
}
複製代碼

useImperativeHandle/forwardRef

這裏就很少說了,基本上沒有什麼坑點和疑難點。

說一下基本用法和ant-design form中的使用

正常使用

useImperativeHandle(ref, createHandle, [deps])
複製代碼

useImperativeHandle 可讓你在使用 ref 時自定義暴露給父組件的實例值。在大多數狀況下,應當避免使用 ref 這樣的命令式代碼。useImperativeHandle 應當與 forwardRef一塊兒使用:

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />; } FancyInput = forwardRef(FancyInput); 複製代碼

在本例中,渲染的父組件能夠調用 inputRef.current.focus()

ant-design form

// ref從第二個參數取,這裏都是一致的
const Example = (props,ref) => {
  const bar = () => {}
  useImperativeHandle(ref, () => ({
    text: bar,
  }));
  return (
    <> <Form> {//....} </Form> </> ); }; export default memo(Form.create()(forwardRef(Example))); 複製代碼
const Parent = () => {
  return (
    <>
      <Example
      	// 注意這個再也不是傳ref了,而是傳wrappedComponentRef。由於antd的form他返回的是一個新的對象,這個是他自定義的一個接收ref的值
        wrappedComponentRef={editTemplateRef}
      />
    </>
  );
};
複製代碼

useDebugValue/自定義Hook

前言

useDebugValue是專門用於服務自定義的Hook的。

具體看看使用就好

useDebugValue,目的是能在react的瀏覽器調試工具上顯示你的自定義hooks,或者給hooks標記一些東西 當使用一個參數的時候,就是把第一個參數標記在react的調試工具上,下面寫一個簡單的例子

import React, { useDebugValue, useState } from 'react';

const useTest = () => {
    const [str, setStr] = useState<string>('');
    useDebugValue('debug');
    return {
        str, setStr
    }
}
export default (): JSX.Element => {
    const { str, setStr } = useTest();
    return (
        <> <h2>{str}</h2> <button onClick={() => { setStr('從新渲染'); }}>這是???</button> </> ); } 複製代碼

會在自定義的hooks標記到react的調試工具上面,主要用於調試工具調試使用

當傳入第二個參數的狀況下,第二個參數是一個回調函數,會把第一個參數當成本身的形參傳入,進行一系列的操做,return回去,而後纔會在react調試工具的hooks中打印出來,否則不會顯示

import React, { useDebugValue, useState } from 'react';

const useTest = () => {
    const [str, setStr] = useState<string>('');
    useDebugValue(str, (value:string) => {
        console.log(value);
        return '這是改造後的' + value;
    });
    return {
        str, setStr
    }
}
export default (): JSX.Element => {
    const { str, setStr } = useTest();
    return (
        <> <h2>{str}</h2> <button onClick={() => { setStr('從新渲染'); }}>這是???</button> </> ); } 複製代碼

結果:

同時在控制檯上打印了一個空字符

因爲str的初始值是空的,因此打印就是空的了,這只是調試使用,hooks差很少就這些了,沒有其餘的了

觀看以後

若是有哪裏寫的不對或者有疑問的歡迎你們在評論區互動。

相關文章
相關標籤/搜索