React Hook实战(一)

527 阅读19分钟

React Hook实战(一)

目录:

  • 引-为什么用Hook
  • 基本使用
  • 自定义实现Hook
  • Hook-react的真正实现
  • Class 和 Hook对比
  • 总结-问题思考

引-为什么用Hook

在过去,我们必须使用生命周期方法(如componentDidUpdate)的特殊函数的类组件和特殊状态处理的方法以便处理状态更改。React class中,尤其是this.context的JavaScript对象,对于人和机器来说都很难阅读和理解,因为它总是引用不同的东西,所以有时(例如,在事件处理程序中)我们需要手动将它重新绑定到类对象。计算机不知道类中的哪些方法将被调用,以及如何修改这些方法,这使得性能优化和代码优化变得困难。此外,clsss有时需要我们一次在多个地方编写代码。 例如,如果我们希望在组件初始化或数据更新时获取数据,举个例子:

首先,我们通过扩展React.component类来定义我们的类组件:

class Example extends React.Component {

然后,我们定义componentDidMount生命周期方法,在该方法中,我们从一个API中提取数据

  componentDidMount () {
    fetch(`http://my.api/${this.props.name}`)
    .then(...)
  }

我们还需要定义componentDidUpdate生命周期方法,当prop发生变化时判断是否更新状态。

  componentDidUpdate (prevProps) {
    if (this.props.name !== prevProps.name) { 
      fetch(`http://my.api/${this.props.name}`)
      .then(...)
    }
  }
}

为了减少代码的重复性,我们可以定义一个名为fetchData的单独方法来获取数据,如下所示:

  fetchData () {
    fetch(`http://my.api/${this.props.name}`)
    .then(...)
  }

最后,我们调用componentDidMount和ComponentDidUpdate中的方法

  componentDidMount () {
    this.fetchData()
  }
  componentDidUpdate (prevProps) {
    if (this.props.name !== prevProps.name) { this.fetchData()
  }
}

然而,即使这样,我们仍然需要在两个地方调用fetchData。每当我们更新传递给方法的参数时,我们需要在两个地方更新它们,这使得这个模式很容易出现bug和将来的bug。

在Hook之前,如果我们想封装状态管理逻辑,我们必须使用高阶组件和呈现道具。例如,我们创建一个React组件,该组件使用上下文处理用户身份验证,如下所示:

我们首先导入authenticateUser函数,以便用上下文包装组件,然后导入AuthenticationContext组件,以便访问上下文:

import authenticateUser, { AuthenticationContext } from './auth'

然后,我们定义app组件,在其中我们使用AuthenticationContext.Consumer组件

const App = () => (
  <AuthenticationContext.Consumer>
    {user =>

现在,我们根据用户是否登录显示不同的文本

      user ? `${user} logged in` : 'not logged in'

最后我们补充一下上下文

      }

    </AuthenticationContext.Consumer>
  )

export default authenticateUser(App)

在前面的示例中,我们使用高阶authenticateUser组件向现有组件添加身份验证逻辑。然后我们用一个authenticationcontext.Consumer将user对象注入到组件中。可以想象,使用许多上下文将导致一个包含许多子zu'jian的大型组件。例如,当我们想要使用三个上下文时,wrapper hell如下所示:

<AuthenticationContext.Consumer>
  {user => (
    <LanguageContext.Consumer>
      {language => (
        <StatusContext.Consumer>
        {status => (
          ...
        )}
      </StatusContext.Consumer>
      )}
    </LanguageContext.Consumer>
  )}
</AuthenticationContext.Consumer>

这不是很容易阅读和修改,而且如果我们以后需要更改某些内容,它也容易出错。此外,如果我们查看一个大型组件树,其中许多组件只是充当wrapper,这种传统方式使调试变得困难。

React Hook基于React基本原理,Hook试图通过使用现有的JavaScript特性来封装状态管理。因此,我们不再需要学习和理解专门的React特性;我们可以简单地利用现有的JavaScript知识来使用Hook。

我们可以使用Hook解决前面提到的所有问题。我们不再需要使用类组件,因为Hook只是可以在函数组件中调用的函数。我们也不再需要为上下文使用高阶组件和渲染props,因为我们可以简单地使用Hook上下文来获取所需的数据。此外,Hook允许我们在组件之间重用有状态逻辑,而无需创建高阶组件。

