Hooks的使用方法和实现原理

9,044 阅读7分钟

Hooks简介和概述?

Hooks 是 React 函数组件内一类特殊的函数(通常以 "use" 开头,比如 "useState"),使开发者能够在 function component 里依旧使用 state 和 life-cycles,以及使用 custom hooks 复用业务逻辑。

为什么要引进Hooks,要解决什么问题

当前react经常遇见的问题:

  1. 很难复用逻辑(只能用HOC,或者render props),会导致组件树层级很深
  2. 大型组件很难拆分和重构,也很难测试。
  3. 类组件很难理解,比如方法需要bindthis指向不明确
  4. 业务逻辑分散在组件的各个方法之中,导致重复逻辑或关联逻辑。

Hooks让我们更好地进行代码逻辑复用。 函数组件可以很好地进行逻辑复用,但是函数组件是无状态的,只能作为【纯组件】展示,不能处理局部state。Hooks让函数组件拥有了局部state,可以处理状态逻辑。

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]

这两个返回值分别对应之前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进行合并。

而使用Hooks,会将state进行拆分为一个个value,更新后,直接使用新值替换,不会进行state的合并。[state,setState]的结构也让值的更新逻辑更加清晰。

React默认提供的常用Hooks

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

useContext()

配合React.createContext({})使用,在组件间的共享状态

示例:

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']