React系列-輕鬆學會Hooks(中)

咱們在第一篇中介紹了Mixin HOC Render Props,接下來來介紹另一種很是重要的狀態邏輯複用模式,那就是react-hooksjavascript

React系列-Mixin、HOC、Render Props(上)
React系列-輕鬆學會Hooks(中)
React系列-自定義Hooks很簡單(下)java

HOC、Render Props、組件組合、Ref 傳遞……代碼複用爲何這樣複雜?,根本緣由在於細粒度代碼複用不該該與組件複用捆綁在一塊兒 也就是咱們前面所說的這些模式是在既有(組件機制的)遊戲規則下探索出來的上層模式react

❗️❗️HOC、Render Props 等基於組件組合的方案至關於先把要複用的邏輯包裝成組件再利用組件複用機制實現邏輯複用。天然就受限於組件複用,於是出現擴展能力受限、Ref 隔斷、Wrapper Hell……等問題es6

🤔直接的代碼複用方式

想一想在咱們平時開發中,咱們要複用一段邏輯是否是抽離出一個函數,好比用到的防抖函數、獲取token函數可是對於react的複用邏輯不一樣,在沒有hooks出來以前,函數是內部是沒法支持state的,因此抽離成函數的模式好像是辦不到,實際也能夠作到的數據庫