例如,前面提到的生命周期方法的问题可以使用Hook来解决:

function Example ({ name }) {
  useEffect(() => {
    fetch(`http://my.api/${this.props.name}`)
    .then(...)
}, [ name ])
// ...
}

这里实现的效果为Hook将在组件挂载时以及prop更改时自动触发。此外,前面提到的wrapper hell也可以使用Hook解决,如下所示

const user = useContext(AuthenticationContext)
const language = useContext(LanguageContext)
const status = useContext(StatusContext)

现在我们知道了Hook可以解决哪些问题,让我们开始使用吧。

Hook的基本使用:

React中组件可以大体分为类组件和函数组件,在React中如果需要更改一个组件状态的时候,那么这个组件必须是类组件,那么能否让函数组件拥有类组件的功能?这时候我们就需要使用Hook让我们函数组件拥有了类似组件的特性。Hook是React16.8中新增得功能,他们允许我们在不编写类的情况下使用状态和其他React功能。Hook又提供了一种写组件的方法,使编写一个组件更简单更方便,同时可以自定义hook把公共的逻辑提取出来,让逻辑在多个组件之间共享。

我们从一个请求数据的代码示例demo开始切入:

import React, { useState } from 'react';
import "./Welcome.scss";

function Welcome() {
  const [data, setData] = useState({ hits: [{
    objectID:"001",
    url:"https://www.jd.com/",
    title:"JD"
  }] });

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}
export default Welcome;

该组件是一个项目列表,初始化的data和状态更新函数来自useState这个Hook,通过调用useState,来创建App组件的内部状态。初始状态是一个object,其中的hits为一个空数组。如果我们要添加调用后端数据,我们可以使用axios来发起请求,同样也可以使用fetch。

function Welcome() {
  const [data, setData] = useState({ hits: [{
    objectID:"001",
    url:"https://www.jd.com/",
    title:"JD"
  }] });

  useEffect(async () => {
    const result = await axios(
      'http://localhost/api/v1/search?query=redux'
    );
    setData(result.data);
  });

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

在useEffect中,我们请求了后端的数据,还通过调用setData来更新了本地的状态,这样会触发界面的更新。但是,运行这个程序的时候,会出现无限循环的情况。假设我们只希望在组件mount时请求数据,那么我们可以传递一个空数组作为useEffect的第二个参数,这样就能避免在组件更新时执行useEffect,只会在组件mount时执行。useEffect的第二个参数可用于定义其依赖的所有变量,如果其中一个变量发生变化,则useEffect会再次运行,如果包含变量的数组为空,则在更新组件时useEffect不会再执行,因为它不会监听任何变量的变更。

function Welcome() {
  const [data, setData] = useState({ hits: [{
    objectID:"001",
    url:"https://www.jd.com/",
    title:"JD"
  }] });

  useEffect(async () => {
    const result = await axios(
      'http://localhost/api/v1/search?query=redux'
    );
    setData(result.data);
  },[]);

  return (
    <ul>
      {data.hits.map(item => (
        <li key={item.objectID}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}
    </ul>
  );
}

demo2

在代码中,我们使用async / await从第三方API获取数据,由于每个async函数都会默认返回一个隐式的promise。但是,useEffect不希望返回任何内容,这就是为什么不能直接在useEffect中使用async函数,因此,我们可以不直接调用async函数,而是像下面这样:

  useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'http://localhost/api/v1/search?query=redux',
      );

      setData(result.data);
    };

    fetchData();
  }, []);

在useEffect中,我们可以把请求数据前将loading置为true,在请求完成后,将loading置为false.

loading处理完成后,还需要处理错误,这里的逻辑是一样的,使用useState来创建一个新的state,然后在useEffect中特定的位置来更新这个state。由于我们使用了async/await,可以使用一个try-catch, 每次useEffect执行时,将会重置error;在出现错误的时候,将error置为true;在正常请求完成后,将error置为false。

function Welcome() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://localhost/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);
      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }
      setIsLoading(false);
    };

    fetchData();
  }, [url]);

}

