前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[第8期] Deep into React Hooks

[第8期] Deep into React Hooks

作者头像
皮小蛋
发布2020-03-02 10:42:46
6160
发布2020-03-02 10:42:46
举报
文章被收录于专栏:前端皮小蛋前端皮小蛋

前言

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

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

Hooks 系统总览

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

Dispatcher

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

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

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

对应源码

代码语言:javascript
复制
  if (enableHooks) {
    ReactCurrentOwner.currentDispatcher = Dispatcher;
  } else {
    ReactCurrentOwner.currentDispatcher = DispatcherWithoutHooks;
  }
代码语言:javascript
复制
render 执行完毕之后,就销毁dispatcher, 这样也能组织在 react 渲染周期之外意外的调用Hooks.

对应源码:

代码语言:javascript
复制
// 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.'

源码如下:

代码语言:javascript
复制
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。

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

代码语言:javascript
复制
{
  foo: 'foo',
  bar: 'bar',
  baz: 'baz',
}

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

代码语言:javascript
复制
{
  memoizedState: 'foo',
  next: {
    memoizedState: 'bar',
    next: {
      memoizedState: 'bar',
      next: null
    }
  }
}

此处源码:

代码语言:javascript
复制
function createHook(): Hook {
  return {
    memoizedState: null,

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

    next: null,
  };
}

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

对应源码:

代码语言:javascript
复制
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 变量里, 这个变量是全局的, 意味着可以在外部去访问, 比如:

代码语言:javascript
复制
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

比如:

代码语言:javascript
复制

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

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

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

此处源码:

代码语言:javascript
复制
function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

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

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

简单示例:

代码语言:javascript
复制
const ParentComponent = () => {
  const [name, setName] = useState()
  
  return (
    <ChildComponent toUpperCase={setName} />
  )
}

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

官网中也有类似的例子:

代码语言:javascript
复制
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.

暂时就先分析到这, 后面会再做补充。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端皮小蛋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Hooks 系统总览
  • Dispatcher
  • Hooks 队列
    • State Hooks
      • Effect Hooks
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档