React进阶:HOOK

Posted No Silver Bullet

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React进阶:HOOK相关的知识,希望对你有一定的参考价值。


一、前言

HookReact 16.8.0 的新增特性,React Native 0.59及以上版本支持 Hook。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

请注意,要启用 Hook,所有 React 相关的 package 都必须升级到 16.8.0 或更高版本。如果你忘记更新诸如 React DOM 之类的 packageHook 将无法运行。

Hook 是一些可以让你在函数组件里“钩入React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React

HOOK可以让我们在函数组件中使用 state 、生命周期以及其他 react 特性,而不仅限于 class 组件中使用。react hooks 的出现,标示着 react 中不会在存在无状态组件,而是包含类组件函数组件react hooks 即是应用在函数组件中。

如果你在编写函数组件并意识到需要向其添加一些 state,以前的做法是必须将其它转化为 class。现在你可以在现有的函数组件中使用 Hook

二、HOOK函数介绍

2.1 State hook

hooks 使我们在函数组件中拥有使用state的能力, 就是通过 useState 来实现的,首先来看一个简单的例子,这个例子用来显示一个计数器。当你点击按钮,计数器的值就会增加:

  function App () {
 	// 声明一个叫 “count” 的 state 变量。
    const [ count, setCount ] = useState(0)
    return (
      <div>
        点击次数: { count } 
        <button onClick={() => { setCount(count + 1)}}>点我</button>
      </div>
      )
  }

其中,useState 就是一个 Hook。从代码中可以看到,useState 的使用非常简单,我们从 React 中拿到 useState 后,只需要在使用的地方直接调用 useState 函数就可以。 通过在函数组件里调用它来给组件添加一些内部 stateReact 会在重复渲染时保留这个 stateuseState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state旧的 state 进行合并。

那么这里为什么叫 countsetCount?可以使用别的名字吗,这里使用了 es6解构赋值,所以你可以给它起任何名字,updateCount, doCountanything,当然,为了编码规范,所以建议统一使用一种命名规范,尤其是第二个值。

useState 唯一的参数就是初始 state。在上面的例子中,计数器是从零开始的,所以初始 state 就是 0。值得注意的是,不同于 this.state,这里的 state 不一定要是一个对象 —— 如果你有需要,它也可以是。这个初始 state 参数只有在第一次渲染时会被用到。

当我们在使用 useState 时,修改值时传入同样的值,我们的组件会重新渲染吗,例如这样

  function App () {
    const [ count, setCount ] = useState(0)
    console.log('component render count')
    return (
      <div>
        点击次数: { count } 
        <button onClick={() => { setCount(count)}}>点我</button>
      </div>
      )
  }

结果是不会,提升了组件的渲染性能。

useState 默认值
useState 支持我们在调用的时候直接传入一个值,来指定 state 的默认值,比如这样 useState(0), useState({ a: 1 }), useState([ 1, 2 ]),还支持我们传入一个函数,来通过逻辑计算出默认值,比如这样:

function App (props) {
   const [ count, setCount ] = useState(() => {
     return props.count || 0
   })
   return (
     <div>
       点击次数: { count } 
       <button onClick={() => { setCount(count + 1)}}>点我</button>
     </div>
     )
}

这个时候,就有小伙伴问了,那我组件每渲染一次,useState 中的函数就会执行一边吗,浪费性能,其实不会,useState 中的函数只会执行一次,我们可以做个测试:

function App (props) {
    const [ count, setCount ] = useState(() => {
      console.log('useState default value function is call')
      return props.count || 0
    })
    return (
      <div>
        点击次数: { count } 
        <button onClick={() => { setCount(count + 1)}}>点我</button>
      </div>
      )
  }

结果如下:
在这里插入图片描述
声明多个 state 变量

当我们使用多个 useState 的时候,React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用顺序。Hook 的调用顺序在每次渲染中都是相同的,所以它能够正常工作:

function App (props) {
  let count, setCount
  let sum, setSum
  if (count > 2) {
    [ count, setCount ] = useState(0)
    [ sum, setSum ] = useState(10)
  } else {
    [ sum, setSum ] = useState(10)
    [ count, setCount ] = useState(0)
  }
  return (
    <div>
      点击次数: { count } 
      总计:{ sum }
      <button onClick={() => { setCount(count + 1); setSum(sum - 1)}}>点我</button>
    </div>
    )
}