Hook是可以在函数组件中调用的函数。我们也不再需要为上下文使用高阶组件和传统的class的方式,因为我们可以简单地使用Hook上下文来获取所需的数据。此外,hook允许我们在组件之间重用有状态的逻辑,而无需创建高阶组件。我们来简单看一下Hook提供的其他方法:

方法名用法示例思考
useRef该方法返回一个可变的ref对象,其中.current属性初始化为传递的参数initialValueimport { useRef } from 'react'; const refContainer = useRef(initialValue)useRef用于处理对React中的元素和组件的引用。我们可以通过将ref属性传递给元素或组件来设置引用。
useReducer这个是useState的替代方案,其工作方式与Redux库类似import { useReducer } from 'react';
const [ state, dispatch ] = useReducer(reducer, initialArg, init)
useReducer常用于处理复杂的状态逻辑。
useMemoMemoization是一种优化技术,它缓存函数调用的结果,useMemo允许我们计算一个值并将其记录下来import { useMemo } from 'react';
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
当我们希望避免重新执行费时的操作时,useMemo对于性能优化非常有用。
useCallback这个方法允许我们传递一个内联回调函数和一组依赖项,并将返回回调函数的记忆版本。import { useCallback } from 'react'; const memoizedCallback = useCallback(() => {doSomething(a, b) }, [a, b])当将回调函数传递给子组件时,useCallback非常有用。它的工作方式类似于useMemo,但用于回调函数。
useLayoutEffectuseLayoutEffect与useffect相同,但它只在所有的文档对象模型(Document Object Model,DOM)改变之后才触发。import { useLayoutEffect } from 'react'; useLayoutEffect(didUpdate)useLayoutEffect可用于从DOM读取信息。(最好使用useffect,useLayoutEffect将阻止试图更新并减慢应用程序的渲染速度)
useDebugValueuseDebugValue可用于在创建自定义Hook时在React DevTools中显示标签。import { useDebugValue } from 'react'; useDebugValue(value)在自定义Hook中可以使用useDebugValue来显示Hook的当前状态,这样可以更容易地调试组件。

除了React官方提供的所有语法糖之外,社区已经发布了很多库。这些库还提供了一些方法,我们可以看一下其中非常受欢迎的几个:

useInput

useInput用于轻松实现输入处理,并将输入字段的状态与变量同步。它可以如下使用:

import { useInput } from 'react-hookedup'
function App () {
  const { value, onChange } = useInput('')
  return <input value={value} onChange={onChange} />
}

如我们所见,useInput极大地简化了React中输入字段的处理。

useResource

useResource可用于通过应用程序中的请求实现异步数据加载。我们可以使用它如下

