阅读源码后,来说讲React Hooks是怎么实现的

React 16.7-alpha中新增了新功能:Hooks。总结他的功能就是:让FunctionalComponent具备ClassComponent的功能。前端

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

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

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

  useEffect(() => {
    subscribeToSomething()

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

  return (
    <input value={data} onChange={handleChange} /> ) } 复制代码

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

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

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

export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))
复制代码

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

同时,若是你的组件内事件多,那么你的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 = () => {}来快捷绑定,但也就解决了一个声明的问题,总体的复杂度仍是在的。redux

而后还有在componentDidMountcomponentDidUpdate中订阅内容,还须要在componentWillUnmount中取消订阅的代码,里面会存在不少重复性工做。最重要的是,在一个ClassComponent中的生命周期方法中的代码,是很难在其余组件中复用的,这就致使了了代码复用率低的问题。数组

还有就是class代码对于打包工具来讲,很难被压缩,好比方法名称。前端工程师

更多详细的你们能够去看ReactConf的视频,我这里就很少讲了,这篇文章的主题是从源码的角度讲讲Hooks是如何实现的react-router

先来了解一些基础概念

首先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都是封闭性较强的

原理

在知道上面的基础以后,对于Hooks为何可以保存无状态组件的原理就比较好理解了。

咱们假设有这么一段代码:

function FunctionalComponent () {
  const [state1, setState1] = useState(1)
  const [state2, setState2] = useState(2)
  const [state3, setState3] = useState(3)
}
复制代码

先来看一张图

react-hooks

在咱们执行functionalComponent的时候,在第一次执行到useState的时候,他会对应Fiber对象上的memoizedState,这个属性原来设计来是用来存储ClassComponentstate的,由于在ClassComponentstate是一整个对象,因此能够和memoizedState一一对应。

可是在Hooks中,React并不知道咱们调用了几回useState,因此在保存state这件事情上,React想出了一个比较有意思的方案,那就是调用useState后设置在memoizedState上的对象长这样:

{
  baseState,
  next,
  baseUpdate,
  queue,
  memoizedState
}
复制代码

咱们叫他Hook对象。这里面咱们最须要关心的是memoizedStatenextmemoizedState是用来记录这个useState应该返回的结果的,而next指向的是下一次useState对应的`Hook对象。

也就是说:

hook1 => Fiber.memoizedState
state1 === hook1.memoizedState
hook1.next => hook2
state2 === hook2.memoizedState
hook2.next => hook3
state3 === hook2.memoizedState
复制代码

每一个在FunctionalComponent中调用的useState都会有一个对应的Hook对象,他们按照执行的顺序以相似链表的数据格式存放在Fiber.memoizedState

重点来了:就是由于是以这种方式进行state的存储,因此useState(包括其余的Hooks)都必须在FunctionalComponent的根做用域中声明,也就是不能在if或者循环中声明,好比

if (something) {
  const [state1] = useState(1)
}

// or

for (something) {
  const [state2] = useState(2)
}
复制代码

最主要的缘由就是你不能确保这些条件语句每次执行的次数是同样的,也就是说若是第一次咱们建立了state1 => hook1, state2 => hook2, state3 => hook3这样的对应关系以后,下一次执行由于something条件没达成,致使useState(1)没有执行,那么运行useState(2)的时候,拿到的hook对象是state1的,那么整个逻辑就乱套了,因此这个条件是必需要遵照的!

setState

上面讲了Hooksstate是如何保存的,那么接下去来说讲如何更新state

咱们调用的调用useState返回的方法是酱紫的:

var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [workInProgressHook.memoizedState, dispatch];
复制代码

调用这个方法会建立一个update

var update = {
  expirationTime: _expirationTime,
  action: action,
  callback: callback !== undefined ? callback : null,
  next: null
}
复制代码

这里的action是咱们调用setState1传入的值,而这个update会被加入到queue上,由于可能存在一次性调用屡次setState1的清空(跟React的batchUpdate有关,之后有机会讲。)

在收集完这全部update以后,会调度一次React的更新,在更新的过程当中,确定会执行到咱们的FunctionalComponent,那么就会执行到对应的useState,而后咱们就拿到了Hook对象,他保存了queue对象表示有哪些更新存在,而后依次进行更新,拿到最新的state保存在memoizedState上,而且返回,最终达到了setState的效果。

总结

其实本质上跟ClassComponent是差很少的,只不过由于useState拆分了单一对象state,因此要用一个相对独特的方式进行数据保存,并且会存在必定的规则限制。

可是这些条件彻底不能掩盖Hooks的光芒,他的意义是在是太大了,让React这个 函数式编程范式的框架终于摆脱了要用类来建立组件的尴尬场面。事实上类的存在乎义确实不大,好比PuerComponent如今也有对应的React.memo来让函数组件也能达到相同的效果。

最后,由于真的要把源码摊开来说,就会涉及到一些其余的源码内容,好比workInProgress => current的转换,expirationTime涉及的调度等,反而会致使你们没法理解本篇文章的主体Hooks,因此我在写完完整源码解析后又总结概括了这篇文章来单独发布。但愿能帮助各位童鞋更好得理解Hooks,并能大胆用到实际开发中去。

由于:真的很好用啊!!!

注意

目前react-hot-loader不能和hooks一块儿使用,详情,因此你能够考虑等到正式版再用。

我是Jocky,一个专一于React技巧和深度分析的前端工程师,React绝对是一个越深刻学习,越能让你以为他的设计精巧,思想超前的框架。关注我获取最新的React动态,以及最深度的React学习。更多的文章看这里

相关文章
相关标签/搜索