react-hooks詳解

1 關(guān)于hook

1.1 為什么使用hook

在react類組件(class)寫法中,有setState和生命周期對(duì)狀態(tài)進(jìn)行管理,但是在函數(shù)組件中不存在這些,故引入hooks(版本:>=16.8),使開發(fā)者在非class的情況下使用更多react特性。

以下是實(shí)現(xiàn)一個(gè)輸入框,類組件和函數(shù)組件兩種寫法的對(duì)比:

/**
 * @name 類組件
 */
import React from 'react';
export default class Home extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'world'
    };
  }
  componentDidMount() {
    console.log('組件掛載后要做的操作')
  }
  componentWillUnmount() {
    console.log('組件卸載要做的操作')
  }
  componentDidUpdate(prevProps, prevState) {
    if (prevState.name !== this.state.name) {
      console.log('組件更新后的操作')
    }
  }
  render() {
    return (
      <div>
        <p>hello {this.state.name}</p>
        <input type="text" placeholder="input new name"
          onChange={(e) => this.setState({ name: e.target.value })}>
        </input>
      </div>
    );
  }
}


/**
 * @name 函數(shù)組件
 */
import React, { useState, useEffect } from 'react';

export default function Home() {
  const [name, setName] = useState('world');
  return (
    <div>
      <p>hello {name}</p>
      <DemoState />
    </div>
  )
}

function DemoState() {
  const [n1, setN1] = useState(1)
  const [n2, setN2] = useState(2)
  const [n3, setN3] = useState(3)

  useEffect(() => {
    setN1(10)
    setN1(100)
  }, [])
  const handleClick = () => {
    setN2(20)
    setN3(30)
  }
  console.log('demo-state', n1, n2, n3)
  return <button onClick={handleClick}>click</button>
}

上述例子中,useState相當(dāng)于constructor,完成數(shù)據(jù)的初始化;

useEffect相當(dāng)于componentDidMount和componentDidUpdate兩個(gè)生命周期,通過return () => {}的方式解綁生命周期,相當(dāng)于componentWillUnmount周期,以監(jiān)聽頁面滾動(dòng)為例,通過effect實(shí)現(xiàn)監(jiān)聽與解綁如下:

useEffect(() = >{
window.addEventListener(‘scroll’, throttleFunc)
return () = >{
window.removeEventListener(‘scroll’, throttleFunc)
}
}, [])

在同一個(gè)effect鉤子中實(shí)現(xiàn)綁定與解綁,使?fàn)顟B(tài)的管理更加方便、代碼更簡潔。

此外還有發(fā)生在頁面渲染前的useMemo相當(dāng)于shouldComponentUpdate周期等,具體關(guān)系如下表:

class組件 hooks
shouldComponentUpdate useMemo
render 函數(shù)本身
getDerivedStateFromProps useState 中的 update
getDerivedStateFromError
constructor useState
componentWillUnmount useEffect中的return函數(shù)
componentDidUpdate useEffect
componentDidMount useEffect
componentDidCatch

結(jié)論:使用hooks的函數(shù)組件,簡化了很多代碼,不用維護(hù)復(fù)雜的生命周期,也不用擔(dān)心this的指向問題。

1.2 什么是hook

hooks掛載在Fiber結(jié)點(diǎn)上的memoizedState,filber結(jié)構(gòu)如下:

FiberNode { // fiber結(jié)構(gòu)
  memoziedState, // 組件更新的依據(jù)
  type, // 原生或react   
  key,
  tag,
  ...
}

memoziedState這個(gè)字段很重要,是組件更新的唯一依據(jù)。在class組件里,它就是this.state的結(jié)構(gòu),調(diào)用this.setState的時(shí)候,其實(shí)就是修改了它的數(shù)據(jù),數(shù)據(jù)改變了組件就會(huì)重新執(zhí)行。

也就是說,即使是class組件,也不會(huì)主動(dòng)調(diào)用任何生命周期函數(shù),而是在memoziedState改變后,組件重新執(zhí)行,在執(zhí)行的過程中才會(huì)經(jīng)過這些周期。

所以,這就解釋了函數(shù)式組件為什么可以通過hooks改變狀態(tài),實(shí)際上就是修改了對(duì)應(yīng)fiber節(jié)點(diǎn)的memoziedState。

hooks主要有以下特點(diǎn):

1、無需修改組件結(jié)構(gòu)的情況下復(fù)用狀態(tài)邏輯;

2、可將組件中相互關(guān)聯(lián)的部分拆分成更小的函數(shù),復(fù)雜組件將變得更容易理解;

3、每一個(gè)組件內(nèi)的函數(shù)(包括事件處理函數(shù),effects,定時(shí)器或者api調(diào)用等等)會(huì)捕獲某次渲染中定義的props和state;

4、memo緩存組件 ,useMemo緩存值, useCallback緩存函數(shù)

5、每次render都有自己的props、state和effects。(每一個(gè)組件內(nèi)的函數(shù),包括事件處理函數(shù),effects,定時(shí)器或者api調(diào)用等等,會(huì)捕獲某次渲染中定義的props和state);

6、更新狀態(tài)的時(shí)候(如setCount(count + 1)),React會(huì)重新渲染組件,每一次渲染都能拿到獨(dú)立的count狀態(tài),這個(gè)狀態(tài)值是函數(shù)中的一個(gè)常量;