import { useResource } from 'react-request-hook'
  const [profile, getProfile] = useResource(id => ({ url: `/user/${id}`,
  method: 'GET'
})

如我们所见,使用useResource来处理获取数据功能是非常简单的。

Navigation Hooks

Navigation是Navi库的一部分,用于通过React中的Hook实现路由功能。Navi库提供了更多与路由相关的Hook。我们可以使用它们如下

import { useCurrentRoute, useNavigation } from 'react-navi'
const { views, url, data, status } = useCurrentRoute()
const { navigate } = useNavigation()

Navigation Hooks使得路由更容易处理。

Life cycle Hooks

react hookedup库提供各种Hooks,包括react的所有生命周期侦听器。(请注意,在使用Hook进行开发时,不建议考虑组件的生命周期。这些钩子只是提供了一种将现有组件重构为Hook的方法。)在这里,我们列出了其中的两个,如下所示

import { useOnMount, useOnUnmount } from 'react-hookedup'
useOnMount(() => { ... })
useOnUnmount(() => { ... })

react hookedup可以直接替换类组件中的生命周期方法。

Timer Hooks

react hookedup库还为setInterval和setTimeout提供了方法。这些工作方式类似于直接调用setTimeout或setInterval。但作为一个React Hook,它将在重新渲染的实惠保持执行,如果我们在函数组件中直接定义计时器而不使用Hook,那么每次组件重新渲染时,我们都将重置计时器。我们可以将时间以毫秒为单位作为第二个参数传递。我们可以如下使用:

import { useInterval, useTimeout } from 'react-hookedup'
useInterval(() => { ... }, 1000)
useTimeout(() => { ... }, 1000)

实现一个Hook

在实现一个Hook前我们先来深入了解State Hook吧,我们先从State Hook如何在内部工作开始,我们将自己重新实现它。接下来,我们将了解钩子的一些局限性,以及它们存在的原因。然后,我们将了解可能的替代Hook api及其相关问题。最后,我们将学习如何解决由于Hook的限制而导致的常见问题。最后,我们将探讨一下如何使用Hook来实现React中的有状态函数组件。

我们将需要ReactDOM,以便在useState Hook的重新实现中渲染组件。如果我们使用实际的React Hook,这将在内部处理。

import React from 'react'
import ReactDOM from 'react-dom'

现在,我们定义自己的useState函数。useState函数将initialState作为参数:

function useState (initialState) {

然后,我们定义一个值,在其中存储我们的状态。首先,该值将设置为initialState,该值作为参数传递给函数:

  let value = initialState

接下来,我们定义setState函数,在该函数中,我们将把值设置为不同的值,并渲染我们的MyName组件

  function setState (nextValue) {
    value = nextValue
    ReactDOM.render(<MyName />,
    document.getElementById('root'))
  }

最后,我们将value和setState函数作为数组返回:

  return [ value, setState ]
}

我们使用数组而不是对象的原因是,我们通常希望重命名value和setState变量。使用数组可以方便地通过解构重命名变量。

const [ name, setName ] = useState('')

我们的Hook函数使用闭包来存储当前值。闭包是变量存在和存储的环境。在我们的例子中,函数提供闭包,value变量存储在闭包中。setState函数也在同一个闭包中定义,这就是为什么我们可以访问该函数中的value变量。在useState函数之外,除非从函数返回value变量,否则无法直接访问该value变量。那么我们实现的简单Hook有什么问题呢?

如果现在运行我们的Hook demo,我们会注意到当我们的组件重新渲染时,状态被重置。这是由于在每次呈现组件时都重新初始化value变量,这是因为每次渲染组件时都调用useState方法。接下来,我们将使用一个全局变量来解决这个问题,然后将value放到一个数组,然后我们定义多个Hook。正如我们所了解到的,该value存储在useState函数定义的闭包中。每次组件重新提交时,闭包都会重新初始化,这意味着我们的value将被重置。要解决这个问题,我们需要将值存储在函数外部的全局变量中。这样,值变量将位于函数外部的闭包中,这意味着当再次调用函数时,闭包将不会重新初始化。我们可以定义全局变量如下:

首先,我们在useState函数定义上方添加以下一行

let value
function useState (initialState) {

然后,用以下代码替换函数中的第一行

  if (typeof value === 'undefined') value = initialState

现在,我们的useState函数使用全局值变量,而不是在它的闭包中定义值变量,因此当函数再次被调用时,它不会被重新初始化。

我们的Hook功能是可以使用的,但是,如果我们想添加另一个hook,我们会遇到另一个问题:所有Hook都写入同一个全局值变量,让我们通过在组件中添加第二个Hook来仔细研究这个问题。

假设我们要添加lastName状态,如下所示:

我们首先在当前Hook之后创建一个新的Hook,

const [ name, setName ] = useState('')
const [ lastName, setLastName ] = useState('')

然后,我们定义另一个handleChange函数

function handleLastNameChange (evt) {
  setLastName(evt.target.value)
}

接下来,我们将lastName变量放在名字后面:

<h1>My name is: {name} {lastName}</h1>

最后,我们添加另一个input输入框:

<input type="text" value={lastName} onChange= 
{handleLastNameChange}
/>

当我们这样写时,我们会注意到我们重新实现的Hook函数对两个状态使用相同的值,因此我们总是同时更改两个字段。为了实现多个Hook,而不是只有一个全局变量,我们应该有一个存放Hook的数组。我们现在要将value变量重构为value数组,以便可以定义多个Hook。

我们删除以下代码行

let value

替换为以下代码段

let values = []
let currentHook = 0

然后,编辑useState函数的第一行,我们现在在其中初始化values数组的currentHook索引处的值:

if (typeof values[currentHook] === 'undefined')
values[currentHook] = initialState

我们还需要更新setter函数,以便更新相应的状态值。在这里,我们需要将currentHook值存储在一个单独的hookIndex变量中,因为currentHook值稍后会更改。这可以确保在useState函数的闭包中创建currentHook变量的副本。否则,useState函数将从外部闭包访问currentHook变量,该闭包在每次调用useState时都会被修改。

let hookIndex = currentHook
function setState (nextValue) {
  values[hookIndex] = nextValue
  ReactDOM.render(<MyName />,
  document.getElementById('root'))
}

编辑useState函数的最后一行,如下所示

return [ values[currentHook++], setState ]

使用values[currentHook++],我们将currentHook的当前值作为索引传递给values数组,然后将currentHook增加1。这意味着从函数返回后currentHook将增加。在开始渲染组件时,仍需要重置currentHook计数器。在组件定义之后添加以下:

function Name () {
  currentHook = 0

最后,我们简单地重新实现useState Hook。如我们所见,使用全局数组存储Hook state解决了我们在定义多个Hook时遇到的问题。我们如果想添加一个复选框来切换first name字段的使用呢?

首先,我们添加一个新的Hook来存储复选框的状态:

const [ enableFirstName, setEnableFirstName ] = useState(false)

然后,我们定义一个处理函数

function handleEnableChange (evt) {
  setEnableFirstName(!enableFirstName)
}

接下来,我们渲染一个复选框

<input type="checkbox" value={enableFirstName} onChange= {handleEnableChange} />

添加对enableFirstName变量的检查

<h1>My name is: {enableFirstName ? name : ''} {lastName}
</h1>

我们是否可以将Hook定义放入if条件或三元表达式中,就像我们在下面的代码片段中一样?

const [ name, setName ] = enableFirstName ? useState('')
:  [ '', () => {} ]

最新版本的react-scripts在定义条件Hooks时实际上会抛出一个错误,因此我们需要通过运行以下命令来降级本例中的库:

>  npm install --save react-scripts@^2.1.8

在这里,如果名字被禁用,我们会返回初始状态和一个空的setter函数,这样编辑输入字段就不起作用。我们会注意到编辑last name仍然有效,但是编辑first name 不起作用,在下面的截图中我们可以看到,现在只能编辑 last name。

react

当我们单击复选框时程序会执行以下操作:

  1. 复选框已选中
  2. 启用name输入字段
  3. last name字段的值现在是first name字段的值

我们可以在以下屏幕截图中看到单击复选框的结果:

react

我们可以看到 last name状态现在在first name字段中。这些值已经交换,因为Hook的顺序很重要。正如我们从实现中了解到的,我们使用currentHook索引来知道每个Hook的状态存储在哪里。但是,当我们在两个现有的Hook之间插入一个附加Hook时,顺序就会混乱。

在选中复选框之前,values数组如下所示:

  • [false, '']
  • Hook: enableFirstName, lastName

然后,我们在lastName字段中输入了一些文本:

  • [false, 'Hook']
  • Hook: enableFirstName, lastName

接下来,我们切换了复选框,它激活了我们的新Hook

  • [true, 'Hook', '']
  • Hook order: enableFirstName, name, lastName

如我们所见,在两个现有Hook之间插入一个新Hook会使name hook获取下一个Hook(lastName)的状态,因为它现在具有与lastName钩子以前相同的索引。现在,lastName Hook没有值,这导致它设置初始值为空字符串。因此,切换复选框会将lastName字段的值放入name字段。

Hook-react的真正实现

我们简单的Hook实现已经让我们了解了Hook是如何在内部工作的。然而,hook不使用全局变量。相反,它们将状态存储在React component中。它们也在内部处理Hook计数器,因此我们不需要手动重置函数组件中的计数。此外,当状态发生变化时,真正的Hook会自动触发component的重新渲染。然而,要做到这一点,需要从React函数组件调用Hook。不能在React外部或React class组件内部调用React Hook。我们应该始终在函数组件的开头定义Hook,并且永远不要将它们嵌套在if或其他构造函数中。我们应该在React函数内部调用React hook组件,React hook不能有条件地定义,也不能在循环中定义。

那么,我们如何实现条件的Hook呢?我们可以定义Hook并在需要时使用它,而不是使Hook成为条件的Hook。我们可以重新分组我们的组件。解决有条件的hook的另一种方法是将一个组件拆分为多个组件,然后有条件地渲染这些组件。例如,假设我们希望在用户登录后从数据库中获取用户信息。

我们不能执行以下操作,因为使用if条件可以更改Hook的顺序

function UserInfo ({ username }) {
  if (username) {
    const info = useFetchUserInfo(username)
    return <div>{info}</div>
  }
  return <div>Not logged in</div>
}

我们必须为用户登录时创建一个单独的组件,如下所示:

function LoggedInUserInfo ({ username }) { const info = useFetchUserInfo(username) 
  return <div>{info}</div>
}

function UserInfo ({ username }) {
  if (username) {
    return <LoggedInUserInfo username={username} />
  }
  return <div>Not logged in</div>
}

对非登录和登录状态使用两个独立的组件是有意义的,因为我们希望每个组件都有一个单一的功能的。至于循环中的Hook,我们可以使用包含数组的单个状态Hook,也可以拆分组件。例如,假设我们想显示所有在线用户。

我们可以使用数组包含所有用户数据,如下所示:

function OnlineUsers ({ users }) {
  const [ userInfos, setUserInfos ] = useState([])
  //	... fetch & keep userInfos up to date ...
  return ( <div>
  {users.map(username => {
    const user = userInfos.find(u => u.username === username)
    return <UserInfo {...user} />
  })}
  </div>
  )
}

然而,这可能是有问题的。例如,我们可能不想通过OnlineUsers组件更新所有用户状态,因为我们必须从数组中选择需要修改的用户的状态,然后修改数组。更好的解决方案是在UserInfo组件中使用Hook。这样,我们就可以使每个用户的状态保持最新,而不必处理数组逻辑:

function OnlineUsers ({ users }) {
  return (
    <div>
    {users.map(username => <UserInfo username={username} />)} </div>
  )
}
function UserInfo ({ username }) {
  const info = useFetchUserInfo(username)
  //	... keep user info up to date ...

如我们所见,为每个功能模块使用一个单独的组件可以保持代码的简单和简洁,同时也避免了React Hook的限制。以上我们首先重新实现useState函数,使用全局状态和闭包。然后我们了解到,为了实现多个Hook,我们需要使用状态数组来代替。然而,通过使用状态数组,我们必须在函数调用之间保持hook顺序的一致性。这个限制使得我们不能使用条件中的Hook和循环中的Hook。然后,我们了解了Hook的可能替代方案。那么真正的react是怎么实现Hook的呢,我们来看一段react官方的源码:

react react

在 React 中,实现方式却有一些差异的。React 中是通过类似单链表的形式来代替数组的。(如下图所示)我们知道,react 会生成一棵组件树(或Fiber 单链表)[Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。],树中每个节点对应了一个组件。hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。memoizedState 数组是按hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。我们只能在函数最外层调用 Hook自定义的共享同一个memoizedState,共享同一个顺序。每一次重新渲染的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

react

type Hooks = {
  memoizedState: any, // 指向当前渲染节点 Fiber
  baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
  baseUpdate: Update<any> | null,// 当前需要更新的 Update ,每次更新完之后,会赋值上	一个 
  update,方便 react 在渲染错误的边缘,数据回溯
  queue: UpdateQueue<any> | null,// UpdateQueue 通过
  next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
}
type Effect = {
  tag: HookEffectTag, // effectTag 标记当前 hook 作用在 life-cycles 的哪一个阶段
  create: () => mixed, // 初始化 callback
  destroy: (() => mixed) | null, // 卸载 callback
  deps: Array<mixed> | null,
  next: Effect, // 同上
};

Hook函数组件在第一次渲染时和再次渲染时的实现是不同的,组件所调用的 Hook 实际上指向的是不同的 Hook。函数组件在第一次渲染时所使用的 Hook 指向的是对应的 mountXXX,而在更新时,Hook 指向的是对应的 updateXXX,如下图所示:

react

Class 和 Hook对比

从生命周期上看

react

我们来对比汇总一个表格

class 组件Hooks 组件
constructoruseState
getDerivedStateFromPropsuseState 里面 update 函数
shouldComponentUpdateuseMemo
render函数本身
componentDidMountuseEffect
componentDidUpdateuseEffect
componentWillUnmountuseEffect 里面返回的函数
componentDidCatch
getDerivedStateFromError

从编码上看

class 组件Hooks 组件
代码逻辑清晰(构造函数、componentDidMount等)需要配合注释和变量名
不容易内存泄漏容易发生内存泄漏

总结-问题思考:

  • React 是如何把对 Hook 的调用和组件联系起来的。

Hook 本质就是 JavaScript 函数,不要在循环,条件或嵌套函数中调用 Hook, 确保总是在你的 React 函数的最顶层调用他们。

  • React 怎么知道哪个 state 对应哪个 useState?

React 靠的是 Hook 调用的顺序。只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。

  • Hook 会因为在渲染时创建函数而变慢吗

不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。除此之外,可以认为 Hook 的设计在某些方面更加高效:Hook 避免了 class 需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。符合语言习惯的代码在使用 Hook 时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。

  • 使用useMemo ?

demo7

这行代码会调用 computeExpensiveValue(a, b)。但如果依赖数组 [a, b] 自上次赋值以来没有改变过,useMemo 会跳过二次调用,只是简单复用它上一次返回的值。可以把 useMemo 作为一种性能优化的手段,但不要把它当做一种语义上的保证。未来,React 可能会选择「忘掉」一些之前记住的值并在下一次渲染时重新计算它们,比如为离屏组件释放内存。建议自己编写相关代码以便没有 useMemo 也能正常工作 —— 然后把它加入性能优化。

  • 如何实现 shouldComponentUpdate

demo7

可以用 React.memo 包裹一个组件来对它的 props 进行浅比较。这不是一个 Hook 因为它的写法和 Hook 不同。React.memo 等效于 PureComponent,但它只比较 props。

  • effect 的依赖频繁变化该怎么处理?

demo7

传入空的依赖数组 [],意味着该 hook 只在组件挂载时运行一次,并非重新渲染时。但如此会有问题,在 setInterval 的回调中,count 的值不会发生变化。因为当 effect 执行时,我们会创建一个闭包,并将 count 的值被保存在该闭包当中,且初值为 0。每隔一秒,回调就会执行 setCount(0 + 1),因此count 永远不会超过 1。

demo7

指定 [count] 作为依赖列表就能修复这个 Bug,但会导致每次改变发生时定时器都被重置。事实上,每个 setInterval 在被清除前(类似于 setTimeout)都会调用一次。但这并不是我们想要的。要解决这个问题,我们可以使用 setState 的函数式更新形式。它允许我们指定state该如何改变而不用引用当前state。

  • 和DOM的交互

demo7

获取 DOM 节点的位置或是大小的基本方式是使用 callback ref。每当 ref 被附加到一个另一个节点,React 就会调用 callback。在这个案例中,我们没有选择使用 useRef,因为当 ref 是一个对象时它并不会把当前 ref 的值的变化通知到我们。使用 callback ref 可以确保即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。

  • 如何获取上一轮的 props 或 state

demo7

可以通过 ref 来手动实现,考虑到这是一个相对常见的使用场景,很可能在未来 React 会自带一个 usePrevious Hook。

  • Hook 能否覆盖 class 的所有使用场景

官方给 Hook 设定的目标是尽早覆盖 class 的所有使用场景。目前暂时还没有对应不常用的 getSnapshotBeforeUpdate、getDerivedStateFromError 和 componentDidCatch 生命周期的 Hook 等价写法,但官方计划尽早把它们加进来。目前 Hook 还处于早期阶段,一些第三方的库可能还暂时无法兼容 Hook。

  • Hook,class,两者混用?

我们不能在 class 组件内部使用 Hook,但我们可以在组件树里混合使用 class 组件和使用了 Hook 的函数组件。不论一个组件是 class 还是一个使用了 Hook 的函数,都只是这个组件的实现细节而已。长远来看,官方期望 Hook 能够成为我们编写 React 组件的主要方式。

参考文献

1、官方文档

2、useEffect 完整指南

3、React 高阶组件

4、简书 React Hooks

5、探索 React 的内在 — Fiber & Algebraic Effects

6、使用React.memo优化React 应用