react hooks 學(xué)習(xí)記錄

Hooks Api 索引

基礎(chǔ) Api

useState useEffect useContext
返回一個(gè) state,以及更新 state 的函數(shù)。 相當(dāng)于componentDidMount、componentDidUpdate,componentWillUnmount 這三個(gè)函數(shù)的組合。 接收一個(gè) context 對(duì)象(React.createContext 的返回值)并返回該 context 的當(dāng)前值。當(dāng)前的 context 值由上層組件中距離當(dāng)前組件最近的 <MyContext.Provider> 的 value prop 決定。

額外Api

useReducer useCallback useMemo useRef useImperativeHandle useLayoutEffect useDebugValue

useState 官方文檔例子

import React, { useState } from 'react';

function Example() {
  // 聲明一個(gè)叫 "count" 的 state 變量
  const [count, setCount] = useState(0);

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

等價(jià)的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>
    );
  }
}

聲明多個(gè)state

function ExampleWithManyStates() {
  // 聲明多個(gè) state 變量
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: '學(xué)習(xí) Hook' }]);

提示:方括號(hào)有什么用?

我們用方括號(hào)定義了一個(gè) state 變量
等號(hào)左邊名字并不是 React API 的部分,你可以自己取名字
這種 JavaScript 語(yǔ)法叫數(shù)組解構(gòu)。它意味著我們同時(shí)創(chuàng)建了 fruitsetFruit 兩個(gè)變量,fruit 的值為 useState 返回的第一個(gè)值,setFruit 是返回的第二個(gè)值

const [fruit, setFruit] = useState('banana');
var fruitStateVariable = useState('banana'); // 返回一個(gè)有兩個(gè)元素的數(shù)組
var fruit = fruitStateVariable[0]; // 數(shù)組里的第一個(gè)值
var setFruit = fruitStateVariable[1]; // 數(shù)組里的第二個(gè)值

useState除了可以靜態(tài)的賦值還可以u(píng)seState(()=>init)
useState返回的更新數(shù)據(jù)方法setXXX(currentState=>currentState+1)默認(rèn)會(huì)傳入當(dāng)前的數(shù)據(jù)狀態(tài)

useEffect

Effect Hook 可以讓你在函數(shù)組件中執(zhí)行副作用操作
解釋這個(gè) Hook 之前先理解下什么是副作用。網(wǎng)絡(luò)請(qǐng)求、訂閱某個(gè)模塊或者 DOM 操作都是副作用的例子,Effect Hook 是專(zhuān)門(mén)用來(lái)處理副作用的。正常情況下,在Function Component的函數(shù)體中,是不建議寫(xiě)副作用代碼的,否則容易出 bug。

下面的Class Component例子中,副作用代碼寫(xiě)在了componentDidMount和componentDidUpdate中:

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

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

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

可以看到componentDidMount和componentDidUpdate中的代碼是一樣的。而使用 Effect Hook 來(lái)改寫(xiě)就不會(huì)有這個(gè)問(wèn)題:

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

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>
  );
}
//useEffect 沒(méi)有傳遞依賴參數(shù),所以每次數(shù)據(jù)更新都會(huì)調(diào)用useEffect

useEffect會(huì)在每次 DOM 渲染后執(zhí)行,不會(huì)阻塞頁(yè)面渲染。它同時(shí)具備componentDidMount、componentDidUpdatecomponentWillUnmount三個(gè)生命周期函數(shù)的執(zhí)行時(shí)機(jī)

此外還有一些副作用需要組件卸載的時(shí)候做一些額外的清理工作的,例如訂閱某個(gè)功能:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

componentDidMount訂閱后,需要在componentWillUnmount取消訂閱。使用 Effect Hook 來(lái)改寫(xiě)會(huì)是這個(gè)樣子:

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    
    // 返回一個(gè)函數(shù)來(lái)進(jìn)行額外的清理工作:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

當(dāng)useEffect的返回值是一個(gè)函數(shù)的時(shí)候,React 會(huì)在下一次執(zhí)行這個(gè)副作用之前執(zhí)行一遍清理工作,整個(gè)組件的生命周期流程可以這么理解:

組件掛載 --> 執(zhí)行副作用 --> 組件更新 --> 執(zhí)行清理函數(shù) --> 執(zhí)行副作用 --> 組件更新 --> 執(zhí)行清理函數(shù) --> 組件卸載

上文提到useEffect會(huì)在每次渲染后執(zhí)行,但有的情況下我們希望只有在 stateprops 改變的情況下才執(zhí)行。如果是Class Component,我們會(huì)這么做:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

使用 Hook 的時(shí)候,我們只需要傳入第二個(gè)參數(shù):

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 只有在 count 改變的時(shí)候才執(zhí)行 Effect

第二個(gè)參數(shù)是一個(gè)數(shù)組,可以傳多個(gè)值,一般會(huì)將 Effect 用到的所有 props 和 state 都傳進(jìn)去。

當(dāng)副作用只需要在組件掛載的時(shí)候和卸載的時(shí)候執(zhí)行,第二個(gè)參數(shù)可以傳一個(gè)空數(shù)組[],實(shí)現(xiàn)的效果有點(diǎn)類(lèi)似componentDidMountcomponentWillUnmount的組合。

useCallback useMemo

在介紹一下這兩個(gè)hooks的作用之前,我們先來(lái)回顧一下react中的性能優(yōu)化。在hooks誕生之前,如果組件包含內(nèi)部state,我們都是基于class的形式來(lái)創(chuàng)建組件。當(dāng)時(shí)我們也知道,react中,性能的優(yōu)化點(diǎn)在于:

1.調(diào)用setState,就會(huì)觸發(fā)組件的重新渲染,無(wú)論前后的state是否不同
2.父組件更新,子組件也會(huì)自動(dòng)的更新