7、沒有了顯性的生命周期,所有渲染后的執(zhí)行方法都在useEffect里面統(tǒng)一管理

8、函數(shù)式編程,不需要定義constructor、render、class;

9、某一個(gè)組件,方法需不需要渲染、重新執(zhí)行完全取決于開發(fā)者,方便管理。

1.3 常見hook

useState、useEffect、useMemo、useCallback、useRef、useContext、useReducer…。

所有的鉤子都是為函數(shù)引入外部功能,react約定,鉤子一律使用use前綴命名。

2 常用hook

2.1 useState

示例:

const [stateA, setStateA] = useState(0)

參數(shù)是初始state(定義初始state最好給出初始值,方便后期維護(hù), 0/false/’’/[]/{})。

返回值:一個(gè)是當(dāng)前state,一個(gè)是更新state的函數(shù)。

useState的實(shí)現(xiàn)很簡單,只有兩行

export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}


重點(diǎn)都在dispatcher上,dispatcher通過resolveDispatcher()來獲取,這個(gè)函數(shù)只是將ReactCurrentDispatcher.current的值賦給了dispatcher

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  return dispatcher;
}

useState掛在dispatcher上,resolveDispatcher() 返回的是 ReactCurrentDispatcher.current,所以u(píng)seState(xxx)等價(jià)于ReactCurrentDispatcher.current.useState(xxx)。

useState(hooks)的具體執(zhí)行過程如下:

1.png
  • updateContainer → … → beginWork
  • beginWork中會(huì)根據(jù)當(dāng)前要執(zhí)行更新的fiber的tag來判斷執(zhí)行什么,在函數(shù)式組件,執(zhí)行了updateFunctionComponent(判斷執(zhí)行函數(shù)式/組件式更新)
    首次渲染時(shí),React Fiber 會(huì)從 packages/react-reconciler/src/ReactFiberBeginWork.js 中的 beginWork() 開始執(zhí)行。在beginWork函數(shù)中,可以根據(jù)workInProgress(是一個(gè)Fiber節(jié)點(diǎn))上的tag值來走不通的方法加載或更新組件,如下:
function beginWork(current: Fiber | null, workInProgress: Fiber, renderExpirationTime: ExpirationTime, ) : Fiber | null {
    /****/
    // 根據(jù)不同的組件類型走不同的方法
    switch (workInProgress.tag) {
        // 不確定組件    
    case IndeterminateComponent:
        {
            const elementType = workInProgress.elementType;
            // 加載初始組件       
            return mountIndeterminateComponent(current, workInProgress, elementType, renderExpirationTime, );
        }
        // 函數(shù)組件     
    case FunctionComponent:
        {
            const Component = workInProgress.type;
            const unresolvedProps = workInProgress.pendingProps;
            const resolvedProps = workInProgress.elementType === Component ? unresolvedProps: resolveDefaultProps(Component, unresolvedProps);
            // 更新函數(shù)組件       
            return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderExpirationTime, );
        }
        // 類組件     
    case ClassComponent:
        {
            /****/
        }
    }
}

  • 在updateFunctionComponent中,對(duì)hooks的處理如下
nextChildren = renderWithHooks(
  current,
  workInProgress,
  Component,
  nextProps,
  context,
  renderExpirationTime,
);

所以,React Hooks 的渲染核心是renderWithHooks,在renderWithHooks函數(shù)中,初始化了Dispatcher。

export
function renderWithHooks < Props, SecondArg > (current: Fiber | null, workInProgress: Fiber, 
             Component: (p: Props, arg: SecondArg) = >any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ) : any {

    // 若Fiber為空,則認(rèn)為是首次加載
    ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;

    // 掛載時(shí)的Dispatcher
    const HooksDispatcherOnMount: Dispatcher = {
        readContext,
        // ...
        useCallback: mountCallback,
        useContext: readContext,
        useEffect: mountEffect,
        useMemo: mountMemo,

        useState: mountState,
        // ...
    };

    // 更新時(shí)的Dispatcher
    const HooksDispatcherOnUpdate: Dispatcher = {
        readContext,
        // ...
        useCallback: updateCallback,
        useContext: readContext,
        useEffect: updateEffect,
        useMemo: updateMemo,
        useRef: updateRef,
        useState: updateState,
        // ....
    };
}

  • 在renderWithHooks中,會(huì)先根據(jù)fiber的memoizedState是否為null,來判斷是否已經(jīng)初始化。因?yàn)閙emoizedState在函數(shù)式組件中是存放hooks的。是則mount,否則update(判斷是否執(zhí)行過,沒有則掛載,有則更新)
  • 在mount(掛載)時(shí),函數(shù)式組件執(zhí)行,ReactCurrentDispatcher.current為HooksDispatcherOnMount,被調(diào)用,會(huì)初始化hooks鏈表、initialState、dispatch函數(shù),并返回。這里就完成了useState的初始化,后續(xù)函數(shù)式組件繼續(xù)執(zhí)行,完成渲染返回。(首次渲染過程)
  • 在update(更新)時(shí),函數(shù)式組件執(zhí)行,ReactCurrentDispatcher.current為HooksDispatcherOnUpdate,被調(diào)用,updateWorkInProgressHook用于獲取當(dāng)前work的Hook。然后根據(jù)numberOfReRenders 是否大于0來判斷是否處理re-render狀態(tài):是的話,執(zhí)行renderPhaseUpdates,獲取第一個(gè)update,然后循環(huán)執(zhí)行,獲取新的state,直到下一個(gè)update為null;否的話,獲取update鏈表的第一個(gè)update,進(jìn)行循環(huán),判斷update的優(yōu)先級(jí)是否需要更新,對(duì)于優(yōu)先級(jí)高的進(jìn)行更新。(更新過程)
  • 結(jié)果返回當(dāng)前狀態(tài)和修改狀態(tài)的方法
    以掛載為例,生成一個(gè)hook對(duì)象(mountState),并對(duì)hook對(duì)象進(jìn)行初始化(mountWorkInProgressHook),具體如下:
function mountState < S > (initialState: (() = >S) | S, ) : [S, Dispatch < BasicStateAction < S >> ] {
    // 創(chuàng)建一個(gè)新的hook對(duì)象,并返回當(dāng)前workInProgressHook
    const hook = mountWorkInProgressHook();
    if (typeof initialState === 'function') {
        initialState = initialState();
    }
    hook.memoizedState = hook.baseState = initialState; // 第二步:獲取初始值并初始化hook對(duì)象
    const queue = hook.queue = { // 新建一個(gè)隊(duì)列  
        // 保存 update 對(duì)象
        pending: null,
        // 保存dispatchAction.bind()的值
        dispatch: null,
        // 一次新的dispatch觸發(fā)前最新的reducer
        // useState 保存固定函數(shù): 可以理解為一個(gè)react 內(nèi)置的reducer
        // (state, action) => { return typeof action === 'function' ? action(state) : action }
        lastRenderedReducer: reducer
        // 一次新的dispatch觸發(fā)前最新的state
        lastRenderedState: (initialState: any),
    }
    // 綁定當(dāng)前 fiber 和 queue.
    const dispatch: Dispatch < BasicStateAction < S > ,
    >=(queue.dispatch = (dispatchAction.bind(null, currentlyRenderingFiber, queue, ) : any));
    // 返回當(dāng)前狀態(tài)和修改狀態(tài)的方法
    return [hook.memoizedState, dispatch];
}

function mountWorkInProgressHook() {

    // 初始化的hook對(duì)象
    var hook = {
        memoizedState: null,
        // 存儲(chǔ)更新后的state值
        baseState: null,
        // 存儲(chǔ)更新前的state
        baseQueue, // 更新函數(shù)
        queue: null,
        // 存儲(chǔ)多次的更新行為
        next: null // 指向下一次useState的hook對(duì)象
    };

    // workInProgressHook是一個(gè)全局變量,表示當(dāng)前正在處理的hook
    // 如果workInProgressHook鏈表為null就將新建的hook對(duì)象賦值給它,如果不為null,那么就加在鏈表尾部。
    if (workInProgressHook === null) {
        currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
    } else {
        workInProgressHook = workInProgressHook.next = hook;
    }

    return workInProgressHook;
}

初始化完成后,setFn又是怎么對(duì)stateA值進(jìn)行更新的呢?實(shí)際上就是通過dispatchAction方法進(jìn)行更新的,如下:

// currentlyRenderingFiber$1是一個(gè)全局變量,表示當(dāng)前正在渲染的FiberNode
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);

此外還有對(duì)hook對(duì)象的更新(dispatchAction),如下:

function dispatchAction(fiber, queue, action) {
    // ...
    // 1. 創(chuàng)建update對(duì)象
    // 該對(duì)象保存的是調(diào)度優(yōu)先級(jí)/state/reducer以及用戶調(diào)用dispatch/setState 時(shí)傳入的action
    const update: Update < S,
    A > ={
        lane,
        action,
        eagerReducer: null,
        eagerState: null,
        next: (null: any),
    };
    // 2. 將update更新到queue.pending中,最后的update.next 指向第一個(gè)update對(duì)象,形成一個(gè)閉環(huán)。
    const pending = queue.pending;
    if (pending === null) {
        // This is the first update. Create a circular list.
        update.next = update;
    } else {
        update.next = pending.next;
        pending.next = update;
    }
    queue.pending = update;
}

簡單理解:

  • 初次渲染的時(shí)候,按照 useState,useEffect 的順序,把 state,deps 等按順序塞到 memoizedState 數(shù)組中,共享同一個(gè) memoizedState,共享同一個(gè)順序。
  • 更新的時(shí)候,按照順序,從 memoizedState 中把上次記錄的值拿出來。
let memoizedState = []; // hooks 存放在這個(gè)數(shù)組
let cursor = 0; // 當(dāng)前 memoizedState 下標(biāo)

function useState(initialValue) {
  memoizedState[cursor] = memoizedState[cursor] || initialValue;
  const currentCursor = cursor;
  function setState(newState) {
    memoizedState[currentCursor] = newState;
    render();
  }
  return [memoizedState[cursor++], setState]; // 返回當(dāng)前 state,并把 cursor 加 1
}