当我们在运行时改变 useState 的顺序,数据会混乱,增加 useState, 程序会报错。

2.2 Effect hook

Effect Hook 可以让你在函数组件中执行副作用操作,什么是副作用呢,就是除了状态相关的逻辑,比如网络请求监听事件查找 dom等动作均视为副作用。

React 组件中有两种常见副作用操作:需要清除的不需要清除的。我们来更仔细地看一下他们之间的区别。

无需清除的 effect

有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。

使用 class 的示例
Reactclass 组件中,render 函数是不应该有任何副作用的。一般来说,在这里执行操作太早了,我们基本上都希望在 React 更新 DOM 之后才执行我们的操作。

这就是为什么在 React class 中,我们把副作用操作放到 componentDidMountcomponentDidUpdate 函数中。以 React 实现计数器的 class 组件为例。它在 ReactDOM 进行操作之后,立即更新了 documenttitle 属性:

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>
    );
  }
}

在上面的代码中,需要在 class组件中在两个生命周期函数中编写重复的代码逻辑。

这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 Reactclass 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。

现在让我们来看看如何使用 useEffect 执行相同的操作。

使用 Hook 的示例

useEffect 解决了 class 组件存在的生命周期臃肿问题。

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

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

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

useEffect 做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

为什么在组件内部调用 useEffect? 将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 javascript闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API

useEffect 会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

需要清除的 effect

之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。

使用 Class 示例
React class 中,你通常会在 componentDidMount 中设置订阅,并在 componentWillUnmount 中清除它。例如,假设我们有一个 ChatAPI 模块,它允许我们订阅好友的在线状态。以下是我们如何使用 class 订阅和显示该状态:

class FriendStatus extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }
  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }

  render() {
    if (this.state.isOnline === null) {
      return 'Loading...';
    }
    return this.state.isOnline ? 'Online' : 'Offline';
  }
}

你会注意到 componentDidMountcomponentWillUnmount 之间的代码逻辑相互对应。使用生命周期函数迫使我们拆分这些逻辑代码,即使这两部分代码都作用于相同的副作用。

使用 Hook 示例
你可能认为需要单独的 effect 来执行清除操作。但由于添加和删除订阅的代码的紧密性,所以 useEffect 的设计原则是相关的业务逻辑需要在同一个地方执行。如果 effect 返回一个函数,React 将会在执行清除操作时调用它:

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

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // Specify how to clean up after this effect:
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

为什么要在 effect 中返回一个函数? 这是 effect 可选的清除机制。每个 effect 都可以返回一个清除函数。如此可以将添加和移除订阅的逻辑放在一起。它们都属于 effect 的一部分。

React 何时清除 effectReact 会在组件卸载的时候执行清除操作。正如之前学到的,effect 在每次渲染的时候都会执行。这就是为什么 React 会在执行当前 effect 之前对上一个 effect 进行清除。

注意

并不是必须为 effect 中返回的函数命名。这里我们将其命名为 cleanup
是为了表明此函数的目的,但其实也可以返回一个箭头函数或者给起一个别的名字。

useEffect 生命周期

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

以往我们在绑定事件、解绑事件、设定定时器、查找 dom 的时候,都是通过 componentDidMountcomponentDidUpdatecomponentWillUnmount 生命周期来实现的,而 useEffect 会在组件每次 render 之后调用,就相当于这三个生命周期函数,只不过可以通过传参来决定是否调用。

需要注意的是,useEffect 会返回一个回调函数,作用于清除上一次副作用遗留下来的状态,如果useEffect 只调用一次,该回调函数相当于 componentWillUnmount 生命周期。

具体看下面例子:

  function App () {
    const [ count, setCount ] = useState(0)
    const [ width, setWidth ] = useState(document.body.clientWidth)

    const onChange = () => {
      setWidth(document.body.clientWidth)
    }

    useEffect(() => {
      window.addEventListener('resize', onChange, false)
      return () => {
        window.removeEventListener('resize', onChange, false)
      }
    })

    useEffect(() => {
      document.title = count
    })

    return (
      <div>
        页面名称: { count } 
        页面宽度: { width }
        <button onClick={() => { setCount(count + 1)}}>点我</button>
      </div>
      )
  }

