Hooks的使用方法和實現原理

Hooks簡介和概述?

Hooks 是 React 函數組件內一類特殊的函數(一般以 "use" 開頭,好比 "useState"),使開發者可以在 function component 裏依舊使用 state 和 life-cycles,以及使用 custom hooks 複用業務邏輯。javascript

爲何要引進Hooks,要解決什麼問題

當前react常常碰見的問題:java

  1. 很難複用邏輯(只能用HOC,或者render props),會致使組件樹層級很深
  2. 大型組件很難拆分和重構,也很難測試。
  3. 類組件很難理解,好比方法須要bindthis指向不明確
  4. 業務邏輯分散在組件的各個方法之中,致使重複邏輯或關聯邏輯。

Hooks讓咱們更好地進行代碼邏輯複用。 函數組件能夠很好地進行邏輯複用,可是函數組件是無狀態的,只能做爲【純組件】展現,不能處理局部state。Hooks讓函數組件擁有了局部state,能夠處理狀態邏輯。react

Hooks的種類

  • State hooks (在 function component 中使用 state)
  • Effect hooks (在 function component 中使用生命週期和 side effect)
  • Custom hooks (自定義 hooks 用來複用組件邏輯,解決了上述的第一個動機中闡述的問題)

State Hooks

import { useState } from 'react';

function Example() {
  // Declare a new state variable, which we'll call "count" const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } 複製代碼

Hooks會返回一個Tuple,結構爲[value, setValue]web

這兩個返回值分別對應以前react裏的算法

  • this.state
  • this.setState

咱們還能夠在函數中同時使用多個state編程

function ExampleWithManyStates() {
  // Declare multiple state variables!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}
複製代碼

以前更新state中值,經過this.setState({ fruit: 'orange' }),會對以前的state和更新後的state進行合併。redux

而使用Hooks,會將state進行拆分爲一個個value,更新後,直接使用新值替換,不會進行state的合併。[state,setState]的結構也讓值的更新邏輯更加清晰。react-native

React默認提供的經常使用Hooks

  • useState()
  • useContext()
  • useReducer()

useContext()

配合React.createContext({})使用,在組件間的共享狀態數組

示例:bash

const AppContext = React.createContext({});

<AppContext.Provider value={{
  username: 'superawesome'
}}>
  <div className="App">
    <Navbar/>
    <Messages/>
  </div>
</AppContext.Provider>
複製代碼

而後在Navbar組件內就能夠直接使用AppContext

const Navbar = () => {
  const { username } = useContext(AppContext);
  return (
    <div className="navbar">
      <p>AwesomeSite</p>
      <p>{username}</p>
    </div>
  );
}
複製代碼

useReducer()

用來簡單替代redux作狀態管理,可是無法提供中間件(middleware)和時間旅行(time travel)等複雜場景

示例:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </>
  );
}
複製代碼

Effect Hooks

effectHooks讓咱們能夠在函數組件內使用生命週期方法,咱們能夠在這裏更新DOM,獲取數據等具備'反作用'的行爲。effect Hook會在組件每次render後執行,ruturn的函數會在組件卸載時執行,若要讓effect hook只在組件首次加載時執行,能夠傳入一個空數組做爲第二個參數,也能夠在數組中指定依賴項,只有依賴項改變時,effectHooks纔會執行。

import { useState, useEffect } from 'react';

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

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }
  // ...
}
複製代碼

Custom Hooks

自定義Hook是一個以'use'開頭的javascript函數,能夠調用其餘的Hooks,從而進行邏輯封裝,複用代碼。例如:

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

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  });

  return isOnline;
}
複製代碼

其餘函數組件就可使用:

function FriendStatus(props) {
  const isOnline = useFriendStatus(props.friend.id);

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}
*********************************
function FriendListItem(props) {
  const isOnline = useFriendStatus(props.friend.id);

  return (
    <li style={{ color: isOnline ? 'green' : 'black' }}>
      {props.friend.name}
    </li>
  );
}
複製代碼