function useEffect(callback, depArray) {
  const hasNoDeps = !depArray;
  const deps = memoizedState[cursor];
  const hasChangedDeps = deps
    ? !depArray.every((el, i) => el === deps[i])
    : true;
  if (hasNoDeps || hasChangedDeps) {
    callback();
    memoizedState[cursor] = depArray;
  }
  cursor++;
}

  • 具體 看下圖
    1、初始化時(shí):數(shù)組為空,下標(biāo)置0
1.png

2、首次渲染:將遇到的hook的依賴項(xiàng)加入數(shù)組,與下標(biāo)一一對(duì)應(yīng)

2.png

3、事件觸發(fā):觸發(fā)hook的內(nèi)容被修改,修改后的數(shù)據(jù)替換掉數(shù)組中原先的數(shù)據(jù)

3.png

4、重渲染:ReRender 的時(shí)候,重新去執(zhí)行函數(shù)組件,但是對(duì)之前已經(jīng)執(zhí)行過的函數(shù)組件并不會(huì)做任何操作

4.png

下面看一個(gè)實(shí)例:

const DemoState = memo(() => {
  const [n1, setN1] = useState(1)
  const [n2, setN2] = useState(2)
  const [n3, setN3] = useState(3)

  useEffect(() => {
    setN1(10)
    setN1(100)
  }, [])
  const handleClick = () => {
    setN2(20)
    setN3(30)
  }
  console.log('demo-state', n1, n2, n3)
  return <div>
    <div className={`${classPrefix}-title`}>---useState---</div>
    <button onClick={handleClick}>改變n2、n3</button>
  </div>
})
// demo-state 1 2 3 => demo-state 100 2 3 => demo-state 100 20 30

渲染時(shí),effect對(duì)n1進(jìn)行了兩次賦值,實(shí)際上僅刷新一次;點(diǎn)擊事件分別對(duì)n1和n2進(jìn)行賦值,實(shí)際上也僅刷新一次。

結(jié)論:setState返回的函數(shù)執(zhí)行會(huì)導(dǎo)致re-render;
框架內(nèi)部會(huì)對(duì)多次函數(shù)操作進(jìn)行合并,保證useState拿到最新的狀態(tài),避免重復(fù)渲染。

如果初始state需要通過復(fù)雜計(jì)算獲得,可以傳入一個(gè)函數(shù),在函數(shù)中計(jì)算并返回初始state,此函數(shù)只在初始渲染時(shí)被調(diào)用,具體如下:

const [count, setCount] = useState(() => {
  const initialCount = someExpensiveComputation(props)
  return initialState
})

那么,state在class和function中有什么區(qū)別嗎?下面兩段代碼中,1s內(nèi)點(diǎn)擊事件各觸發(fā)5次會(huì)有什么表現(xiàn)嗎?

class HooksDemoRule extends React.PureComponent {
  state = {
    count: 0
  }
  increment = () => {
    console.log('---setState---')
    setTimeout(() => {
      this.setState({
        count: this.state.count + 1
      })
    }, 3000)
  }
  render() {
    return <div>
      <div className={`${classPrefix}-title`}>---useState && setState---</div>
      <div className={`${classPrefix}-text`}>setState 當(dāng)前值:{this.state.count}</div>
      <button onClick={this.increment}>+1</button>
      <IKWebviewRouterLink
        ikTo='home'
        className={`${classPrefix}-to-home`}
      ><p>返回</p></IKWebviewRouterLink>
    </div>
  }
}
const SecondDemo = memo(() => {
  const [count, setCount] = useState(0)
  const increment = () => {
    console.log('---useState---')
    setTimeout(() => {
      setCount(count + 1)
    }, 3000)
  }
  return <div>
    <div className={`${classPrefix}-title`}>---useState && setState---</div>
    <div className={`${classPrefix}-text`}>useState 當(dāng)前值:{count}</div>
    <button onClick={increment}>+1</button>
    <IKWebviewRouterLink
      ikTo='rule'
      className={`${classPrefix}-to-home`}
    ><p>返回</p></IKWebviewRouterLink>
  </div>
})

最終結(jié)果:在類組件中,頁面上的數(shù)字依次從0增加到5;在函數(shù)組件中,頁面上的數(shù)字只會(huì)從0增加到1。

原因在于,在類組件中,通過this.state引用count,每一次setTimeout的時(shí)候都能通過引用拿到上一次的最新count,所以最后加到5。但是在函數(shù)組件中,每一次更新都是重新執(zhí)行當(dāng)前函數(shù),1s內(nèi)setTimeout里讀取的count實(shí)際上都是初始值0,所以最后只加到1。如果想讓函數(shù)組件也加到5要怎么實(shí)現(xiàn)呢,下文useRef會(huì)講到。

簡單來說,類組件的state依賴上一次state,函數(shù)組件的state是重新執(zhí)行當(dāng)前函數(shù)。

2.2 useEffect

useEffect的實(shí)現(xiàn)很簡單,也是只有兩行:

export function useEffect (create: () = >(() = >void) | void, deps: Array < mixed > |void | null, ) : void {
  const dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, deps);
}

