React Hook实战(一)

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。javascript

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属性初始化为传递的参数initialValue import { 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经常使用于处理复杂的状态逻辑。
useMemo Memoization是一种优化技术,它缓存函数调用的结果,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,但用于回调函数。
useLayoutEffect useLayoutEffect与useffect相同,但它只在全部的文档对象模型(Document Object Model,DOM)改变以后才触发。 import { useLayoutEffect } from 'react'; useLayoutEffect(didUpdate) useLayoutEffect可用于从DOM读取信息。(最好使用useffect,useLayoutEffect将阻止试图更新并减慢应用程序的渲染速度)
useDebugValue useDebugValue可用于在建立自定义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 组件
constructor useState
getDerivedStateFromProps useState 里面 update 函数
shouldComponentUpdate useMemo
render 函数自己
componentDidMount useEffect
componentDidUpdate useEffect
componentWillUnmount useEffect 里面返回的函数
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 组件的主要方式。

参考文献

一、官方文档css

二、useEffect 完整指南html

三、React 高阶组件java

四、简书 React Hooksreact

五、探索 React 的内在 — Fiber & Algebraic Effectsios

六、使用React.memo优化React 应用数据库