Deep into React Hooks

前言

在React 16.7 的版本中,Hooks 诞生了,截止到目前, 也有五六个月了, 想必大家也也慢慢熟悉了这个新名词。

我也一样, 对着这个新特性充满了好奇, 也写了几个demo 体验一下, 这个特性使得我们可以在一个函数组件中实现管理状态, 可以说是十分的神奇。 楼主最近也看了一些这方面的文章, 在这里总结分享一下, 希望对大家有所启发。

Hooks 系统总览

《Deep into React Hooks》

首先, 我们需要知道的是, 只有在 React scope 内调用的 Hooks 才是有效的,那 React 用什么机制来保证 Hooks 是在正确的上下文被调用的呢?

Dispatcher

dispatcher 是一个包含了诸多 Hook functions 的共享对象,在 render phase,它会被自动的分配或者销毁,它也保证 Hooks 不会在React component 之外被调用。

Hooks 功能的开启和关闭由一个flag 控制,这意味着, 在运行时之中, 可以动态的开启,关闭 Hooks相关功能。

React 16.6.X 也有一些试验性的功能是通过这种方式控制的, 具体实现参考:

对应源码

  if (enableHooks) {
    ReactCurrentOwner.currentDispatcher = Dispatcher;
  } else {
    ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
  }

render 执行完毕之后,就销毁dispatcher, 这样也能组织在 react 渲染周期之外意外的调用Hooks.

对应源码:

// We're done performing work. Time to clean up.
  isWorking = false;
  ReactCurrentOwner.currentDispatcher = null;
  resetContextDependences();
  resetHooks();

// Yield back to main thread.

Hooks 的执行是由一个叫resolveDispatcher 的函数来决定的。 就像之前提到的, 在React 渲染周期之外 调用Hooks 是无效的, 这时候, React 也会跑出错误:

'Hooks can only be called inside the body of a function component.'

源码如下

function resolveDispatcher() {
  const dispatcher = ReactCurrentOwner.currentDispatcher;
  invariant(
    dispatcher !== null,
    'Hooks can only be called inside the body of a function component.',
  );
  return dispatcher;
}

以上我们了解了Hooks的基础机制, 下面我们再看几个核心概念。

Hooks 队列

我们都知道, Hooks 的调用顺序十分重要。

React 假设当你多次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的。

Hooks 不是独立的,就好比是根据调用顺序被串起来的一系列结点。

在了解这个机制之前,我们需要了解几个概念:

  • 在初次渲染的时候, Hooks会被赋予一个初始值。
  • 这个值在运行时会被更新。
  • React 会记住Hooks的状态。
  • React 给根据调用顺序给你提供正确的state。
  • React 会知道每个Hook具体属于哪个Fiber。

用一个例子来解释吧, 假设, 我们有一个状态集:

{
  foo: 'foo',
  bar: 'bar',
  baz: 'baz',
}

处理Hooks的时候,会被处理成一个队列, 每一个结点都是一个 state 的 model :

{
  memoizedState: 'foo',
  next: {
    memoizedState: 'bar',
    next: {
      memoizedState: 'bar',
      next: null
    }
  }
}

此处源码

function createHook(): Hook {
  return {
    memoizedState: null,

    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };
}

在一个function Component 被渲染之前, 一个名为 prepareHooks 的方法会被调用, 在这个方法里, 当前的Fiber 和 Hooks 队列重的第一个结点会被储存到一个全局变量里, 这样, 下次调用 useXXX 的时候, React 就知道改运行哪个context了。

对应源码:

let currentlyRenderingFiber
let workInProgressQueue
let currentHook

// Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:123
function prepareHooks(recentFiber) {
  currentlyRenderingFiber = workInProgressFiber
  currentHook = recentFiber.memoizedState
}

// Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:148
function finishHooks() {
  currentlyRenderingFiber.memoizedState = workInProgressHook
  currentlyRenderingFiber = null
  workInProgressHook = null
  currentHook = null
}

// Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:115
function resolveCurrentlyRenderingFiber() {
  if (currentlyRenderingFiber) return currentlyRenderingFiber
  throw Error("Hooks can't be called")
}
// Source: https://github.com/facebook/react/tree/5f06576f51ece88d846d01abd2ddd575827c6127/react-reconciler/src/ReactFiberHooks.js:267
function createWorkInProgressHook() {
  workInProgressHook = currentHook ? cloneHook(currentHook) : createNewHook()
  currentHook = currentHook.next
  workInProgressHook
}

function useXXX() {
  const fiber = resolveCurrentlyRenderingFiber()
  const hook = createWorkInProgressHook()
  // ...
}

function updateFunctionComponent(recentFiber, workInProgressFiber, Component, props) {
  prepareHooks(recentFiber, workInProgressFiber)
  Component(props)
  finishHooks()
}

更新结束后, 一个名为 finishHooks 的方法会被调用, Hooks 队列中第一个结点的引用会被记录在 memoizedState 变量里, 这个变量是全局的, 意味着可以在外部去访问, 比如:

const ChildComponent = () => {
  useState('foo')
  useState('bar')
  useState('baz')

  return null
}

const ParentComponent = () => {
  const childFiberRef = useRef()

  useEffect(() => {
    let hookNode = childFiberRef.current.memoizedState

    assert(hookNode.memoizedState, 'foo')
    hookNode = hooksNode.next
    assert(hookNode.memoizedState, 'bar')
    hookNode = hooksNode.next
    assert(hookNode.memoizedState, 'baz')
  })

  return (
    <ChildComponent ref={childFiberRef} />
  )
}

下面我们就拿最常见的Hook来具体分析。

State Hooks

比如:


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

其实, useState 的背后,是 useReducer, 它提供一个一个简单的预先定义的 reducer handler源码实现

也就意味着, 我们通过 useState拿到的两个值, 其实分别是一个 reducer 的 state, 和 一个 action 的 dispatcher.

此处源码:

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

如代码所示, 我们可以直接提供一个 state 和对应的 action dispatcher。 但是与此同时, 我们也可以直接传递一个包含action 的dispatcher 进去, 接收一个旧的state, 返回新的state.

这意味着我们可以把一个state的setter当作一个参数传递给Component, 然后在父组件里修改state, 而不用传递一个新的prop进去。

简单示例:

const ParentComponent = () => {
  const [name, setName] = useState()
  
  return (
    <ChildComponent toUpperCase={setName} />
  )
}

const ChildComponent = (props) => {
  useEffect(() => {
    props.toUpperCase((state) => state.toUpperCase())
  }, [true])
  
  return null
}

官网中也有类似的例子:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <>
      Count: {count}
      <button onClick={() => setCount(initialCount)}>Reset</button>
      <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
      <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
    </>
  );
}

说完了State, 我们再看一下Effect。

Effect Hooks

Efftect 稍微有些不同, 它增加了额外的逻辑层。 在深入具体的实现之前, 我们需要事先了解几点概念:

  • Effect Hooks 在 render 的时候被创建, 在 painting 之后被执行, 在下一次painting 之前被销毁。
  • Effect Hooks 按照定义的顺序执行。

需要注意的一点是, paintingrender 还是有所区别的,render method 只是创建了一个Fiber node, 还没开始 paint.

// 脑坑疼, 休息一下再补充,未完待续…

《Deep into React Hooks》

    原文作者:皮小蛋
    原文地址: https://segmentfault.com/a/1190000018743977
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