useEffect產(chǎn)生的hook會(huì)放在fiber.memoizedState上,調(diào)用后生成一個(gè)effect對(duì)象,存儲(chǔ)到對(duì)應(yīng)hook的memoizedState中,與其他effect連接成環(huán)形鏈表。
單個(gè)的effect對(duì)象包含以下幾個(gè)屬性:
create:傳入useEffect函數(shù)的第一個(gè)參數(shù),即回調(diào)函數(shù);
destory:回調(diào)函數(shù)中的return函數(shù),在改effect銷毀的時(shí)候執(zhí)行,默認(rèn)發(fā)生在第一次渲染后,也可以讓它在依賴項(xiàng)數(shù)組中的值改變時(shí)執(zhí)行,通過return清除副作用函數(shù)(如監(jiān)聽、訂閱、計(jì)時(shí)器等);
deps:依賴項(xiàng),傳入的第二個(gè)參數(shù),用來控制該Effect包裹的函數(shù)執(zhí)不執(zhí)行。如果依賴項(xiàng)為空數(shù)組[],則該Effect在每次組件掛載時(shí)執(zhí)行,且僅執(zhí)行一次,相當(dāng)于class組件中的componentDidMount和componentDidupdate生命周期的融合;如果沒有第二個(gè)參數(shù),則effect會(huì)不停地調(diào)用。
next:指向下一個(gè)effect;
tag:effect的類型,區(qū)分useEffect和useLayoputEffect。
hook會(huì)掛載到fiber.memoizedState上。hook按出現(xiàn)順序進(jìn)行存儲(chǔ),memoizedState存儲(chǔ)了useEffect的effect對(duì)象(effect1),next指向useLayoutEffect的effect對(duì)象(effect2),effect2的next又會(huì)指向effect1,最終形成閉環(huán)。結(jié)構(gòu)如下:

1.png
const DemoEffect = memo(() => {
  useEffect(() => {
    console.log('useEffect1');
    const timeId = setTimeout(() => {
      console.log('useEffect1-setTimeout-2000');
    }, 2000);
    return () => {
      clearTimeout(timeId);
    };
  }, []);
  useEffect(() => {
    console.log('useEffect2');
    const timeId = setInterval(() => {
      console.log('useEffect2-setInterval-1000');
    }, 1000);
    return () => {
      clearInterval(timeId);
    };
  }, []);
  return (
    <div>
      <div className={`${classPrefix}-title`}>---useEffect---</div>
      {(() => {
        console.log('render');
        return null;
      })()}
    </div>
  );
})
// render => useEffect1 => useEffect2 => useEffect2-setInterval-1000 => useEffect1-setTimeout-2000 => index.js:67 useEffect2-setInterval-1000 * n

結(jié)論:effect在頁面完成渲染后按照先后順序執(zhí)行,并且內(nèi)部執(zhí)行時(shí)異步的

useEffect和useLayoutEffect:
useLayoutEffect也是一個(gè)hook方法,跟useEffect類似,區(qū)別在于渲染時(shí)機(jī)不同,useEffect發(fā)生在瀏覽器渲染結(jié)束后執(zhí)行,useLayoutEffect則是發(fā)生在dom更新完成后。

PS:useEffect和useLayoutEffect都是effect鉤子
下面是一個(gè)方塊移動(dòng)的例子,在effect中添加右移的方法,理解兩者的區(qū)別:兩者都發(fā)生在render之后,且useLayoutEffect發(fā)生在useEffect之前

const moveTo = (dom, delay, options) => {
  dom.style.transform = `translate(${options.x}px)`
  dom.style.transition = `left ${delay}ms`
}
const Animate = memo(() => {
  const squRef = useRef()
  const squRef1 = useRef()
  const squRef2 = useRef()

  useLayoutEffect(() => { // 方塊直接出現(xiàn)在右側(cè),不會(huì)閃一下
    console.log('useLayoutEffect-1')
    moveTo(squRef1.current, 500, { x: 600 })
  }, [])
  useEffect(() => { // 會(huì)有方塊移動(dòng)的過程,閃一下
    console.log('useEffect')
    moveTo(squRef.current, 500, { x: 600 })
  }, [])
  useLayoutEffect(() => {
    console.log('useLayoutEffect-2')
    moveTo(squRef2.current, 500, { x: 600 })
  }, [])
  console.log('render')
  return (
    <>
      <div className={`${classPrefix}-title`}>---useEffect && useLayoutEffect---</div>
      <div className={`${classPrefix}-square`} ref={squRef}></div>
      <div className={`${classPrefix}-square1`} ref={squRef1}></div>
      <div className={`${classPrefix}-square1`} ref={squRef2}></div>
    </>
  )
})
// render -> useLayoutEffect-1 -> useLayoutEffect-2 -> useEffect

useLayoutEffect和useEffect很像,唯一的不同點(diǎn)就是useEffect是異步執(zhí)行,而useLayoutEffect是同步執(zhí)行的。

當(dāng)函數(shù)組件刷新(渲染)時(shí),

包含useEffect的組件整個(gè)運(yùn)行過程如下:
1、觸發(fā)組件重新渲染(通過改變組件state或者組件的父組件重新渲染,導(dǎo)致子節(jié)點(diǎn)渲染)
2、組件函數(shù)執(zhí)行
3、組件渲染后呈現(xiàn)到屏幕上
4、useEffect hook執(zhí)行