上面例子要处理两种副作用逻辑,这里我们既要处理 title,还要监听屏幕宽度改变,按照 class 的写法,我们要在生命周期中处理这两种逻辑,但在 hooks 中,我们只需要两个 useEffect 就能解决这些问题,我们之前提到,useEffect 能够返回一个函数,用来清除上一次副作用留下的状态,这个地方我们可以用来解绑事件监听,这个地方存在一个问题,就是 useEffect 是每次 render 之后就会调用,比如 title 的改变,相当于 componentDidUpdate,但我们的事件监听不应该每次 render 之后,进行一次绑定和解绑,就是我们需要 useEffect 变成 componentDidMount, 它的返回函数变成 componentWillUnmount,这里就需要用到 useEffect 函数的第二个参数。

useEffect 的第二个参数

useEffect 的第二个参数,分三种情况:

  • 什么都不传,组件每次 render 之后 useEffect 都会调用,相当于 componentDidMountcomponentDidUpdate;
  • 传入一个空数组 [], 只会调用一次,相当于 componentDidMountcomponentWillUnmount;
  • 传入一个数组,其中包括变量,只有这些变量变动时,useEffect 才会执行;

具体看下面例子:

  function App () {
    const [ count, setCount ] = useState(0)
    const [ width, setWidth ] = useState(document.body.clientWidth)

    const onChange = () => {
      setWidth(document.body.clientWidth)
    }

    useEffect(() => {
      // 相当于 componentDidMount
      console.log('add resize event')
      window.addEventListener('resize', onChange, false)

      return () => {
        // 相当于 componentWillUnmount
        window.removeEventListener('resize', onChange, false)
      }
    }, [])

    useEffect(() => {
      // 相当于 componentDidUpdate
      document.title = count
    })

    useEffect(() => {
      console.log(`count change: count is ${count}`)
    }, [ count ])

    return (
      <div>
        页面名称: { count } 
        页面宽度: { width }
        <button onClick={() => { setCount(count + 1)}}>点我</button>
      </div>
      )
  }

根据上面例子的运行结果,第一个 useEffect 中的 ‘add resize event’ 只会在第一次运行时输出一次,无论组件怎么 render,都不会在输出;第二个 useEffect 会在每次组件 render 之后都执行,title 每次点击都会改变; 第三个 useEffect, 只有在第一次运行和 count 改变时,才会执行,屏幕发生改变引起的 render 并不会影响第三个 useEffect

2.3 Context hook

context 中的 ProviderConsumer,在类组件和函数组件中都能使用,contextType 只能在类组件中使用,因为它是类的静态属性,具体如何使用 useContext 呢?

const value = useContext(MyContext);

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider>value prop 决定。

当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext providercontext value 值。即使祖先使用 React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

useContext 的参数必须是 context 对象本身:

  • 正确: useContext(MyContext)
  • 错误: useContext(MyContext.Consumer)
  • 错误: useContext(MyContext.Provider)

调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以通过使用 memoization 来优化。

提示

如果你在接触 Hook 前已经对 context API 比较熟悉,那应该可以理解,useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>

useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context

把如下代码与 Context.Provider 放在一起

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

2.4 Memo hook

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

useMemo 是什么呢?它跟 memo 有关系吗?memo 就是函数组件的 PureComponent,用来做性能优化的手段,useMemo 也是,useMemoVuecomputed 计算属性类似,都是根据依赖的值计算出结果,当依赖的值未发生改变的时候,不触发状态改变,useMemo 具体如何使用呢,看下面例子:

function App () {
  const 

以上是关于React进阶:HOOK的主要内容,如果未能解决你的问题,请参考以下文章

使用Hook更新上下文状态值

React Hook的使用

react讲解(函数式组件,react hook)

react讲解(函数式组件,react hook)

react讲解(函数式组件,react hook)

react讲解(函数式组件,react hook)