// 設置提示語tip
export const setTip = function (context: any{
  // vote-tip提示
  const tipVisible = JSON.parse(localStorage.getItem('tipVisible'as string)
  if (Object.is(tipVisible, null)) {
    localStorage.setItem('tipVisible''true')
  } else if (Object.is(tipVisible, false)) {
    context.setState({
      tipVisiblefalse
    })
  }
}
複製代碼

好比筆者在業務開發中嘗試把關聯到state複用邏輯像基本工具函數同樣單獨抽離出來,這裏的context實際就是當前組件,也就是我經過this去讓函數支持了state,可是這樣的代碼很難維護,由於 你可能找不到它們的關聯性編程

hooks應運而生

從Mixin、HOC 、Render Props模式解決狀態邏輯複用問題,可是沒有去根本的解決複用問題,函數應是代碼複用的基本單位,而不是組件,因此說爲甚麼hook是顛覆性的,由於它從本質上解決了狀態邏輯複用問題,以函數做爲最小的複用單位,而不是組件數組

什麼是 Hook?

官方介紹:Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。promise

什麼是函數組件

函數組件只是一個執行函數取返回值的過程,簡單理解:state變化,函數組件執行,觸發render更新視圖,跟Class組件仍是不同的,類組件是state變化,觸發render方法更新而不是,這表明什麼❓,表明類組件的屬性不會被重複聲明,而函數組件每次state一變化,就從新執行,會重複聲明,因此這也是爲何須要useMemouseCallBack這兩個hook,咱們接下來會講到緩存

const Counter=()=>{
  const [
    number,
    setNumber
  ] = useState(0)
  console.log("我觸發執行了")
  return (
    <>
      <p>{number}</p>
      <button
        onClick={
          () =>
 setNumber(number + 1)
        }
      >
        改數字
      </button>
    </>
  )
}
複製代碼

另一個有意思的點是:開發中若是咱們使用類組件那麼就要跟this打交道,然而使用了Hook幫咱們擺脫了this場景問題,可是又引入了一個問題,你使用了函數,那麼天然而然就會跟閉包打交道,有什麼你會不知不覺陷入閉包陷阱(接下來會說到),挺神奇的羈絆,可是閉包帶來的好處太多了性能優化

記憶函數or緩存函數❓

react-hook的實現離不開記憶函數(也稱作緩存函數)或者應該說得益於閉包,咱們來實現一個記憶函數

const memorize = function(fn{
    const cache = {}       // 存儲緩存數據的對象
    return function(...args{        // 這裏用到數組的擴展運算符
      const _args = JSON.stringify(args)    // 將參數做爲cache的key
      return cache[_args] || (cache[_args] = fn.apply(fn, args))   // 若是已經緩存過,直接取值。不然從新計算而且緩存
    }
  }
複製代碼

測試一下:

const add = function(a{
  return a + 1
}
const adder = memorize(add)
adder(1)    // 2    cache: { '[1]': 2 }
adder(1)    // 2    cache: { '[1]': 2 }
adder(2)    // 3    cache: { '[1]': 2, '[2]': 3 }
複製代碼

useState

爲何使用

開發中咱們會常常遇到,當咱們一個函數組件想要有本身維護的state的時候,不得已只能轉換成class

useState 的出現是 :useState 是容許你在 React 函數組件中添加 state 的 Hook 簡單的講就是:可讓你在在函數組件裏面使用 class的setState

如何使用

useState接受一個參數,返回了一個數組

  // 使用es6解構賦值,useState(0)的意思是給count賦予初始值0
  // count是一個狀態值,setCount是給這個狀態值進行更新的函數
  const [count, setCount] = useState(0);
複製代碼

舉個例子🌰:

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>

  );
}
複製代碼

知識點合集

初始化值

useState的初始值,只在第一次有效

場景;點擊按鈕更新子組件的count

const Child = ({data}) =>{
    const [count, setCount] = useState(data)
    return (
        <div>
            <div>child</div>
            <div>count:{count} --- data:{data}</div>
        </div>

    );
}

const Parent =()=>{
    const [data, setData] = useState(0)

    return(
        <div>
            <div>
                {data}
            </div>
            <button onClick={()=>setCount(data+1)}>更新data</button>
            <Child data={data}/>
        </div>
    )
}
複製代碼

測試一下:

// 點擊按鈕
 <div>count:0 --- data:1</div>
複製代碼

更新是直接替換

useState返回更新state的函數與class 組件的 this.setState不一樣,它不會把新的 state 和舊的 state 進行合併,而是直接替換至關於直接返回一個新的對象,因此這也是閉包陷阱產生的緣由之一

let testUser={name:"vnues"// 定義全局變量
const Parent =()=>{
    const [user, setUser] = useState(testUser)
    if(user!==testUser){
        testUser=user
        console.log(testUser)
    }
    return(
        <div>
            <button onClick={()=>setCount({age:18})}>更新data</button>
            <Child data={data}/>
        </div>
    )
}
複製代碼

測試一下:

// 點擊按鈕
testUser:{age:18}
複製代碼

能夠看到,函數運行是進入if條件裏的,這說明什麼,說明user和testUser的指向不一樣了,證實是直接替換

useState原理

通常而言,函數從新執行,表明着從新初始化狀態以及聲明,那麼我就很好奇,函數組件的hook是如何保存上一次的狀態,來看看它的原理吧

let memoizedStates = [] // 存儲state 
let index = 0 
function useState (initialState{
  // 判斷memoizedStates有沒有緩存值,沒有則仍是個初始化的useState 
  memoizedStates[index] = memoizedStates[index] || initialState
  let currentIndex = index
  function setState (newState{
    memoizedStates[currentIndex] = newState // 直接替換
    render() // 進行視圖更新
  }
  return [memoizedStates[index++], setState]
}

function render({
  index = 0 // 從新執行函數組件,從新清零
  ReactDOM.render(<Counter />, document.getElementById('root'));
}
複製代碼

❗️注意上面的代碼,有個 index=0 的操做,由於添加的時候按照順序添加的,渲染的時候也要按照順序渲染的。,因此咱們不能把hook寫在循環或者判斷裏

舉個例子🌰

const Test=()=>{
   const [count, setCount] = useState(0)
   // 只執行一次
   useEffect(()=>{
       setCount(100
   },[])
   return (<div>{count}</div>)
}
複製代碼

函數組件Test運行以下:

因此瞭解useState原理有助於咱們平常開發中解決bug

useEffect

Effect Hook 可讓你在函數組件中執行反作用操做,

什麼是反作用操做(side effect)

反作用是函數式編程裏的概念,在函數式編程的教材中,以下的行爲是稱之爲反作用的

  • 修改一個變量
  • 修改一個對象的字段值
  • 拋出異常
  • 在控制檯顯示信息、從控制檯接收輸入
  • 在屏幕上顯示(GUI)
  • 讀寫文件、網絡、數據庫。

爲何使用

Effect Hook的出現: 一點是讓你能夠在函數組件裏面使用 class的生命週期函數,你能夠認爲是componentDidMount,componentDidUpdate 和 componentWillUnmount 這三個函數的組合(官方後續還會實現其它生命週期函數,敬請期待),另一點是可讓你集中的處理反作用操做(好比網絡請求,DOM操做等)

如何使用

useEffect(fn, []) // 接收兩個參數 一個是回調函數 另一個是數組類型的參數(表示依賴)
複製代碼

❗️❗️注意:useEffect的執行時機是:React 保證了每次運行 effect 的同時,DOM 都已經更新完畢,默認狀況下,useEffect 會在每次渲染後都執行, ,它在第一次渲染以後和每次更新以後都會執行,咱們能夠根據依賴項進行控制

知識點合集

useEffect只接受函數
 // ❌由於async返回的是個promise對象
 useEffect(async() => {
     const data = await getAjax()
 })
 // 建議😄
  useEffect(() => {
     // 利用匿名函數
     (async()=>{
         const data = await getAjax()
     })()

 })
複製代碼

模擬React的生命週期

  • constructor:函數組件不須要構造函數。你能夠經過調用 useState 來初始化 state。

  • componentDidMount:經過 useEffect 傳入第二個參數爲[]實現。

  • componentDidUpdate:經過 useEffect 傳入第二個參數爲空或者爲值變更的數組。

  • componentWillUnmount:主要用來清除反作用。經過 useEffect 函數 return 一個函數來模擬。

  • shouldComponentUpdate:你能夠用 React.memo 包裹一個組件來對它的 props 進行淺比較。來模擬是否更新組件。

  • componentDidCatch and getDerivedStateFromError:目前尚未這些方法的 Hook 等價寫法,但很快會加上。

// 模擬 componentDidMount
useEffect(()=>{
    // 邏輯
},[])
複製代碼
 // 模擬componentDidUpdate
 useEffect(fn)
複製代碼

模擬Vue的$watch方法

useEffect(fn,[user]) // 對user作監控
複製代碼

使用多個 Effect 實現關注點分離

就像你可使用多個 state 的 Hook 同樣,你也可使用多個 effect。這會將不相關邏輯分離到不一樣的 effect 中

function FriendStatusWithCounter(props{
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status{
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
}
複製代碼

如何清除反作用

在 React 組件中有兩種常見反作用操做:須要清除的和不須要清除的

無需清除的 effect

有時候,咱們只想在 React 更新 DOM 以後運行一些額外的代碼。好比發送網絡請求,手動變動 DOM,記錄日誌,這些都是常見的無需清除的操做。由於咱們在執行完這些操做以後,就能夠忽略他們了

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

  );
}
複製代碼
須要清除的 effect

以前,咱們研究瞭如何使用不須要清除的反作用,還有一些反作用是須要清除的。例如訂閱外部數據源。這種狀況下,清除工做是很是重要的,能夠防止引發內存泄露!

如何清除:在useEffect的回調函數return一個取消訂閱的函數

 useEffect(() => {
    // 訂閱
    ChatAPI.subscribeToFriendStatus(props.id, handleStatusChange);
    return () => {
      // 取消訂閱
      ChatAPI.unsubscribeFromFriendStatus(props.id, handleStatusChange);
    };
  });
複製代碼

useEffect與閉包陷阱

閉包陷阱:就是咱們在React Hooks進行開發時,經過useState定義的值拿到的都不是最新的現象。

來看一個場景:咱們想要count一直加1下去

const App = () => {
  const [count, setCount] = useState(0)
  useEffect(() => {
    const timeId = setInterval(() => {
      console.log(count)
      setCount(count + 1)
    }, 1000)
    return () => { clearInterval(timeId) }
  }, [])
  return (
    <span style={{ fontSize: 30color: "red" }}>{count}</span>
  )
}
複製代碼

如上圖,useEffect的回調函數訪問App函數的變量count造成了閉包Closure(App)

來看看結果:

count並不會和想象中那樣每過一秒就自身+1並更新dom,而是從0變成1後。count一直都是爲1,並不會一直加下去,這就是常見的閉包陷阱

緣由是useEffect(fn,[])只執行一次(後面再也不執行),setInterval裏的回調函數與APP函數組件造成了閉包,count爲0,此時執行setCount操做,state變化,函數組件App從新執行,執行const [count, setCount] = useState(0) ,你能夠理解成從新聲明count變量也就是說setInterval裏訪問的count變量跟此次從新聲明的count變量無關(❗️理解這句話很重要),咱們能夠稍微改變了,useEffect(fn,[])只執行一次,也就是拿到第一次count變量就再也不拿了,咱們把依賴性去掉,讓它更新後就從新拿到count

const App = () => {
  const [count, setCount] = useState(0)
  useEffect(() => {
    const timeId = setInterval(() => {
      console.log(count)
      setCount(count + 1)
    }, 1000)
    return () => { clearInterval(timeId) }
  })
  return (
    <span style={{ fontSize: 30color: "red" }}>{count}</span>
  )
}
複製代碼

👆能夠看到效果是實現的了

若是你沒看明白上述所講的,咱們換個例子🌰看看就清晰了:

const App = () => {
  const [user, setUser] = useState({ name"vnues"age18 })
  useEffect(() => {
    const timeId = setInterval(() => {
      console.log(user)
      setUser({ name"落落落洛克"age: user.age + 1 })
    }, 1000)
    return () => { clearInterval(timeId) }
  }, [])
  return (
    <span style={{ fontSize: 30color: "red" }}>{user.name}{user.age}</span>
  )
}
複製代碼

⚠️上述須要注意的點:setUser操做是直接替換,另外,解決閉包陷阱的幾種方式咱們放到下面再具體介紹

useRef

useRef 返回一個可變的 ref 對象,其 .current屬性被初始化爲傳入的參數(initialValue),另外ref對象的引用在整個生命週期保持不變

爲何使用

useRef能夠用於訪問DOM節點或React組件實例和做爲容器保存可變變量

如何使用

const refContainer = useRef(initialValue)
複製代碼

知識點合集

獲取DOM元素的節點

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的參數(initialValue),經過current屬性能夠訪問到DOM節點

const App=()=>{
  const inputRef = useRef(null);
  console.log(inputRef) // 沒有訪問到 此時dom還未掛載
  useEffect(() => {
    console.log(inputRef) // dom掛載完畢
  }, [])
  return <div>
    <input type="text" ref={inputRef} />
  </div>
}
複製代碼

結果以下:

注意:createRef 每次渲染都會返回一個新的引用,而 useRef 每次都會返回相同的引用。具體關於(ref React.createRef useRef、React.forwardRef這些形式我會單獨抽一個章節來說到)

獲取子組件的實例

// 實際就是利用了一個回調
const Child2 = React.forwardRef((props, ref) => {
  return <button ref={ref}>Child2</button>
})


const App = () => {
  const childRef = useRef();
  const child2Ref = useRef()
  useEffect(() => {
    console.log('useRef')
    console.log(childRef.current)
    childRef.current.handleLog();
    console.log("child2Ref", child2Ref)

  }, [])
  return (
    <div>
      <h1>Hello World!</h1>
      <Child ref={childRef} count="1" />
      <Child2 ref={child2Ref} />
    </div>
  )
}

// 由於函數組件沒有實例,若是想用ref獲取子組件的實例,子組件組要寫成類組件
class Child extends Component {
  handleLog = () => {
    console.log('Child Component');
  }
  render() {
    const { count } = this.props;
    return <h2>count: { count }</h2>
  }
}
複製代碼

結果:

注意一點:組件實例是對於類組件來講的 函數組件沒有實例,使用React.forwardRefAPI是轉發ref拿到子組件的DOM中想要獲取的節點,並非獲取實例由於函數組件沒有實例這一律念,

存儲可變變量的容器

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

官方的說法:useRef 不只適用於 DOM 引用。 「ref」 對象是一個通用容器, 其 current 屬性是可變的,能夠保存任何值(能夠是元素、對象、基本類型、甚至函數)

咱們來看看👇的分析:

在類組件和函數組件中,咱們都有兩種方法在re-render(從新渲染)之間保持數據:

在類組件中
  • 在組件狀態中:每次狀態更改時,都會從新渲染組件。

  • 在實例變量中:該變量的引用將在組件的整個生命週期內保持不變。
    實例變量的更改不會產生從新渲染。

在函數組件中

在函數組件中使用Hooks能夠達到與類組件等效的效果:

  • 在state中:使用useState或useReducer。state的更新將致使組件的從新渲染。

  • 在ref(使用useRef返回的ref)中:等效於類組件中的實例變量,更改.current屬性不會致使從新渲染。(至關於 ❗️❗️ref對象充當this,其current屬性充當實例變量

const App = () => {
  const counter = useRef(0);
  const handleBtnClick = () => {
    counter.current = counter.current + 1;
    console.log(counter)
  }
  console.log("我更新啦")
  return (
    <>
      <h1>{`The component has been re-rendered ${counter.current} times`}</h1>
      <button onClick={handleBtnClick}>點擊</button>
    </>
  );
}
複製代碼
替代函數組件的局部變量
//  const name="vnues"
const App = () => {
  const [count, setCount] = useState(0)
  const name="vnues" // 聲明局部變量
   const handleBtnClick = () => {
    setCount(count+1)
  }
  return (
    <>
      <span>{count}</span>
      <button onClick={handleBtnClick}>點擊</button>
    </>
  )
}
複製代碼

有時候咱們存在這種狀況,須要聲明一個變量去保存值,可是若是函數組件state變化,函數從新執行,會形成從新聲明name,顯然沒有必要,有同窗說能夠放在全局下,避免不必的重複聲明,實際也是一個解決方法,可是若是沒有及時回收,容易形成內存泄漏,咱們能夠利用Ref容器的特色,使用current去保存可變的變量

const App = () => {
  const [count, setCount] = useState(0)
  const ref=useRef("vnues"// 利用容器的特色
   const handleBtnClick = () => {
    setCount(count+1)
  }
  return (
    <span>{count}</span>
     <button onClick={handleBtnClick}>點擊</button>
  )
}
複製代碼

ref引用保持不變

因爲useRef返回ref對象的引用在FunctionComponent 生命週期保持不變,自己又是做爲容器的狀況保存可變的變量,因此咱們能夠利用這些特性能夠作不少操做,這一點與useState不一樣

解決閉包陷阱

你能夠這樣理解:此處的countRef就是至關於全局變量,一處被修改,其餘地方全更新…

const App = () => {
  const [count, setCount] = useState(0)
  const countRef = useRef(count)
  useEffect(() => {
    const timeId = setInterval(() => {
      countRef.current = countRef.current + 1
      setCount(countRef.current)
    }, 1000)
    return () => clearInterval(timeId)
  }, [])
  return (
    <span>{countRef.current}</span>
  )
}
複製代碼

結果:

獲取上一輪的 props 或 state

Ref 不只能夠拿到組件引用、建立一個 Mutable 反作用對象,還能夠配合 useEffect 存儲一個較老的值,最經常使用來拿到 previousProps,React 官方利用 Ref 封裝了一個簡單的 Hooks 拿到上一次的值:

const usePrevious=(value)=>{
  const ref = useRef()
  useEffect(() => {
    ref.current = value
  });
  return ref.current
}
複製代碼

因爲 useEffect 在 Render 完畢後才執行,所以 ref 的值在當前 Render 中永遠是上一次 Render 時候的,咱們能夠利用它拿到上一次 props或者state

更新Ref是反作用操做

官方文檔說到:Unless you’re doing lazy initialization, avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects

簡單點說就是更新Ref是反作用操做,咱們不該該在render-parse(渲染階段)更新,而是在useEffect或者useLayoutEffect去完成反作用操做

咱們先來看看FunctionComponent 生命週期圖:

從圖中能夠發現,在Render phase 階段是不容許作 「side effects」 的,也就是寫反作用代碼,這是由於這個階段可能會被 React 引擎隨時取消或重作。

修改 Ref 屬於反作用操做,所以不適合在這個階段進行。咱們能夠看到,在 Commit phase 階段能夠作這件事

// ❌的寫法
const RenderCounter = () => {
  const counter = useRef(0);

  // Since the ref value is updated in the render phase,
  // the value can be incremented more than once
  counter.current = counter.current + 1;

  return (
    <h1>{`The component has been re-rendered ${counter} times`}</h1>
  );
};
複製代碼
// 正確✅的寫法
const RenderCounter = () => {
  const counter = useRef(0);

  useEffect(() => {
    // 每次組件從新渲染,
    // counter就加1
    counter.current = counter.current + 1;
  }); 

  return (
    <h1>{`The component has been re-rendered ${counter} times`}</h1>
  );
};
複製代碼

useCallback

該hooks返回一個 memoized 回調函數,❗️根據依賴項來決定是否更新函數

爲何使用

react中Class的性能優化。在hooks誕生以前,若是組件包含內部state,咱們都是基於class的形式來建立組件。react中,性能的優化點在於:

  • 調用setState,就會觸發組件的從新渲染,不管先後的state是否不一樣
  • 父組件更新,子組件也會自動的更新

基於上面的兩點,咱們一般的解決方案是:

  • 使用immutable進行比較,在不相等的時候調用setState

  • shouldComponentUpdate中判斷先後的props和state,若是沒有變化,則返回false來阻止更新

在hooks出來以後,咱們可以使用function的形式來建立包含內部state的組件。可是,使用function的形式,失去了上面的shouldComponentUpdate,咱們沒法經過判斷先後狀態來決定是否更新。並且,在函數組件中,react再也不區分mount和update兩個狀態,這意味着函數組件的每一次調用都會執行其內部的全部邏輯,那麼會帶來較大的性能損耗。所以useMemo 和useCallback就是用來優化性能問題

舉個例子🌰:

const set = new Set() // 藉助ES6新增的數據結構Set來判斷
export default function Callback({
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');
    const callback =() => {
        console.log(count);
    }
    set.add(callback)
    return <div>
        <h4>{count}</h4>
        <h4>{set.size}</h4>
        <button onClick={() => setCount(count + 1)}>+</button>
    </div>
;
}
複製代碼

如何使用

內聯回調函數及依賴項數組做爲參數傳入 useCallback,它將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時纔會更新。當你把回調函數傳遞給通過優化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時,它將很是有用。

⚠️不是根據先後傳入的回調函數fn來比較是否相等,而是根據依賴項決定是否更新回調函數fn,筆者一開始想錯了

const memoizedCallback = useCallback(fn, deps)
複製代碼

知識點合集

useCallback的依賴參數

該回調函數fn僅在某個依賴項改變時纔會更新若是沒有任何依賴項,則deps爲空

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
)
複製代碼

useCallback與React.memo完美搭配

場景:有一個父組件,其中包含子組件,子組件接收一個函數做爲props;一般而言,若是父組件更新了,子組件也會執行更新;可是大多數場景下,更新是沒有必要的,咱們能夠藉助useCallback來返回函數,而後把這個函數做爲props傳遞給子組件;這樣,子組件就能避免沒必要要的更新。

React.memo 爲高階組件。它與 React.PureComponent 很是類似但只適用於函數組件,而不適用 class 組件能對props作淺比較,防止組件無效的重複渲染

// 父組件
const Parent=()=>{
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');

    const callback = useCallback(() => {
        return count;
    }, [count]);
    return <div>
        <h4>{count}</h4>
        <Child callback={callback}/>
        <div>
            <button onClick={() => setCount(count + 1)}>+</button>
            <input value={val} onChange={event => setVal(event.target.value)}/>
        </div>
    </div>;
}

// 子組件
const Child=({ callback })=>{
    const [count, setCount] = useState(() => callback());
    useEffect(() => {
        setCount(callback());
    }, [callback]);
    return <div>
        {count}
    </div>
}
export default React.memo(Child) // 用React.memo包裹
複製代碼

若是你的函數組件在給定相同 props 的狀況下渲染相同的結果,那麼你能夠經過將其包裝在 React.memo 中調用,以此經過記憶組件渲染結果的方式來提升組件的性能表現。這意味着在這種狀況下,React 將跳過渲染組件的操做並直接複用最近一次渲染的結果。

useMemo

useCallback相似,區別就是useCallback返回一個 memoized函數,而useMemo返回一個memoized 值

❗️你能夠這樣認爲:

useCallback(fn, deps) 至關於 useMemo(() => fn, deps)。
複製代碼

爲何使用

和爲何使用useCallback相似,另一點就是緩存昂貴的計算(避免在每次渲染時都進行高開銷的計算)

export default function WithMemo({
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
    // 緩存了昂貴的計算
    const expensive = useMemo(() => {
        console.log('compute');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
            sum += i;
        }
        return sum;
    }, [count]);

    return <div>
        <h4>{count}-{expensive}</h4>
        {val}
        <div>
            <button onClick={() => setCount(count + 1)}>+c1</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
複製代碼

上面咱們能夠看到,使用useMemo來執行昂貴的計算,而後將計算值返回,而且將count做爲依賴值傳遞進去。這樣,就只會在count改變的時候觸發expensive執行,在修改val的時候,返回上一次緩存的值。

如何使用

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
 // computeExpensiveValue得有返回值
複製代碼

知識點合集

useMemo與React.memo完美搭配

const Couter = React.memo(function Couter(props{
  console.log('Couter render')
  return (
    <> 
       <span>{count}</span>
       <div onClick={props.clickHandle}>{props.count}</div>
    </>

  )
})

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

  const db = useMemo(() => {
    return count * 2;

  }, [count])

  return (
    <div>
      <Couter count={count}></Couter>
      <div>{db}</div>
      <button onClick={() => { setCount(count + 1) }}>點擊</button>
    </div>
  )
}
複製代碼

用對性能優化

有同窗會想到,居然useCallbackuseMemo這麼好用,我可不可像👇下面例子🌰中的寫法同樣,對我之後的項目這樣優化

const App = () => {
  const [count, setCount] = useState(0);
  /**
   * 對全部的方法我都採用useCallback作緩存優化
   * const handleBtn=()=>{
   *  setCount(count + 1)
   * }
   */

  const handleBtn = useCallback(() => {
    setCount(count + 1)
  }, [count])


  /**
   * 對全部的局部變量我都採用useMemo作緩存優化
   * const db=count*2
   */


  const db = useMemo(() => {
    return count * 2;
  }, [count])

  return (
    <div>
      <Couter count={count}></Couter>
      <div>{db}</div>
      <button onClick={() => { handleBtn }}>點擊</button>
    </div>

  )
}
複製代碼

❗️筆者一開始也有這樣想到,若是你真按照這種形式去開發咱們的項目,那麼恭喜你,你會死的很慘

爲何useCallback和useMemo更加糟糕

性能優化不是免費的,它們老是帶來成本,但這並不老是帶來好處來抵消成本,因此咱們採用useCallback和useMemo作性能優化,應該是作到花費的成本大於收入的成本

首先,咱們須要知道useCallback,useMemo自己也有開銷。useCallback,useMemo 會「記住」一些值,同時在後續 render 時,將依賴數組中的值取出來和上一次記錄的值進行比較,若是不相等纔會從新執行回調函數,不然直接返回「記住」的值。這個過程自己就會消耗必定的內存和計算資源。所以,過分使用 useCallback,useMemo 可能會影響程序的性能,而且也加大了維護成本,畢竟代碼更加複雜化了

何時使用 useMemo 和 useCallback?

使用useMemo 和 useCallback出於這兩個目的

  • 保持引用相等

  • 對於組件內部用到的 object、array、函數等,若是用在了其餘 Hook 的依賴數組中,或者做爲 props 傳遞給了下游組件,應該使用 useMemo 和 useCallback

  • 自定義 Hook 中暴露出來的 object、array、函數等,都應該使用useMemo 和 useCallback,以確保當值相同時,引用不發生變化(你能夠理解成是第一種說法的衍生,即自定義hooks比做組件,由於一個函數組件state一變化就會從新執行函數)

  • 昂貴的計算

  • 好比👆例子🌰的expensive函數

無需使用useMemo 和 useCallback 的場景

  • 若是返回的值是原始值: string, boolean, null, undefined, number, symbol(不包括動態聲明的 Symbol),通常不須要使用useMemo 和 useCallback

  • 僅在組件內部用到的 object、array、函數等(沒有做爲 props 傳遞給子組件)且沒有用到其餘 Hook 的依賴數組中,通常不須要使用useMemo 和 useCallback

實際場景

場景:有一個父組件,其中包含子組件,子組件接收一個函數做爲props;一般而言,若是父組件更新了,子組件也會執行更新;可是大多數場景下,更新是沒有必要的,咱們能夠藉助useCallback來返回函數,而後把這個函數做爲props傳遞給子組件;這樣,子組件就能避免沒必要要的更新。

// 父組件
const Parent=()=>{
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');

    const callback = useCallback(() => {
        return count;
    }, [count]);
    return <div>
        <h4>{count}</h4>
        <Child callback={callback}/>
        <div>
            <button onClick={() => setCount(count + 1)}>+</button>
            <input value={val} onChange={event => setVal(event.target.value)}/>
        </div>
    </div>;
}

// 子組件
const Child=({ callback })=>{
    const [count, setCount] = useState(() => callback());
    useEffect(() => {
        setCount(callback());
    }, [callback]);
    return <div>
        {count}
    </div>
}
export default React.memo(Child) // 用React.memo包裹
複製代碼

這個場景是複用上述例子🌰的場景,這就是要保持引用不變的場景,顯然此次收益的成本大於優化付出的成本,子組件能夠避免沒必要要的渲染

最後

相關文章
相關標籤/搜索