包含useLayoutEffect的組件整個(gè)運(yùn)行過程如下:
1、觸發(fā)組件重新渲染(通過改變組件state或者組件的父組件重新渲染,導(dǎo)致子組件渲染)
2、組件函數(shù)執(zhí)行
3、useLayoutEffect hook執(zhí)行, React等待useLayoutEffect的函數(shù)執(zhí)行完畢
4、組件渲染后呈現(xiàn)到屏幕上

useEffect異步執(zhí)行的優(yōu)點(diǎn)是,react渲染組件不必等待useEffect函數(shù)執(zhí)行完畢,造成阻塞。

百分之99的情況,使用useEffect就可以了,唯一需要用到useLayoutEffect的情況就是,在使用useEffect的情況下,我們的屏幕會(huì)出現(xiàn)閃爍的情況(組件在很短的時(shí)間內(nèi)渲染了兩次)。

2.3 useReducer

參數(shù):第一個(gè)是reducer純函數(shù),第二個(gè)是初始state,第三個(gè)是修改初始state,用于重置
返回值是一個(gè)數(shù)組,數(shù)組第一個(gè)元素是state的當(dāng)前值,第二個(gè)元素是發(fā)送action的dispatch函數(shù)

管理包含多個(gè)子值的state對(duì)象時(shí),應(yīng)該怎么處理呢?以獲取某一接口為例,具體操作如下:

const fetchReducer = (state, action) => {
  switch (action.type) {
    case 'FETCH_INIT': // 接口初始化
      return {
        ...state,
        status: '初始化',
        loading: true,
        error: false
      };
    case 'FETCH_SUCCESS': // 請(qǐng)求成功
      return {
        ...state,
        status: '成功',
        loading: false,
        error: false,
        data: action.data
      };
    case 'FETCH_FAIL': // 請(qǐng)求失敗
      return {
        ...state,
        status: '失敗',
        loading: false,
        error: true
      };
    default:
      return null
  }
};

const DemoReducer = memo(() => {
  const [state, dispatch] = useReducer(fetchReducer, {
    loading: false,
    error: false,
    status: '',
    data: {}
  });

  const getData = async () => {
    const { data } = await Apis.GET_USER_INFO().catch(() => {
      dispatch({ type: 'FETCH_FAIL', data: null })
      return false
    })
    if (!data) return
    dispatch({ type: 'FETCH_SUCCESS', data })
  }

  useEffect(() => {
    dispatch({ type: 'FETCH_INIT' })
    getData()
  }, [])
  console.log('state---', state)

  return <div>
    <div className={`${classPrefix}-title`}>---useReducer---</div>
    <div className={`${classPrefix}-text`}>請(qǐng)求狀態(tài): {state.status}</div>
  </div>
})

結(jié)論:useReducer可以處理多個(gè)用useState實(shí)現(xiàn)的邏輯(加載狀態(tài)、錯(cuò)誤信息、請(qǐng)求數(shù)據(jù))

思考題:useState的出現(xiàn),讓我們可以使用多個(gè)state變量來保存state,如

const [width, setWidth] = useState(100)
const [height, setHeight] = useState(100)
const [left, setPageX] = useState(0)
const [top, setPageY] = useState(0)

也可以像class組件的this.state一樣,將所有state放在一個(gè)obj中,如

const [state, setState] = useState({ width: 100, height: 100, left: 0, top: 0 });

也可以使用useReducer處理,如