Hooks必須在函數頂層使用,不能用於條件,循環,嵌套中。 Hooks會逐步徹底替代class組件,目前還沒法支持getSnapshotBeforeUpdate和componentDidCatch生命週期的功能。

Hooks的實現原理

首先咱們須要整理下react的數據更新和視圖渲染機制。以前都是經過調用setState來更改數據,頁面進行re-render,咱們先來看看setState是如何工做的。

react的基礎架構

React的基礎架構分爲三個部分:react基礎包、react-reconciler、renderer渲染模塊

react基礎模塊: react 基礎 API 及組件類,組件內定義 render 、setState 方法和生命週期相關的回調方法,相關 API 以下:

const React = {
  Children: {},

  createRef,
  Component,
  PureComponent,

  createContext,
  forwardRef,

  Fragment: REACT_FRAGMENT_TYPE,
  StrictMode: REACT_STRICT_MODE_TYPE,
  unstable_AsyncMode: REACT_ASYNC_MODE_TYPE,
  unstable_Profiler: REACT_PROFILER_TYPE,

  createElement: __DEV__ ? createElementWithValidation : createElement,
  cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
  createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
  isValidElement: isValidElement,
};
複製代碼

renderer渲染模塊: 針對不一樣宿主環境採用不一樣的渲染方法實現,如 react-dom, react-webgl, react-native, react-art, 依賴 react-reconciler模塊, 注入相應的渲染方法到 reconciler 中,react-dom 中相關的 API 以下:

const ReactDOM: Object = {
  createPortal,

  findDOMNode(
    componentOrElement: Element | ?React$Component<any, any>,
  ): null | Element | Text {},

  hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {},

  render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {},

  unstable_renderSubtreeIntoContainer() {},

  unmountComponentAtNode(container: DOMContainer) {},

  unstable_batchedUpdates: DOMRenderer.batchedUpdates,

  unstable_deferredUpdates: DOMRenderer.deferredUpdates,

  unstable_interactiveUpdates: DOMRenderer.interactiveUpdates,

  flushSync: DOMRenderer.flushSync,

  unstable_flushControlled: DOMRenderer.flushControlled,
}
複製代碼

react-reconciler核心模塊:負責調度算法及 Fiber tree diff, 鏈接 react基礎包 和 renderer 模塊,注入 setState 方法到 component 實例中,在 diff 階段執行 react 組件中 render 方法,在 patch 階段執行 react 組件中生命週期回調並調用 renderer 中注入的相應的方法渲染真實視圖結構。

setState的工做原理

setState定義在React.Component中,可是React包中只是定義API,並無具體實現邏輯。相似的還有createContext()等大多數功能都是在‘渲染器’中實現的。react-dom、react-dom/server、 react-native、 react-test-renderer、 react-art都是常見的渲染器。因此咱們在使用react新特性的時候,react和react-dom都須要更新。

setState在React.Component中定義updater

Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
複製代碼

在具體的渲染器中會本身實現updater:

// React DOM 內部
var classComponentUpdater = {
  isMounted: isMounted,
  enqueueSetState: function (inst, payload, callback) {
    var fiber = get(inst);
    var currentTime = requestCurrentTime();
    var expirationTime = computeExpirationForFiber(currentTime, fiber);

    var update = createUpdate(expirationTime);
    update.payload = payload;
    if (callback !== undefined && callback !== null) {
      {
        warnOnInvalidCallback$1(callback, 'setState');
      }
      update.callback = callback;
    }

    flushPassiveEffects();
    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  enqueueReplaceState: function (inst, payload, callback) {
    //註釋了
  },
  enqueueForceUpdate: function (inst, callback) {
    //註釋了
  }
};
複製代碼

Hooks也是使用了相同的設計,使用了‘dispatcher’對象,來代替‘updater’。咱們調用useState()時,都被轉發給當前的dispatcher。 updater字段和dispatcher對象都是使用依賴注入的通用編程原則的形式。在這兩種狀況下,渲染器將諸如setState之類的功能的實現「注入」到通用的React包中,以使組件更具聲明性。

useState的工做原理

useState是如何讓無狀態的函數組件能夠保存狀態,更新視圖,和this.setState的更新有啥區別?

React中有一個基礎對象ReactElement,它由React.createElement()建立的

React.createElement(
  type,
  [props],
  [...children]
)

//舉個例子
class Hello extends React.Component {
  render() {
    return <div>Hello {this.props.toWhat}</div>;
  }
}
ReactDOM.render(
  <Hello toWhat="World" />,
  document.getElementById('root')
);

//徹底等價於
class Hello extends React.Component {
  render() {
    return React.createElement('div', null, `Hello ${this.props.toWhat}`);
  }
}
ReactDOM.render(
  React.createElement(Hello, {toWhat: 'World'}, null),
  document.getElementById('root')
);

const element = {
    $$typeof: REACT_ELEMENT_TYPE, // 是不是普通Element_Type
    
    // Built-in properties that belong on the element
    type: type, // 咱們的組件,好比`class MyComponent`
    key: key,
    ref: ref,
    props: props,
    children: children,
    
    // Record the component responsible for creating this element.
    _owner: owner,
};
複製代碼

這是一個vdom節點,在React16以前,React會根據這個vdom節點生成真實的dom結構。React16以後,官方引入了Fiber結構,react的基本架構也變得更加複雜了。React會將vdom節點對應爲一個Fiber節點,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;
    
    // Fiber
    this.return = null;
    this.child = null;
    this.sibling = null;
    
    this.memoizedState = null;
    this.updateQueue = null;
    
    this.index = 0;
    this.ref = null;
    this.pendingProps = pendingProps;
    this.memoizedProps = null;
    this.firstContextDependency = null;
    
    // ...others
}
複製代碼

其中的this.updateQueue用來存儲setState的更新隊列,this.memoizedState來儲存組件內的state狀態,類組件中是用來存儲state對象的,在Hooks中用來存儲Hook對象。

//類組件中更新state的update對象
var update = {
    expirationTime: expirationTime,
    tag: UpdateState,
    payload: null,
    callback: null,
    next: null,
    nextEffect: null
};

//函數組件中的Hook對象
{
    baseState,
    next,
    baseUpdate,
    queue,
    memoizedState
};

//類組件中的updateQueue的結構
var queue = {
    baseState: baseState,
    firstUpdate: null,
    lastUpdate: null,
    firstCapturedUpdate: null,
    lastCapturedUpdate: null,
    firstEffect: null,
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null
};

