拥抱 React Hooks (一)基础

React Hooks 起源

  • React 一直都提倡使用函数式组件。更轻便,更优雅,性能更佳。函数式组件又称无状态组件(FSC)
  • 以前,需要使用 state ,生命周期等React 特性,必须重构为 class 组件。
  • Hooks 是 React 16.8 新增的特性,它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
  • 现在,你可以直接在现有的函数式组件中使用 Hooks,而无须重构为 class 组件。
  • 全新的思维方式。no magic, just javascript and some rules。

类组件被诟病

  • 类(累):自js开天辟地,就是面向函数式编程(FP), 面向对象编程(OOP)为何物。烦人的构造函数。super是什么?……
  • this绑定:类方法不会自动绑定 this 到实例上。现有四种bind方式。不优雅,易出错,bind还影响性能(使用箭头函数后有所改善)。
  • setState(): 异步更新机制,state浅合并机制。不理解这些概念,很容易踩坑。
  • 生命周期耦合:每个生命周期方法通常包含一堆不相关的逻辑;不同生命周期中的逻辑又有关联。

下面组件来自实际项目,经过简化和微调(方便演示和直观感受),基本上暴露出了上面所有问题。业务逻辑严谨性不用推敲:

class NumberInput extends React.Component {
  constructor(props) {
    // 为什么必须super,不传props会怎样
    super(props)
    this.state = {
      focus: false
    }
    this.tradingpwd = ''
    // 第一种bind,官方推荐
    ;['onBlur'].forEach(method => {
      this[method] = this[method].bind(this)
    })
  }
  // 下面两个生命周期得相互配合,实现某些功能
  componentDidMount() {
    this.tradingPwdHideInput.focus()
    // 处理某类兼容问题
    let bodyTop = document.body.getBoundingClientRect().top
    const styleText = 'position: fixed; width: 100%; top: ' + bodyTop + 'px'
    document.body.style.cssText = styleText
  }

  componentWillUnmount() {
    this.tradingPwdHideInput.blur()
    document.body.style.position = 'static'
  }

  tradingPwdChange(e) {
    // ...
    this.tradingpwd = e.target.value
    this.props.inputChangeCallback(e.target.value)
    // ...
  }
  // 第二种bind
  onFocus = () => {
    this.setState({
      focus: true
    })
  }
  onBlur() {
    this.setState({
      focus: false
    })
  }

  render() {
    return (
      <div className={classNames('NumberInput')}>
        <input
          type='tel'
          ref={ref => {
            this.tradingPwdHideInput = ref
          }}
          id='tradingPwdHideInput'
          /* 第三种bind */
          onClick={() => {
            this.tradingPwdHideInput.focus()
          }}
          onBlur={ this.onBlur }
          onFocus={ this.onFocus }
          /* 第四种bind,不推荐,在每次 render() 方法执行时绑定类方法,消耗性能*/
          onChange={ this.tradingPwdChange.bind(this) }
        />
      </div>
    )
  }
}

proposal-class-fields 新提案会改善上述情况,目前处于第三阶段。

随着类组件趋于复杂,还有其他诟病:

  • 难拆分,本地state逻辑到处都是,当组件越来越复杂,想拆分比较难。
  • 状态逻辑难复用:需要引入高阶特性进行代码重构,需要调整组件结构,成本高。
  • 抽象地狱:大型React往往使用render props ,HOC,Context 等高阶特性,形成大量包装组件(wrapping components)。层级冗余,逻辑难追踪。

深度包装的组件长这样:

import { compose } from 'recompose';
import { withRouter } from 'react-router-dom';
function App({ history, state, dispatch }) {
  return (
    <ThemeContext.Consumer>
      {theme =>
        <Content theme={theme}>
          ... 
        </Content>
      }
    </ThemeContext.Consumer>
  );
}
export default compose(
  withRouter,
  withReducer(reducer, initialState)
)(App);

确实很抽象。这是 React 中典型的抽象地狱(Abstraction hell)问题,也叫包装地狱(The wrapper hell)

Hooks 优越性

Hooks 引入的一个重要的原因,就是类组件存在着种种诟病。那他必然存在一些优越性。

在说明这些优越性之前,先了解一个概念:

副作用:React 中主要指那些没有发生在数据向视图(M-V)转换过程中的逻辑,如 Ajax 请求、访问原生 DOM 元素、本地持久化缓存、绑定/解绑事件、添加/取消订阅、设置定时器、记录日志等。

Hooks 的优越性:

  • 函数式编程:No class, No super, No this。对于不了解 OOP 的 React 初学者更友好。
  • 有状态逻辑易复用:可以通过 Custom Hook(后面讲解)重构,而不用修改组件结构。
  • 易拆分:状态管理和副作用管理松耦合,原子性强。很容易将一些相关联的逻辑拆分成更小的函数。
  • 可逐步引入:Hooks 向后兼容,与现有代码可并行工作,因此我们可以逐步采用它们。
  • 副作用分组:很多副作用逻辑分散在类组件生命周期函数中。而 Hooks 可以将每个副作用的设置和清理封装在一个函数中。
  • 副作用分离:副作用操作都在页面渲染之后。

抛弃类组件?

既然 Hooks 存在这么多优越性。那是不是就到了抛弃 class 组件的时候了。

对此,官方说:

  • 新版本依然支持 class 相关API,在相当一段时期内,class 组件 和 Hooks 组件并存。
  • 向后兼容,是加法。注意,是函数组件的加法,即 Hooks 只能用在函数组件中。
  • 推荐使用 函数组件 + Hooks

个人觉得:

  • 当下:不抛弃,不放弃。class 组件将我们带到了 OOP 的世界,OOP在编程界举足轻重,其思想是值得学习的。即便 class 组件已然成为一种历史产物,但他的存量巨大,依然需要去维护,去慢慢消化。
  • 未来:有可能弃用 class 组件及其生命周期。一方面,前端的世界本来变化就快。另一方面,class 组件确实存在一些弊端。随着 Hooks的不断成熟(或新的技术诞生), 使得开发效率,代码可读性,维护性,性能等综合优势比较明显的时候,弃用是必然。

所以:

  • 对于 React 老司机:拥抱Hooks,是拥抱变化。这个变化,是加法,是学习新的API,新的技能,新的思想。
  • 对于 React 新手:拥抱Hooks,降低了学习门槛,可以更快入门。但是类组件也非常有必要去了解,理解。知己知彼,重构不殆。

说了这么多,来,我们先来和这些 React Hooks 的 API 见个面:

基础 Hook

  • useState
  • useEffect

其他 Hook

  • useContext
  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue
  • … 还会增加

是不是有点多,其实useState useEffect 这两个已经能应付多数场景了。Let‘s go👇

useState hook

  • 功能:在函数组件中用来进行状态管理,创建一些本地 state。
  • API:const [currentState, setFunction] = useState(initialState);。传一个参数,返回一个数组(包含两个值)- 三要素。
  • initialState:参数,state 初始值。可以是任何类型: String,Object,Array,Bool,Number等。
  • currentState:返回值,state 当前最新值。可自主命名。
  • setFunction:返回值,state 更新函数。可自主命名。你可以在任意位置调用,来改变 state 的值。每次调用,会触发组件重新渲染(这也是返回值用 constlet 的原因)。
  • 特点:可使用多个useState,彼此独立。而类组件,只有一个 state,每次setState 要进行浅合并(内部实现)。

这个API很简单,请看下面示例(24行代码):

import React, { useState } from 'react';
function Form() {
  // ES6 解构
  const [name, setName] = useState('Mary');              // State 变量 1
  const [surname, setSurname] = useState('Poppins');     // State 变量 2
  const [width, setWidth] = useState(window.innerWidth); // State 变量 3

  function handleNameChange(e) {
    setName(e.target.value);
  }

  function handleSurnameChange(e) {
    setSurname(e.target.value);
  }

  return (
    <>
      <input value={name} onChange={handleNameChange} />
      <input value={surname} onChange={handleSurnameChange} />
      <p>Hello, {name} {surname}</p>
      <p>Window width: {width}</p>
    </>
  );
}
export default Form;

<></>React.Fragment 的简写语法。

用class类实现的话(31行代码),上述代码相当于:

import React from 'react';
class Form extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: 'Mary',
      surname:'Poppins',
      width: window.innerWidth
    };
  }
  handleNameChange = (e) => {
    this.setState({ name: e.target.value });
  }

  handleSurnameChange = (e) => {
    this.setState({ surname: e.target.value });
  }

  render() {
    const { name, surname, width } = this.state
    return (
      <>
        <input value={name} onChange={ this.handleNameChange } />
        <input value={surname} onChange={ this.handleSurnameChange } />
        <p>Hello, {name} {surname}</p>
        <p>Window width: {width}</p>
      </>
    );
  }
}
export default Form;

useState「粒度」问题

看到这里,对于写过class组件的我们,很容易产生一个疑问。 实际工作中,一个类组件的 this.state 中往往有十几项,用 Hooks 改写的话难道要写十几个 useState 么?

对于这个常见问题,官方文档有解答

根据官方文档,总结下来,有几点:

  • 建议将 state 分割为多个 useState。粒度更细,更易于管理,更好复用。
  • 可能一起改变的 state 可合并成一个useState( 比如Dom元素的 top left)。
  • state 逻辑趋于复杂,建议使用 reducerCustom Hook 管理(后面介绍)。

当组件的 state 很多的时候,为了提高代码的可读性,也可以把逻辑相关的一些 state 合并为一个 useState( 比如分页参数 )。但这些 state 并不是一起改变的,所以当其中一个 state 改变,调用对应的 setFunction 的时候。你需要做对象合并(不合并就丢了):

const [ pageData, setPageDate ] = useState({ pageSize: 20, current: 1, total:0, })

const onPageChange = current => {
  // 常规操作
  setPageDate( Object.assign( {}, pageData, { current } ) )
  // 官方建议
  setPageDate(currentPageData => ({ ...currentPageData, current}));
}

知识点:调用 useState 的更新函数时,可以传一个箭头函数,这个函数的参数是当前最新的 state, 返回值是要设置的 state 。

useEffect hook

API 可抽象为: useEffect(arrowFunction, [depsArr])

  • arrowFunction: 必须。执行函数,执行副作用操作。它决定了做什么。
  • depsArr: 非必须。一个依赖项数组。它决定了什么时候做(下面示例中介绍)。

根据实际情况,可细分为三种:

// 第一种
// 最基础的,只有箭头函数。没有依赖项,所以组件每次渲染都会执行。
// 相当于  componentDidMount + componentDidUpdate
useEffect(() => { 
  //side-effect 
})
// 第二种
// 有依赖项,是一个空数组,因为它永远不会变,所以只会首次执行。
// 相当于 componentDidMount
useEffect(() => { 
  //side-effect 
}, [])
// 第三种
// 有第二个参数,且非空数组。首次渲染会执行。重新渲染时,只有当依赖项的值改变了才会执行。
useEffect(() => { 
  //side-effect 
}, [...state])

相当是 == 而非 ===

总结下来:

  • 功能:管理 React 函数组件的副作用,赋予生命周期能力。
  • 怎么管:组件每次渲染到屏幕之后,根据依赖项的情况判断是否调用执行函数。
  • 二要素:执行函数,依赖项。
  • 清理机制:你可以在执行函数中返回另一个函数-清理函数,清理函数会在组件卸载的时候,会在组件重新渲染,且useEffect的依赖项值改变的时候调用。起到了 class 组件中componentWillUnmount的作用, 后续会在场景实例中介绍。
  • 使用上:和 useState 一样,可使用多个。建议一个副作用对应一个 useEffect

根据副作用是否需要清理,useEffect 可分为 不需要清理的 useEffect,和 需要清理的 useEffect。下面,我们分别通过一些示例来直观的感受一下。

不需要清理的场景

有时,我们希望在 React 渲染页面之后运行一些额外的代码。 网络请求、手动修改DOM 和日志记录都是不需要清理 副作用 的常见例子。可以这么说,我们运行它们,然后可以马上忘记它们。

就拿官网的例子来说,一个计数器组件,计数发生改变以后,更新 Dom 标题。类组件是这样实现的:


class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`; 
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

useEffect 实现如下:

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);
  // 第一种useEffect
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

规则:每一次渲染后都去运行所有的 effects 可能并不高效。(并且在某些场景下,它可能会导致无限循环。)– Dan Abramov

这条规则告诉我们,在写无依赖的 useEffect 的时候,多一点思考。上面代码现在看没有问题,后续增加了其他 state 和功能以后,这个 useEffect 就不高效了,可改写为:

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  },[count]);// 看这里
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

需要清理的场景

有一些场景,我们需要做副作用的清理,保证引起不必要内存泄漏。比如,手动绑定事件,订阅,定时器等。

以定时器为例,让我们来实现一个秒表组件。这是一个学习和理解 useEffect 非常有意思的例子。

这里,我们直接用 Hooks 来实现:

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

function App() {
  // 秒表开关
  const [isOn, setIsOn] = useState(false);
  // 计数
  const [timer, setTimer] = useState(0);

  useEffect(() => {
    let interval;
    //开关打开的时候才执行
    if (isOn) {
      // 通过定时器增加计数
      interval = setInterval(
        () => setTimer(timer + 1),
        1000,
      );
    }
    // 需要清除定时器
    // 不清理会如何?codesandbox中尝试,页面直接卡死
    return () => clearInterval(interval);
  }); 

  return (
    <>
      <p>{timer}</p>

      {!isOn && (
        <button type="button" onClick={() => setIsOn(true)}>
          Start
        </button>
      )}

      {isOn && (
        <button type="button" onClick={() => setIsOn(false)}>
          Stop
        </button>
      )}
    </>
  );
}

export default App;

运行代码,你会发现,秒表效果实现了。但是,同样的错误,故意犯了2次:既然用了定时器,为什么还要 effect 每次执行。让我们来分析下上面代码的执行流程:

  • 首次加载:effect执行。因为isOnfalse,所以 定时器 没有创建。
  • 点击 start 打开开关(setIsOn(true))。isOn 这个 state 改变,组件重新渲染。effect再次执行,此时创建定时器。
  • 定时器生效,1秒后执行 setTimer(timer + 1)timer 这个 state 改变,触发组件重新渲染(定时器也会清除)。effect再次执行,重新创建定时器。
  • 一直重复上面步骤。

有没有发现问题,定时器在循环创建,清除。用什么定时器,用延时器(setTimeout)好了。最糟糕的是,如果你忘了清除定时器,不光计数会错乱,页面也会奔溃。

怎么优化呢?同样的解决方案。很显然,我们的 effect 依赖 isOn 这个 state,所以我们可以把它作为 useEffect 的依赖项:

//...
useEffect(() => {
  let interval;
  if (isOn) {
    interval = setInterval(
      () => setTimer(timer + 1),
      1000,
    );
  }
  return () => clearInterval(interval);
  },[isOn]); // 看这里!!!!!!!!!
// ...

这样是不是就ok了?拷贝代码到 codeSandBox 验证一下。what?点击 start ,计数器增加到 1 以后不动了!。
页面卡死了么?我们再分析一下流程:

  • 首次加载:effect 执行。因为 isOnfalse,所以定时器没有创建。
  • 点击 start 打开开关。isOn 改变,组件重新渲染。effect 的依赖项 isOn 也改变了,effect 再次执行。此时,isOntrue,定时器创建。
  • 定时器生效,1秒后执行 setTimer(timer + 1)timer 改变,触发组件重新渲染。注意了,此时 effect 的依赖项isOn 并没有改变,所以定时器在重新渲染后不会清除,effect 也不会再次执行。看上去这就是我们想要的,定时器还在工作。那为什么一直是 1

规则:React 约定 Effect 拿到的总是定义它的那次渲染中的 propsstate。– Dan Abramov
我也注意到,上面的代码在 codeSandBox 中执行会看到一条告警信息:React Hook useEffect has a missing dependency: 'timer'. Either include it or remove the dependency array. You can also do a functional update 'setTimer(t => ...)' if you only need 'timer' in the 'setTimer' call. (react-hooks/exhaustive-deps) -- eslint

疑惑解开。这其实就是js常见的闭包,你也可以理解为这是 useEffect 的约定。这个非常非常重要,划重点。
上面的告警信息,已经明确的告诉了我们如何解决这个问题。

办法一: 增加依赖项 timer ,这样timer 改变也会触发重新渲染,然后 effect 都、会再次执行,定时器会拿到新的 timer

useEffect(() => {
  let interval;
  if (isOn) {
    interval = setInterval(
      () => setTimer(timer + 1),
      1000,
    );
  }
  return () => clearInterval(interval);
  },[isOn,timer]); // 看这里!!!!!!!!!

规则: 我鼓励你诚实地告知 effect 依赖作为一条硬性规则,并且要列出所有依赖。– Dan Abramov

办法二:采用 更新函数 来改变 state。前面提到过,useStatesetFunction中 ,可以传一个箭头函数(更新函数),这个函数的参数是当前最新的 state, 返回值是要设置的 state

useEffect(() => {
  let interval;
  if (isOn) {
    interval = setInterval(
      () => setTimer(val => val + 1),// 看这里!!!!
      1000,
    );
  }
  return () => clearInterval(interval);
  },[isOn]); // 看这里

发现没有,使用更新函数后,我们相当于去除了对 timer 的依赖。

规则: 当我们不想增加更多依赖,可以尝试修改 effect 使得依赖更少。– Dan Abramov

所以 方法二 优于 方法一。

我们来看一个实际项目中常见的副作用 - Ajax请求。在class 组件中,我们经常会用生命周期 componentDidMount来处理一些初始化的 Ajax 数据请求,现在我们用 useEffect 来实现。

比如用 axios 请求一个列表:

import React, { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
  const [data, setData] = useState([]);
  useEffect(() => {
    // 更优雅的方式
    const fetchData = async () => {
      const result = await axios(
        'https://hn.algolia.com/api/v1/search?query=redux',
      );
      setData(result.data);
    };
    fetchData();
  }, []);
  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}
export default App;

规则:useEffect 不能接收 async 作为执行函数。useEffect 接收的函数,要么返回一个能清除副作用的函数,要么就不返回任何内容。而 async 返回的是 promise。
useEffect 调用的函数如果依赖 state 或者 props。最好在执行函数中定义。这样依赖容易追踪。

useEffect 的使用,看起来很简单。但是要做到不滥用,正确使用也不是那么容易。主要在使用之前要多一些思考。

Custom Hooks

终于讲到它了, 前面已经提到过。它并不是 React hooks 的 API,而是自定义 hook。顾名思义,React允许你构建自己的 hooks。在学习完前面两个最受欢迎的 hooks 以后,你完全具备了实现自定义 hooks 的能力。

官网定义: 自定义 Hook 是一个 JavaScript 函数,其名称以 ”use” 开头,可以调用其他 Hook。

为什么需要Custom Hooks?

  • useState 解决了函数组件无状态的问题。
  • useEffect 实现了副作用管理,生命周期的功能。
  • Custom Hooks 将解决有状态(stateful)逻辑共享的问题(相当于类组件中Hoc的功能)。👇

我们来到一个实际场景。如今 HTML5 移动应用或 Web app 中越来越普遍的使用了离线浏览技术,所以用 JS 检测浏览器在线/离线状态非常常见。首先,我们用 React Hooks 来实现这个功能:

import React, { useState, useEffect } from 'react';
function App() {
  const [isOffline, setIsOffline] = useState(window.navigator.onLine);
  // 离线事件处理方法
  function onOffline() {
    setIsOffline(true);
  }
  // 在线事件处理方法
  function onOnline() {
    setIsOffline(false);
  }
  useEffect(() => {
    // 事件监听
    window.addEventListener('offline', onOffline);
    window.addEventListener('online', onOnline);
    // 清理函数
    return () => {
      window.removeEventListener('offline', onOffline);
      window.removeEventListener('online', onOnline);
    };
  }, []); // 只需要首次执行
  return (
    <>
       { 
         isOffline
         ? <div>网断已断开 ...</div>
         : <div>网络已连接 ...</div>
       }
    </>
  )
}
export default App;

无论浏览器是否在线,navigator.onLine 属性都会提供一个布尔值。 如果浏览器在线,则设置为 true ,否则设置为 false

OK,我们实现了一个很不错的功能。很明显,这个功能是可复用的,应该共享的。
我们把功能逻辑提取出来,把它封装成一个 Custom hook 就可以了:

import React, { useState, useEffect } from 'react';
// 自定义 hook
function useOffline() {
  const [isOffline, setIsOffline] = useState(window.navigator.onLine);
  function onOffline() {
    setIsOffline(true);
  }
  function onOnline() {
    setIsOffline(false);
  }
  useEffect(() => {
    window.addEventListener('offline', onOffline);
    window.addEventListener('online', onOnline);
    return () => {
      window.removeEventListener('offline', onOffline);
      window.removeEventListener('online', onOnline);
    };
  }, []);
  return isOffline; // 只暴露一个 state
}

// 函数组件
function App() {
  const isOffline = useOffline();
  return (
    <>
       { 
         isOffline
         ? <div>网断已断开 ...</div>
         : <div>网络已连接 ...</div>
       }
    </>
  )
}
export default App;

现在,你应该对 custom hooks 有了一个直观的认识:

  • 一个函数。
  • 一个use开头的函数。
  • 一个使用 React hooks 封装的,处理副作用的函数。
  • 一个在函数组件中引入简单,不需要调整组件结构的函数。

从重构层面来说,就是把组件中的一些 hooks 抽离到一个函数中,再使用这个函数。这个函数就是 React custom hooks

既然是函数,那肯定可以传参。我们来看一个常见场景:很多时候,为了用户体验,页面会本地存储用户数据,然后在页面返回的时候自动填充。现在,我们用一个传参的 custom hooks 来实现该场景:

import React, { useState, useEffect } from 'react';
// 自定义 hook,接收一个 localStorageKey 参数
const useStateWithLocalStorage = localStorageKey => {
  const [value, setValue] = useState(
    localStorage.getItem(localStorageKey) || '',
  );
  useEffect(() => {
    localStorage.setItem(localStorageKey, value);
  }, [value]);
  return [value, setValue];
};
const App = () => {
  // 使用带参数的 自定义 hooks
  const [value, setValue] = useStateWithLocalStorage(
    'myValueInLocalStorage',
  );
  const onChange = event => setValue(event.target.value);
  return (
    <div>
      <input value={value} type="text" onChange={onChange} />
      <p>{value}</p>
    </div>
  );
};

书写 custom hooks 需要注意些什么呢?看官网怎么说:

  • 自定义 Hooks 是一种惯例,它自然地遵循 Hooks 设计的约定。即遵循所有你用到的 Hooks 的规则。
  • 请使用 use 开头。这个习惯非常重要。如果没有它,我们就不能自动检查该 Hook 是否违反了 Hooks 的规则,因为我们无法判断某个函数是否包含对其内部 Hooks 的调用。

原理

顺序调用:每个组件都有一个 “内存单元” 的内部列表。它们只是 JavaScript 对象,你可以想象它是一个数组(实际上是一个单向链表),我们可以在其中放置一些数据。当调用 useState() 这样的 Hook 时,它读取当前单元格(或在第一次呈现时初始化它),然后将指针移动到下一个单元格。这就是多个 useState() 调用各自获取独立本地状态的方式。

  • Hooks 的状态值都被挂载在组件实例对象 FiberNode 的属性中。
  • Hooks 是用链表来保存状态的,属性保存的实际上是这个链表的头指针。
  • useState / useReducer 的信息保存在 FiberNode.memoizedState属性.
  • useEffect 也是以链表的形式挂载在 FiberNode.updateQueue 属性中。
// react-reconciler/src/ReactFiberHooks.js
export type Hook = {
  memoizedState: any, // 最新的状态值
  baseState: any, // 初始状态值,如`useState(0)`,则初始值为0
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null, // 临时保存对状态值的操作,更准确来说是一个链表数据结构中的一个指针
  next: Hook | null,  // 指向下一个链表节点
};

 const effect: Effect = {
    tag, // 用来标识依赖项有没有变动
    create, // 用户使用useEffect传入的函数体
    destroy, // 上述函数体执行后生成的用来清除副作用的函数
    deps, // 依赖项列表
    next: (null: any),
};

想更详细的理解,请点击:
React Hooks 揭秘
React Hooks 原理剖析

缺点

需要开发者遵从许多规则。理解并合理运用这些规则,能写出优雅的,可读性高的,性能好的代码。反之,很容易出现死循环,数据重复请求等问题。最让人担心的是性能,很多时候业务功能实现了,但是其实存在很多不必要的开销。


参考资料
官网-Hooks
What Are React Hooks
React Hooks 详解 + 项目实战