const stateReducer = (state, action) => {

  switch  (action.type) {
    case 'WIDTH':
      return {
        ...state,

        width: action.width
      };
   case 'HEIGHT':
    return {
      ...state,
      height: action.height
    };

   ......

}

const [state, dispatch] = useReducer(stateReducer, { width: 100, height: 100, left: 0, top: 0 })

這三種方法哪種好呢?

2.4 useMemo

參數(shù)是創(chuàng)建函數(shù)和依賴項(xiàng)數(shù)組。

返回值是一個(gè)帶有memoized的值,發(fā)生在render之前, 并且這個(gè)值僅在依賴項(xiàng)改變時(shí)才重新計(jì)算。

const DemoMemo = memo(() => {
  const [num1, setNum1] = useState(1)
  const [num2, setNum2] = useState(2)
  const expensive = useMemo(() => {
    console.log('運(yùn)算')
    let sum = 0
    for (let i = 0; i < num1 * 100; i++) {
      sum += i
    }
    return sum
  }, [num1])
  const handleClick1 = () => {
    console.log('num1++')
    setNum1(num1 + 1)
  }
  const handleClick2 = () => {
    console.log('num2++')
    setNum2(num2 + 1)
  }
  return <div>
    <div className={`${classPrefix}-title`}>---useMemo---</div>
    <div className={`${classPrefix}-text`}>當(dāng)前num1:{num1}</div>
    <div className={`${classPrefix}-text`}>當(dāng)前num2:{num2}</div>
    <div className={`${classPrefix}-text`}>當(dāng)前expensive(僅依賴num1):{expensive}</div>
    <div>
      {(() => {
        console.log('render');
        return null;
      })()}
      <button onClick={handleClick1}>num1++</button>
      <button onClick={handleClick2}>num2++</button>
    </div>
  </div>
})
// 運(yùn)算 => render
// 點(diǎn)擊num1++:  num1++ => 運(yùn)算 => render
// 點(diǎn)擊num2++:  num1++ => render

結(jié)論:useMemo發(fā)生在render前,返回一個(gè)緩存的數(shù)據(jù),且僅在依賴項(xiàng)改變后變化。

使用useMemo可以避免多余的計(jì)算開銷。

2.5 useCallback

參數(shù)是內(nèi)聯(lián)回調(diào)函數(shù)和依賴項(xiàng)數(shù)組,

返回值是回調(diào)函數(shù)的memoized版,該回調(diào)函數(shù)僅在某個(gè)依賴項(xiàng)改變時(shí)才會(huì)更新。

const set = new Set();
const DemoCallback = memo(() => {
  const [num1, setNum1] = useState(1)
  const [num2, setNum2] = useState(2)

  const callback = useCallback(() => {
    // 這里做復(fù)雜運(yùn)算
    return num1;
  }, [num1])
  set.add(callback)
  const handleClick1 = () => {
    setNum1(num1 + 1)
  }
  const handleClick2 = () => {
    setNum2(num2 + 1)
  }
  console.log('demo-callback', set.size)
  return <div>
    <div className={`${classPrefix}-title`}>---useCallback---</div>
    <div className={`${classPrefix}-text`}>當(dāng)前num1:{num1}</div>
    <Child callback={callback}/>
    <div>
      <button onClick={handleClick1}>num1++</button>
      <button onClick={handleClick2}>num2++</button>
    </div>
  </div>
})

const Child = memo(({ callback }) => {
  console.log('---child render')
  return <div>
    <div className={`${classPrefix}-text`}>child刷新(僅依賴num1):{set.size}</div>
  </div>
})
// demo-callback 1 => ---child render
// 點(diǎn)擊num1++:  demo-callback 2 => ---child render
// 點(diǎn)擊num2++:  demo-callback 2

結(jié)論:返回一個(gè)緩存的函數(shù),添加依賴項(xiàng)數(shù)組可以避免函數(shù)的無意義計(jì)算,降低了子組件的渲染開銷。

2.6 useRef

返回值是一個(gè)可變的ref對(duì)象,并且這個(gè)對(duì)象的值發(fā)生改變時(shí)不會(huì)引起頁面的渲染。

const DemoRef = memo(() => {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    inputEl.current.focus()
    inputEl.current.value = '自定義'
  };
  return (
    <>
      <div className={`${classPrefix}-title`}>---useRef---</div>
      {(() => {
        console.log('render');
        return null;
      })()}
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>click</button>
    </>
  );
})
// render
// 點(diǎn)擊click:  

結(jié)論:useRef可以存儲(chǔ)不需要引起頁面渲染的數(shù)據(jù);修改useRef值的唯一方法是修改.current,且修改后不會(huì)引起重渲染。

2.1中的問題,可以通過以下方法解決

const SecondDemoNew = memo(() => {
  const [ count, setCount ] = useState(0)
  const ref = useRef(0)
  const increment = () => {
    console.log('---useState---')
    setTimeout(() => {
      setCount((ref.current += 1))
      // setCount(count => count + 1)
    }, 3000)
  }
  return <div>
    <div className={`${classPrefix}-title`}>---useState && setState---</div>
    <div className={`${classPrefix}-text`}>當(dāng)前值:{count}</div>
    <button onClick={increment}>+1</button>
  </div>
})

另外, useRef也可以用來實(shí)現(xiàn)錨點(diǎn)跳轉(zhuǎn),具體如下

const scrollRef = useRef(null)

const scrollToRef = (ref) = >{ // 跳轉(zhuǎn)
  if (!ref) return

  ref.current.scrollIntoView({
    behavior: 'smooth'
  })
}

......

< div onClick = { () = >scrollToRef(scrollRef) } > 航行進(jìn)度 < /div>

<span ref={scrollRef}></span >

2.7 useContext

跨組件共享數(shù)據(jù)的鉤子函數(shù),接收一個(gè)context對(duì)象,并返回該對(duì)象的當(dāng)前值。

當(dāng)前的context值由上層組件中距離當(dāng)前組件最近的<MyContext.Provider>的value決定,并且父組件的context發(fā)生改變是,子組件都會(huì)重新渲染。

const MyContext = React.createContext() // 創(chuàng)建context,用于支持調(diào)用
const DemoContext = memo(() => {
  const [value, setValue] = useState('initValue')
  return (
    <div>
      <div className={`${classPrefix}-title`}>---useContext---</div>
      {(() => {
        console.log('render');
        return null;
      })()}
      <button onClick={() => {
        setValue('newValue')
      }}>
        改變value
      </button>
      <MyContext.Provider value={value}>
        <Child1 />
        <Child2 />
      </MyContext.Provider>
    </div>
  );
})

const Child1 = memo(() => {
  const value = useContext(MyContext)
  console.log('Child1-value', value)
  return <div className={`${classPrefix}-text`}>Child1-value: {value}</div>
})

const Child2 = () => {
  console.log('Child2');
  return <div className={`${classPrefix}-text`}>Child2</div>;
}
// render => Child1-value initValue => Child2
// 點(diǎn)擊btn后:  render => Child1-value newValue => Child2