基于上面的兩點(diǎn),我們通常的解決方案是:使用immutable進(jìn)行比較,在不相等的時(shí)候調(diào)用setState;在shouldComponentUpdate中判斷前后的props和state,如果沒(méi)有變化,則返回false來(lái)阻止更新。

在hooks出來(lái)之后,我們能夠使用function的形式來(lái)創(chuàng)建包含內(nèi)部state的組件。但是,使用function的形式,失去了上面的shouldComponentUpdate,我們無(wú)法通過(guò)判斷前后狀態(tài)來(lái)決定是否更新。而且,在函數(shù)組件中,react不再區(qū)分mount和update兩個(gè)狀態(tài),這意味著函數(shù)組件的每一次調(diào)用都會(huì)執(zhí)行其內(nèi)部的所有邏輯,那么會(huì)帶來(lái)較大的性能損耗。因此useMemo 和useCallback就是解決性能問(wèn)題的殺手锏

useCallbackuseMemo的參數(shù)跟useEffect一致,他們之間最大的區(qū)別有是useEffect會(huì)用于處理副作用,而前兩個(gè)hooks不能。

useMemouseCallback都會(huì)在組件第一次渲染的時(shí)候執(zhí)行,之后會(huì)在其依賴的變量發(fā)生改變時(shí)再次執(zhí)行;并且這兩個(gè)hooks都返回緩存的值,useMemo返回緩存的變量,useCallback返回緩存的函數(shù)

useMemo

我們來(lái)看一個(gè)反例:

import React from 'react';
 
 
export default function WithoutMemo() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
 
    function expensive() {
        console.log('compute');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
            sum += i;
        }
        return sum;
    }
 
    return <div>
        <h4>{count}-{val}-{expensive()}</h4>
        <div>
            <button onClick={() => setCount(count + 1)}>+c1</button>
            <input value={val} onChange={event => setValue(event.target.value)}/>
        </div>
    </div>;
}

這里創(chuàng)建了兩個(gè)state,執(zhí)行一次昂貴的計(jì)算,拿到count對(duì)應(yīng)的某個(gè)值。我們可以看到:無(wú)論是修改count還是val,由于組件的重新渲染,都會(huì)觸發(fā)expensive的執(zhí)行(能夠在控制臺(tái)看到,即使修改val,也會(huì)打印);但是這里的昂貴計(jì)算只依賴于count的值,在val修改的時(shí)候,是沒(méi)有必要再次計(jì)算的。在這種情況下,我們就可以使用useMemo,只在count的值修改時(shí),執(zhí)行expensive計(jì)算:

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來(lái)執(zhí)行昂貴的計(jì)算,然后將計(jì)算值返回,并且將count作為依賴值傳遞進(jìn)去。這樣,就只會(huì)在count改變的時(shí)候觸發(fā)expensive執(zhí)行,在修改val的時(shí)候,返回上一次緩存的值。

useCallback

講完了useMemo,接下來(lái)是useCallback。useCallback跟useMemo比較類(lèi)似,但它返回的是緩存的函數(shù)。我們看一下最簡(jiǎn)單的用法

const fnA = useCallback(fnB, [a])

上面的useCallback會(huì)將我們傳遞給它的函數(shù)fnB返回,并且將這個(gè)結(jié)果緩存;當(dāng)依賴a變更時(shí),會(huì)返回新的函數(shù)。既然返回的是函數(shù),我們無(wú)法很好的判斷返回的函數(shù)是否變更,所以我們可以借助ES6新增的數(shù)據(jù)類(lèi)型Set來(lái)判斷,具體如下:

import React, { useState, useCallback } from 'react';
 
const set = new Set();
 
export default function Callback() {
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');
 
    const callback = useCallback(() => {
        console.log(count);
    }, [count]);
    set.add(callback);
 
 
    return <div>
        <h4>{count}</h4>
        <h4>{set.size}</h4>
        <div>
            <button onClick={() => setCount(count + 1)}>+</button>
            <input value={val} onChange={event => setVal(event.target.value)}/>
        </div>
    </div>;
}

我們可以看到,每次修改count,set.size就會(huì)+1,這說(shuō)明useCallback依賴變量count,count變更時(shí)會(huì)返回新的函數(shù);而val變更時(shí),set.size不會(huì)變,說(shuō)明返回的是緩存的舊版本函數(shù)。

知道useCallback有什么樣的特點(diǎn),那有什么作用呢?

使用場(chǎng)景是:有一個(gè)父組件,其中包含子組件,子組件接收一個(gè)函數(shù)作為props;通常而言,如果父組件更新了,子組件也會(huì)執(zhí)行更新;但是大多數(shù)場(chǎng)景下,更新是沒(méi)有必要的,我們可以借助useCallback來(lái)返回函數(shù),然后把這個(gè)函數(shù)作為props傳遞給子組件;這樣,子組件就能避免不必要的更新。

import React, { useState, useCallback, useEffect } from 'react';
function 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>;
}
 
function Child({ callback }) {
    const [count, setCount] = useState(() => callback());
    useEffect(() => {
        setCount(callback());
    }, [callback]);
    return <div>
        {count}
    </div>
}

useEffect、useMemo、useCallback都是自帶閉包的。也就是說(shuō),每一次組件的渲染,其都會(huì)捕獲當(dāng)前組件函數(shù)上下文中的狀態(tài)(state, props),所以每一次這三種hooks的執(zhí)行,反映的也都是當(dāng)前的狀態(tài),你無(wú)法使用它們來(lái)捕獲上一次的狀態(tài)。對(duì)于這種情況,我們應(yīng)該使用ref來(lái)訪問(wèn)。

有空繼續(xù)整理

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容