# 从源码看Hooks

React 17-alpha中新增了新功能:Hooks。总结他的功能就是:让FunctionalComponent具有ClassComponent的功能。

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

function FunComp(props) {
  const [data, setData] = useStat('initialState')

  function handleChange(e) {
    setData(e.target.value)
  }

  useEffect(() => {
    subscribeToSomething()

    return () => {
      unSubscribeToSomething()
    }
  })

  return (
    <input value={data} onChange={handleChange} />
  )
}

按照Dan的说法,设计Hooks主要是解决ClassComponent的几个问题:

  1. 很难复用逻辑(只能用HOC,或者render props),会导致组件树层级很深
  2. 会产生巨大的组件(指很多代码必须写在类里面)
  3. 类组件很难理解,比如方法需要bindthis指向不明确

这些确实是存在的问题,比如我们如果用了react-router+redux+material-ui,很可能随便一个组件最后export出去的代码是酱紫的:

export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))

这就是一个4层嵌套的HOC组件

同时,如果你的组件内事件多,那么你的constructor里面可能会酱紫:

class MyComponent extends React.Component {
  constructor() {
    // initiallize

    this.handler1 = this.handler1.bind(this)
    this.handler2 = this.handler2.bind(this)
    this.handler3 = this.handler3.bind(this)
    this.handler4 = this.handler4.bind(this)
    this.handler5 = this.handler5.bind(this)
    // ...more

  }
}

虽然最新的class语法可以用handler = () => {}来快捷绑定,但也就解决了一个声明的问题,整体的复杂度还是在的。

然后还有在componentDidMountcomponentDidUpdate中订阅内容,还需要在componentWillUnmount中取消订阅的代码,里面会存在很多重复性工作。最重要的是,在一个ClassComponent中的生命周期方法中的代码,是很难在其他组件中复用的,这就导致了了代码复用率低的问题。

还有就是class代码对于打包工具来说,很难被压缩,比如方法名称。

更多详细的大家可以去看ReactConf的视频 (opens new window),我这里就不多讲了,这篇文章的主题是从源码的角度讲讲Hooks是如何实现的

# 先来了解一些基础概念

首先useState是一个方法,它本身是无法存储状态的

其次,他运行在FunctionalComponent里面,本身也是无法保存状态的

useState只接收一个参数initial value,并看不出有什么特殊的地方。所以React在一次重新渲染的时候如何获取之前更新过的state呢?

在开始讲解源码之前,大家先要建立一些概念:

# React Element

JSX翻译过来之后是React.createElement,他最终返回的是一个ReactElement对象,他的数据解构如下:

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,

  // Record the component responsible for creating this element.
  _owner: owner,
};

这其中需要注意的是type,在我们写<MyClassComponent {...props} />的时候,他的值就是MyClassComponent这个class,而不是他的实例,实例是在后续渲染的过程中创建的。

# 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.index = 0;

  this.ref = null;

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.firstContextDependency = null;

  // ...others
}

在这里我们需要注意的是this.memoizedState,这个key就是用来存储在上次渲染过程中最终获得的节点的state的,每次执行render方法之前,React会计算出当前组件最新的state然后赋值给class的实例,再调用render

所以很多不是很清楚React原理的同学会对React的ClassComponent有误解,认为statelifeCycle都是自己主动调用的,因为我们继承了React.Component,它里面肯定有很多相关逻辑。事实上如果有兴趣可以去看一下Component的源码,大概也就是100多行,非常简单。所以在React中,class仅仅是一个载体,让我们写组件的时候更容易理解一点,毕竟组件和class都是封闭性较强的

# 开始讲源码

既然我们知道了存储stateprops的是在Fiber对象上,那么我们就不需要担心上面提到的FunctionalComponent无法存储状态的问题了,所以接下去就让我们一步步看看useState具体做了啥吧。

由于是alpha版本,所以在gitgub上没找到源码,似乎React团队的发布策略是正式版发布才会把代码更新到master分支。所以只能看打包过的源码了

// react中的useState
function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

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

到这里React中的useState就结束了,剩下的就是ReactCurrentOwner.currentDispatcher是个啥,这个东西会在react-dom中渲染的过程中变化,所以我们要到react-dom中寻找

renderRoot中我们找到了:

function renderRoot(root, isYieldy, isExpired) {
  // ...
  ReactCurrentOwner$2.currentDispatcher = Dispatcher;

  // workLoop

  ReactCurrentOwner$2.currentDispatcher = null;
  resetHooks();
}

var Dispatcher = {
  readContext: readContext,
  useCallback: useCallback,
  useContext: useContext,
  useEffect: useEffect,
  useImperativeMethods: useImperativeMethods,
  useLayoutEffect: useLayoutEffect,
  useMemo: useMemo,
  useMutationEffect: useMutationEffect,
  useReducer: useReducer,
  useRef: useRef,
  useState: useState
};

renderRoot这个方法是React开始渲染整棵树的入口,讲起来就超级长了,所以有机会做React源码分析的时候再详细讲,可以关注一波获取最新动态。在这里我们只需要知道,在开始渲染的时候给他设置了currentDispatcher,而渲染的过程中是会调用每个节点的,也就是说执行到我们有Hooks的组件的时候自然会调用Dispatcher.useState

这里还有一个方法叫resetHooks,我们后面再讲。

useState的代码:

function useState(initialState) {
  return useReducer(basicStateReducer,
  // useReducer has a special case to support lazy useState initializers
  initialState);
}

useReducer的代码较长,我放在gist里

这里主要做了什么呢?首先要找到workInProgressHook,这个就跟Fiber跟新的时候的workInProgress类似,指代的是当前正在工作的对象。在这里就是执行到了哪个useState,要找到他对应的Hook对象,数据结构如下:

{
  memoizedState: null,

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

  next: null
};

Fiber对象一样,他也有一个memoizedState在使用Hooks的时候,我们把ClassComponent中以对象存储的state,拆成一个个key对应的Hook的关系,所以每个Hook会对应一个memoizedState

找到workInProgressHook使用的是createWorkInProgressHook,在React中存在着这种current => workInProgress的关系,这个一下两下讲不清楚,就不在这详细展开了。这里只需要记住两个关键点:

  • 如果第一次渲染,每个workInProgressHook都会重新创建
  • 如果之前有创建过,会从之前保存的上面复制一个

然后reducer会在这个workInProgressHook上执行更新,这部分代码也比较复杂,就不详细展开了,直接讲一下重点:

  • queue是调用useState返回的那个方法的时候生成的,我们可能一次性调用很多次,所以queue是个链状数据结构

# updateFunctionalComponent

看到这里我们先停一下,我们先来了解一下什么时候才会执行到useState,答案就是在FunctionalComponent被执行的时候,那么也就是updateFunctionalComponent的时候(这个也有机会分析源码的时候再详细讲)。在这里我们看到这么一句代码:

// Component就是FunctionalComponent本身
// nextProps就是新的props
nextChildren = finishHooks(Component, nextProps, nextChildren, context);

finishHooks代码如下:

didScheduleRenderPhaseUpdate在调用useState返回的更新方法的时候会设置为true,我们先默认他为true

# 注意