結(jié)論:useContext會(huì)在context值變化時(shí)重新渲染,<MyContext.Provider>的value發(fā)生變化時(shí),包裹的組件無論是否訂閱value值,都會(huì)重新渲染,可以使用memo對(duì)未使用value的子組件進(jìn)行優(yōu)化。

2.8 自定義hook

有時(shí)候我們需要重復(fù)使用一些狀態(tài)邏輯,怎么處理可以在不增加組件的前提下實(shí)現(xiàn)復(fù)用,自定義hook可以達(dá)到這一目的。

通過自定義hook,抽取多個(gè)組件重復(fù)使用的邏輯,將這些重復(fù)的邏輯添加到一個(gè)叫做useSomething的自定義hook中,調(diào)用這一hook達(dá)到邏輯復(fù)用的目的,在不增加組件的情況下實(shí)現(xiàn)了邏輯共享。

自定義的hook是一個(gè)函數(shù),名稱以“use”開頭,函數(shù)內(nèi)部可用調(diào)用其他hook。以處理請(qǐng)求過程為例,自定義hook如下:

3 優(yōu)化

hook主要從以下三個(gè)方面對(duì)函數(shù)式組件進(jìn)行優(yōu)化:

useCallback用于緩存函數(shù)
useMemo用于緩存計(jì)算結(jié)果,簡單理解useCallback(fn, deps) === useMemo(() => fn, deps)
useReducer用于處理多狀態(tài)的state

4 小結(jié)

hooks執(zhí)行流程:

在react中,組件返回的JSX元素被轉(zhuǎn)換為虛擬DOM,就是下方的vnode,每個(gè)vnode上掛載了一個(gè)_component屬性,這個(gè)屬性指向組件實(shí)例。在組件實(shí)例上又掛載了一個(gè)_hooks屬性,這個(gè)_hooks屬性里保存了執(zhí)行一個(gè)組件時(shí),里面所有Hook方法的相關(guān)信息。

1.png

首先,有一個(gè)全局的currentIndex變量,當(dāng)組件第一次渲染或更新時(shí),它會(huì)在每次進(jìn)入一個(gè)函數(shù)組件的時(shí)候都重置為0,每次遇到一個(gè)hook方法就會(huì)加1,同時(shí)將這個(gè)hook方法加到_list(緩存)中。當(dāng)下次進(jìn)來或進(jìn)入下一組件時(shí),currentIndex又被重置為0;在組件更新時(shí),則會(huì)從_list中根據(jù)currentIndex取出對(duì)應(yīng)項(xiàng)。具體如下添加鏈接描述:

組件渲染 => currentIndex 重置 0 => 遇到 Hooks 方法,放進(jìn) _list數(shù)組 => 索引currentIndex++ => 渲染結(jié)束
組件更新 => currentIndex 重置 0 => 遇到 Hooks 方法,獲取 _list[currentIndex]=> currentIndex++ => 重復(fù)上面步驟 => 更新結(jié)束

hooks使用規(guī)則:(eslint-plugin-react-hooks控制以下兩條規(guī)則)

  • 只能在函數(shù)最外層調(diào)用 Hook。不要在循環(huán)、條件判斷或者子函數(shù)中調(diào)用(hook2拿到的state其實(shí)是上一次hook1執(zhí)行后的state, 而不是上一次hook2執(zhí)行后的state。如果把hook1放在一個(gè)if語句中,當(dāng)這個(gè)hook沒有執(zhí)行時(shí),這樣顯然會(huì)發(fā)生錯(cuò)誤)。
  • 只能在 React 的函數(shù)組件中調(diào)用 Hook(因?yàn)橹挥泻瘮?shù)組件的更新才會(huì)觸發(fā)renderWithHooks函數(shù),處理hooks的相關(guān)邏輯)。
    hooks優(yōu)化策略:
    優(yōu)化本身也會(huì)帶來大量的計(jì)算,無意義的優(yōu)化反而會(huì)增加額外的開銷。所以針對(duì)3中優(yōu)化需謹(jǐn)慎。

https://blog.csdn.net/qq_30997503/article/details/117228632?spm=1001.2014.3001.5502
https://github.com/brickspert/blog/issues/26

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評(píng)論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,786評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,438評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,796評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,995評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,230評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,697評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容

  • Effect Hook可以使得你在函數(shù)組件中執(zhí)行一些帶有副作用的方法。 上面這段代碼是基于上個(gè)state hook...
    xiaohesong閱讀 4,335評(píng)論 0 0
  • React Hooks 在了解React Hooks之前, 我們先看一下 Class 和函數(shù)式 的一般寫法 Cla...
    YM雨蒙閱讀 2,892評(píng)論 0 1
  • useState 1.基本使用 等價(jià)于 2. 復(fù)雜的state 3.使用狀態(tài) 4. 注意事項(xiàng) 1). 如果stat...
    sweetBoy_9126閱讀 3,042評(píng)論 0 6
  • 什么是hooks? hooks 是 react 在16.8版本開始引入的一個(gè)新功能,它擴(kuò)展了函數(shù)組件的功能,使得函...
    JoeRay61閱讀 564評(píng)論 0 0
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭,有人歡樂有人憂愁,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,578評(píng)論 28 53