//每新增一個update就加入到隊列中
function appendUpdateToQueue(queue, update) {
  // Append the update to the end of the list.
  if (queue.lastUpdate === null) {
    // Queue is empty
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
}
複製代碼

memoizedState的更新機制

Hooks的更新分紅兩步,初始化時進行mount操做,更新時進行update操做。分別經過HooksDispatcherOnMountInDEV和HooksDispatcherOnUpdateInDEV兩個對象來存儲全部Hooks更新的函數。

HooksDispatcherOnMountInDEV = {
    readContext: function (context, observedBits) {
    },
    useCallback: function (callback, deps) {
    },
    useContext: function (context, observedBits) {
    },
    useEffect: function (create, deps) {
    },
    useImperativeHandle: function (ref, create, deps) {
    },
    useLayoutEffect: function (create, deps) {
    },
    useMemo: function (create, deps) {
    },
    useReducer: function (reducer, initialArg, init) {
    },
    useRef: function (initialValue) {
    },
    useState: function (initialState) {
      var hook = mountWorkInProgressHook();
      if (typeof initialState === 'function') {
        initialState = initialState();
      }
      hook.memoizedState = hook.baseState = initialState;
      var queue = hook.queue = {
        last: null,
        dispatch: null,
        eagerReducer: basicStateReducer,
        eagerState: initialState
      };
      var dispatch = queue.dispatch = dispatchAction.bind(null,
      // Flow doesn't know this is non-null, but we do. currentlyRenderingFiber$1, queue); return [hook.memoizedState, dispatch]; }, useDebugValue: function (value, formatterFn) { } }; //其中的dispatch即爲咱們調用的‘setState’函數,核心代碼爲: function dispatchAction(fiber, queue, action) { //註釋了******* var update = { expirationTime: renderExpirationTime, action: action, eagerReducer: null, eagerState: null, next: null }; if (renderPhaseUpdates === null) { renderPhaseUpdates = new Map(); } renderPhaseUpdates.set(queue, update); } HooksDispatcherOnUpdateInDEV = { //註釋了********** useState: function (initialState) { currentHookNameInDev = 'useState'; var prevDispatcher = ReactCurrentDispatcher$1.current; ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV; try { return updateState(initialState); } finally { ReactCurrentDispatcher$1.current = prevDispatcher; } }, }; 複製代碼
function updateState(initialState) {
  return updateReducer(basicStateReducer, initialState);
}

function updateReducer(reducer, initialArg, init) {
    //  註釋了**********
    var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
      if (firstRenderPhaseUpdate !== undefined) {
        renderPhaseUpdates.delete(queue);
        var newState = hook.memoizedState;
        var update = firstRenderPhaseUpdate;
        do {
          // Process this render phase update. We don't have to check the // priority because it will always be the same as the current // render's.
          var _action = update.action;
          newState = reducer(newState, _action);
          update = update.next;
        } while (update !== null);
}
複製代碼

update對象中的action就是使用setState的參數,update會被加入到更新queue中,在全部‘update’都收集完後,會觸發react的更新。更新時,執行到函數組件中的useState,而後拿到Hook對象,取出其中的queue對象,依次進行更新,獲得新的state保存到memoizedState上,並返回,更新視圖。

其中memoizedState是用來記錄這個useState應該返回的結果的,而next指向的是下一次useState對應的`Hook對象。

例:

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

執行的順序爲:

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

next是依賴上一次的state的值,若是某個useState沒有執行,這個對應關係就亂了。因此,react規定使用Hooks時,必須在根做用域下使用,不能用於條件語句,循環中。

模擬實現Hooks

整理下Hooks具備的特徵:

  1. 調用useState(),返回一個Tuple([value,setValue])
  2. useState(),只能在頂層做用域使用,依賴於建立時的順序。
  3. 只能在函數組件中使用,不能用於類組件

可使用數組結構來模擬實現:

let state = [];
let setters = [];
let firstRun = true;
let cursor = 0;

function createSetter(cursor) {
  return function setterWithCursor(newVal) {
    state[cursor] = newVal;
  };
}

export function useState(initVal) {
  if (firstRun) {
    state.push(initVal);
    setters.push(createSetter(cursor));
    firstRun = false;
  }

  const setter = setters[cursor];
  const value = state[cursor];

  cursor++;
  return [value, setter];
}

function RenderFunctionComponent() {
  const [firstName, setFirstName] = useState("Rudi"); // cursor: 0
  const [lastName, setLastName] = useState("Yardley"); // cursor: 1

  return (
    <div>
      <Button onClick={() => setFirstName("Richard")}>Richard</Button>
      <Button onClick={() => setFirstName("Fred")}>Fred</Button>
    </div>
  );
}

function MyComponent() {
  cursor = 0; // resetting the cursor
  return <RenderFunctionComponent />; // render
}

console.log(state); // Pre-render: []
MyComponent();
console.log(state); // First-render: ['Rudi', 'Yardley']
MyComponent();
console.log(state); // Subsequent-render: ['Rudi', 'Yardley']

// click the 'Fred' button

console.log(state); // After-click: ['Fred', 'Yardley']
複製代碼
相關文章
相關標籤/搜索