Deep into React Hooks

前言node

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

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


Hooks 系统总览

clipboard.png

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

Dispatcher

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

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

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

对应源码spa

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

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

对应源码:code

// 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.

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

clipboard.png

相关文章
相关标签/搜索