万字总结,React Hooks 初探

“这是我参与8月更文挑战的第6天,活动详情查看: 8月更文挑战javascript

1. React Hooks诞生以前

Hook 是 React 16.8 的新增特性,它可让咱们在不编写class的状况下使用state以及其余的React特性(好比生命周期)。React Hooks 的出现是对类组件函数组件这两种组件形式的思考和侧重。下面就来看看函数组件和类组件分别有哪些优缺点。java

(1)类组件

类组件是基于 ES6中的 Class 写法,经过继承 React.Component 得来的 React 组件。下面是一个类组件:react

import React from 'react';

class ClassComponent extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      text: ""
    }
  }
  
  componentDidMount() {
    //...
  }
  changeText = (newText) => {
    this.setState({
      text: newText
    });
  };

  render() {
    return (
      <div> <p>{this.state.text}</p> <button onClick={this.changeText}>修改</button> </div>
    );
  }
}

export default ClassComponent
复制代码

对于类组件,总结其优势以下:web

  • 组件状态: 类组件能够定义本身的state,用来保存组件内部状态;而函数组件不能够,函数每次调用都会产生新的临时变量;
  • 生命周期: 类组件有生命周期,能够在对应的生命周期中完成业务逻辑,好比在componentDidMount中发送网络请求,而且该生命周期函数只会执行一次;而在函数组件中发送网络请求时,每次从新渲染都会从新发送一次网络请求;
  • 渲染优化: 类组件能够在状态改变时只从新执行render函数以及但愿从新调用的生命周期函数componentDidUpdate等;而函数组件在从新渲染时,整个函数都会被执行。

对于类组件,总结其缺点以下:npm

  • 难以拆分: 随着业务的增多,类组件会变得愈来愈复杂,不少逻辑每每混在一块儿,强行拆分反而会形成过分设计,增长了代码的复杂度;
  • 难以理解:类组件中有 this 生命周期这两大痛点。对于生命周期,不只学习成本高,而且须要将业务逻辑规划在合适的生命周期中,每一个生命周期中的逻辑看上去毫无关联,逻辑就像是被“打散”进生命周期里了同样;除此以外,在类组件中涉及到了 this 的指向,咱们必须搞清楚this的指向究竟是谁,这个过程就很容易出现问题。为了解决 this 不符合预期的问题,可使用 bind、箭头函数来解决。但本质上都是在用实践层面的约束来解决设计层面的问题
  • 难以复用组件状态: 复用状态逻辑主要靠的是 HOC(高阶组件)和 Render Props 这些组件设计模式,React 在原生层面并无提供相关的途径。这些设计模式并不是万能,它们在实现逻辑复用的同时,也破坏着组件的结构,其中一个最多见的问题就是“嵌套地狱”现象。

(2)函数组件

函数组件就是以函数的形态存在的 React 组件。函数组件内部没法定义和维护 state,所以它还有一个别名叫“无状态组件”。下面是一个函数组件:编程

import React from 'react';

function FunctionComponent(props) {
  const { text } = props
  return (
    <div> <p>{`函数组件接收的内容:${text}`}</p> </div>
  );
}

export default FunctionComponent
复制代码

相比于类组件,函数组件肉眼可见的特质天然包括轻量、灵活、易于组织和维护、较低的学习成本等。实际上,类组件和函数组件之间,是面向对象函数式编程这两个设计思想之间的差别。而函数组件更加契合 React 框架的设计理念: image.pngredux

React 组件自己的定位就是函数:输入数据,输出 UI 的函数。React 框架的主要工做就是及时地把声明式的代码转换为命令式的 DOM 操做,把数据层面的描述映射到用户可见的 UI 变化中。从原则上来说,React 的数据应该老是牢牢地和渲染绑定在一块儿的,而类组件没法作到这一点。函数组件就真正地将数据和渲染绑定到一块儿。函数组件是一个更加匹配其设计理念、也更有利于逻辑拆分与重用的组件表达形式。设计模式

为了让开发者更好的编写函数组件。React Hooks 应运而生。数组

2. React Hooks是什么?

(1)概念

为了让函数组件更有用,目标就是给函数组件加上状态。咱们知道,函数和类不一样,它并无一个实例的对象可以在屡次执行之间来保存状态,那就须要一个函数外的空间来保存这个状态,而且可以检测状态的变化,从而触发组件的从新渲染。因此,咱们须要一个机制,将数据绑定到函数的执行。当数据变化时,函数能自动从新执行。这样,任何会影响 UI 展示的外部数据,均可以经过这个机制绑定到 React 的函数组件上。而这个机制就是React Hooks。浏览器

实际上,React Hooks 是一套可以使函数组件更强大、更灵活的“钩子”。在 React 中,Hooks 就是把某个目标结果钩到某个可能会变化的数据源或者事件源上, 那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会从新执行,产生更新后的结果。咱们知道,函数组件相对于类组件更适合去表达 React 组件的执行的,由于它更符合 State => View 逻辑关系,可是由于缺乏状态、生命周期等机制,让它一直功能受到限制,而 React Hooks 的出现,就是为了帮助函数组件补齐这些缺失的能力。

下面就经过一个计数器,来看看使用类组件和React Hooks分别是如何实现的。

使用类组件实现:

import React from 'react'

class CounterClass extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      counter: 0
    }
  }

  render() {
    return (
      <div> <h2>当前计数: {this.state.counter}</h2> <button onClick={e => this.increment()}>+1</button> <button onClick={e => this.decrement()}>-1</button> </div>
    )
  }

  increment() {
    this.setState({counter: this.state.counter + 1})
  }

  decrement() {
    this.setState({counter: this.state.counter - 1})
  }
}

export default CounterClass
复制代码

使用React Hooks实现:

import React, { useState } from 'react';

function CounterHook() {
  const [counter, setCounter] = useState(0);

  return (
    <div> <h2>当前计数: {counter}</h2> <button onClick={e => setState(counter + 1)}>+1</button> <button onClick={e => setState(counter - 1)}>-1</button> </div>
  )
}

export default CounterHook
复制代码

经过两段代码能够看到,使用React Hooks实现的代码更加简洁,逻辑更加清晰。

(2)特色

React的特色主要有如下两点:简化逻辑复用有助于关注分离。

1)简化逻辑复用

在出现Hooks 以前,组件逻辑的复用是比较难实现的,咱们必须借助高阶组件(HOC)和Render Props 这些组件设计模式来实现React Hooks出现以后,这些问题就迎刃而解了。

下面来举一个例子:咱们有多个组件,当用户调整浏览器的窗口大小是,须要从新调整页面的布局。在React中,咱们会根据Size大小来渲染不一样的组件。代码以下:

function render() {
  return size === small ? <SmallComponent /> : <LargeComponent />;
}
复制代码

这段代码看起来很简单。可是若是咱们使用类组件去实现时,就须要使用到高阶组件来解决,下面就用高阶组件来实现一下。

首先要定义一个高阶组件,负责监听窗口的大小的变化,并将变化后的值做为props传给下一个组件:

const withWindowSize = Component => {
 	class WrappedComponent extends React.PureComponent {
 		constructor(props) {
 			super(props);
 			this.state = {
 					size: this.getSize()
 			};
 		}
 		componentDidMount() {
      // 监听浏览器窗口大小
   		window.addEventListener("resize", this.handleResize);
   	}
 		componentWillUnmount() {
      // 移除监听
 			window.removeEventListener("resize", this.handleResize);
 		}
    getSize() {
 			return window.innerWidth > 1000 ? "large""small";
    }
 		handleResize = ()=> {
 			const currentSize = this.getSize();
 			this.setState({
 				size: this.getSize()
 			});
 		}
		render() {
      return <Component size={this.state.size} />;
 		}
 	}
  return WrappedComponent;
};
复制代码

这样就能够调用withWindowSize方法来产生一个新组件,新组件自带size属性,例如:

class MyComponent extends React.Component{
 	render() {
 	const { size } = this.props;
 		return size === small ? <SmallComponent /> : <LargeComponent />;
  }
}

export default withWindowSize(MyComponent); 
复制代码

能够看到,为了传递外部状态(size),咱们不得不给组件外面再套一层,这一层只是为了封装一段可重用的逻辑。这样写缺点是显而易见的:

  • 代码不直观,难以理解,给维护带来巨大挑战;
  • 增长不少额外的组件节点,每个高阶组件都会多一层包装,给调试带来困难。

而React Hooks的出现,就让这种实现变得很简单:

const getSize = () => {
  return window.innerWidth > 1000 ? "large" : "small";
}
const useWindowSize = () => {
  const [size, setSize] = useState(getSize());
  useEffect(() => {
    const handler = () => {
      setSize(getSize())
    };
    window.addEventListener('resize', handler);
    return () => {
      window.removeEventListener('resize', handler);
    };
   }, []);
   return size;
};

// 使用
const Demo = () => {
  const size = useWindowSize();
  return size === small ? <SmallComponent /> : <LargeComponent />;
};
复制代码

能够看到,窗口大小是外部的一个数据状态,经过 Hooks 的方式对其进行封装, 从而将其变成一个可绑定的数据源。这样,当窗口大小变化时,使用这个 Hook 的组件就会从新渲染。并且代码也更加简洁和直观,不会产生额外的组件节点。

2)有助于关注分离

Hooks的另一大好处就是有助于关注分离,在类组件中,咱们须要同一个业务逻辑分散在不一样的生命周期,好比上面是例子,咱们在在 componentDidMount 中监听窗口代销,在 componentWillUnmount 中去解绑监听事件。而在函数组件中,咱们能够将全部逻辑写在一块儿。经过Hooks的方式,把业务逻辑清晰地隔离开,可以让代码更加容易理解和维护。

固然 React Hooks 也不是完美的,它的缺点以下:

  • Hooks 不能彻底地为函数组件补齐类组件的能力,好比 getSnapshotBeforeUpdate、componentDidCatch 这些生命周期,目前都仍是强依赖类组件的。
  • 在类组件中有时一些方法有不少实例,若是用函数组件来解决相同的问题,业务逻辑的拆分和组织是一个很大的挑战。耦合和内聚的边界有时很难把握,函数组件给了咱们必定程度的自由,但也对开发者的水平提出了更高的要求。
  • Hooks 在使用层面有着严格的规则约束,对于 React 开发者来讲,若是不能牢记并践行 Hooks 的使用原则,若是对 Hooks 的关键原理没有扎实的把握,很容易出现预料不到的问题。

(3)使用场景

React Hooks的使用场景以下:

  • Hook的出现基本能够代替以前全部使用类组件的地方;
  • 若是是一个旧的项目,不须要将全部的代码重构为Hooks,由于它彻底向下兼容,能够渐进式的使用它;
  • Hook只能在函数组件中使用,不能在类组件或函数组件以外的地方使用。

注意: Hook指的是相似于useState、 useEffect这样的函数,Hooks是对这类函数的统称。

(4)使用规范

Hooks规范以下:

  • 始终在 React 函数的顶层使用 Hooks,遵循此规则,能够确保每次渲染组件时都以相同的顺序调用 Hook, 这就是让 React 在多个useStateuseEffect 调用之间正确保留 Hook 的状态的缘由;
  • Hooks 仅在 React 函数中使用。

Eslint Plugin 提供了 eslint-plugin-react-hooks 让咱们遵循上述两种规范。其使用方法以下:

  1. 安装插件 eslint-plugin-react-hooks:
npm install eslint-plugin-react-hooks --save-dev
复制代码
  1. 在 eslint 的 config 中配置 Hooks 规则:
{
  "plugins": [
    // ...
    "react-hooks"
  ],
  "rules": {
    // ...
    "react-hooks/rules-of-hooks": "error", // 检查 hooks 规则
    "react-hooks/exhaustive-deps": "warn"  // 检查 effect 的依赖
  }
}
复制代码

3. useState:维护状态

(1)基本使用

useState 是容许咱们在 React 函数组件中添加 state 的一个 Hook,使用形式以下:

import React, { useState } from 'react';

function Example() {
  const [state, setState] = useState(0);
  const [age, setAge] = useState(18);
}

export default Example
复制代码

这里调用 useState 方法时,就定义一个 state 变量,它的初始值为0,它与 class 里面的 this.state 提供的功能是彻底相同的。

对于 useState 方法:

(1)参数:初始化值,它能够是任意类型,比 如数字、对象、数组等。若是不设置为undefined;

(2)返回值:数组,包含两个元素(一般经过数组解构赋值来获取这两个元素);

  • 元素一:当前状态的值(第一次调用为初始化值),该值是只读的,只能经过第二个元素的方法来修改它;
  • 元素二:设置状态值的函数;

实际上,Hook 就是 JavaScript 函数,这个函数能够帮助咱们钩入 React State 以及生命周期等特性。useState 和类组件中的 setState相似。二者的区别在于,类组件中的 state 只能有一个。通常是把一个对象做为一个 state,而后再经过对象不一样的属性来表示不一样的状态。而函数组件中用 useState 则能够很容易地建立多个 state,更加语义化。

(2)复杂变量

上面定义的状态变量(值类型数据)都比较简单,那若是是一个复杂的状态变量(引用类型数据),该如何实现更新呢?下面来看一个例子:

import React, { useState } from 'react'

export default function ComplexHookState() {

  const [friends, setFrineds] = useState(["zhangsan", "lisi"]);
  
  function addFriend() {
    friends.push("wangwu");
    setFrineds(friends);
  }

  return (
    <div> <h2>好友列表:</h2> <ul> { friends.map((item, index) => { return <li key={index}>{item}</li> }) } </ul> // 正确的作法 <button onClick={e => setFrineds([...friends, "wangwu"])}>添加朋友</button> // 错误的作法 <button onClick={addFriend}>添加朋友</button> </div>
  )
}
复制代码

这里定义的状态是一个数组,若是想修改这个数组,须要从新定义一个数组来进行修改,在原数组上的修改不会引发组件的从新渲染。由于,React组件的更新机制对state只进行浅对比,也就是更新某个复杂类型数据时只要它的引用地址没变,就不会从新渲染组件。所以,当直接向原数组增长数据时,就不会引发组件的从新渲染。

对于这种状况,常见的作法就是使用扩展运算符(...)来将数组元素从新赋值给一个新数组,或者对原数据进行深拷贝获得一个新的数据。

(3)独立性

当一个组件须要多个状态时,咱们能够在组件中屡次使用 useState

const [age, setAge] = useState(17)
const [fruit, setFruit] = useState('apple')
const [todos, setTodos] = useState({text: 'learn Hooks'})
复制代码

在这里,每一个 Hook 都是相互独立的。那么当出现多个状态时,react是如何保证它的独立性呢?上面调用了三次 useState,每次都是传入一个值,react 是怎么知道这个值对应的是哪一个状态呢?

其实在初始化时会建立两个数组 statesetters,而且会设置一个光标 cursor = 0 , 在每次运行 useState 函数时,会将参数放到 state 中,并根据运行顺序来依次增长光标 cursor 的值,接着在 setters 中放入对应的 set 函数,经过光标 cursorset 函数和 state 关联起来,最后,即是将保存的 stateset 函数以数组的形式返回出去。好比在运行 setCount(15) 时,就会直接运行 set 函数,set 函数有相应的 cursor 值,而后改变 state

(4)缺点

state虽然便于维护状态,但也有缺点。一旦组件有本身状态,当组件从新建立时,就有恢复状态的过程,这会让组件变得更复杂。

好比一个组件想在服务器获取用户列表并显示,若是把读取到的数据放到本地的 state 里,那么每一个用到这个组件的地方,就都须要从新获取一遍。 而若是经过一些状态管理框架(例如redux),去管理全部组件的 state ,那么组件自己就能够是无状态的。无状态组件能够成为更纯粹的表现层,没有太多的业务逻辑,从而更易于使用、测试和维护。

4. useEffect:执行反作用

(1)基本使用

函数式组件经过 useState 具有了操控 state 的能力,修改 state 须要在适当的场景进行:类组件在组件生命周期中进行 state 更新,函数式组件中须要用 useEffect 来模拟生命周期。目前 useEffect 至关于类组件中的 componentDidMount、componentDidUpdate、componentWillUnmount 三个生命周期的综合。也就是说,useEffect 声明的回调函数会在组件挂载、更新、卸载的时候执行。实际上,useEffect的做用就是执行反作用, 而反作用就是上面所说的这些和当前执行结果无关的代码。 手动操做 DOM、订阅事件、网络请求等都属于React更新DOM的反作用。

useEffect 的使用形式以下:

useEffect(callBack, [])
复制代码

useEffect 接收两个参数,分别是回调函数依赖数组。为了不每次渲染都执行全部的 useEffect 回调,useEffect 提供了第二个参数,该参数一个数组。只有在渲染时数组中的值发生了变化,才会执行该 useEffect 的回调。

(2)使用示例

下面来看一个例子:

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

function App() {
  const [count, setCount] = useState(0)
  
  useEffect(() => {
    console.log(count + '值发生了改变')
  }, [count])
  
  function changeTheCount () {
    setCount(count + 1)
  }
  
  return (
    <div> <div onClick={() => changeTheCount()}> <p>{count}</p> </div> </div>
  ) 
}
export default App
复制代码

上面的代码执行后,点击 3 次数字,count 的值变为了 3,而且在控制台打印了 4 次输出。第一次是初次 DOM 渲染完毕,后面 3 次是每次点击后改变了 count 值,触发了 DOM 从新渲染。因而可知,每次依赖数组中的元素发生改变以后都会执行 effect 函数。

useEffect 还有两个特殊的用法:没有依赖项依赖项为空数组。

1)没有依赖项

对于下面的代码,若是没有依赖项,那它会在每次render以后执行:

useEffect(() => {
    console.log(count + '值发生了改变')
})
复制代码

2)依赖项为空数组

对于下面的代码, 若是依赖项为空数组,那它会在首次执行时触发,对应到类组件的生命周期就是 componentDidMount。

useEffect(() => {
    console.log(count + '值发生了改变')
}, [])
复制代码

除此以外,useEffect 还容许返回一个方法,用于在组件销毁时作一些清理操做,以防⽌内存泄漏。好比移除事件的监听。这个机制就至关于类组件生命周期中的 componentWillUnmount。好比清除定时器:

const [data, setData] = useState(new Date());
useEffect(() => {
 	const timer = setInterval(() => {
  	 setDate(new Date());
  }, 1000);
  return () => clearInterval(timer);
}, []);
复制代码

经过这样的机制,就可以更好地管理反作用,从而确保组件和反作用的一致性。

(3)总结

从上面的示例中能够看到,useEffect主要有如下四种执行时机:

  • 每次 render 后执行:不提供第二个依赖项参数。好比:useEffect(() => {})
  • 组件 Mount 后执行:提供一个空数组做为依赖项。好比:useEffect(() => {}, [])
  • 第一次以及依赖项发生变化后执行:提供依赖项数组。好比:useEffect(() => {}, [deps])
  • 组件 unmount 后执行:返回一个回调函数。好比:useEffect() => { return () => {} }, [])

在使用useEffect时,须要注意如下几点:

  • 依赖数组中的依赖项必定是要在回调函数中使用的,否则就没有任何意义;
  • 依赖项通常是一个常量数组,由于在建立回调函数时,就应该肯定依赖项了;
  • React在每次执行时使用的是浅比较,因此必定要注意对象和数组类型的依赖项。

5. useCallback:缓存回调函数

在类组件的 shouldComponentUpdate 中能够经过判断先后的 propsstate 的变化,来判断是否须要阻止更新渲染。但使用函数组件形式失去了这一特性,没法经过判断先后状态来决定是否更新,这就意味着函数组件的每一次调用都会执行其内部的全部逻辑,会带来较大的性能损耗。useMemouseCallback 的出现就是为了解决这一性能问题。

(1)使用场景

在React函数组件中,每次UI发生变化,都是经过从新执行这个函数来完成的,这和类组件有很大的差异:函数组件没法在每次渲染之间维持一个状态。

好比下面这个计数器的例子:

function Counter() {
 const [count, setCount] = useState(0);
 const increment = () => setCount(count + 1);
 return <button onClick={increment}>+</button>
}
复制代码

因为增长计数的方法increment在组件内部,这就致使在每次修改count时,都会从新渲染这个组件,increment也就没法进行重用,每次都须要建立一个新的increment方法。

不只如此,即便count没有发生改变,当组件内部的其余state发生变化时,组件也会进行从新渲染,那这里的increment方法也会所以从新建立。虽然这些都不影响页面的正常使用,可是这增长了系统的开销,而且每次建立新函数的方式会让接收事件处理函数的组件从新渲染。

对于这种状况,那上面的例子来讲,咱们想要的就是:只有count发生变化时,对应的increment方法才会从新建立。这里就用到useCallback。

(2)基本使用

useCallback会返回一个函数的记忆的值,在依赖不变的状况下,屡次定义时,返回的值是相同的。它的使用形式以下:

useCallback(callBack, [])
复制代码

它的使用形式和useEffect相似,第一个参数是定义的回调函数,第二个参数是依赖的变量数组。只有当某个依赖变量发生变化时,才会从新声明定义的回调函数。

因为useCallback在依赖项发生变化时返回的是函数,因此没法很好的判断返回的函数是否发生变动,这里借助ES6中的数据类型Set来判断:

import React, { useState, useCallback } from "react";

const set = new Set();

export default function Callback() {
  const [count, setCount] = useState(1);
  const [value, setValue] = useState(1);

  const callback = useCallback(() => {
    console.log(count);
  }, [count]);
  set.add(callback);

  return (
    <div> <h1>Count: {count}</h1> <h1>Set.size: {set.size}</h1> <h1>Value: {value}</h1> <div> <button onClick={() => setCount(count + 1)}>Count + 1</button> <button onClick={() => setValue(value + 2)}>Value + 2</button> </div> </div>
  );
}

复制代码

运行效果以下图所示: wu7h5-46iw4.gif 能够看到,当咱们点击Count + 1按钮时,Count和Set.size都增长1,说明产生了新的回调函数。当点击Value + 2时,只有Value发生了变化,而Set.size没有发生变化,说明没有产生的新的回调函数,返回的是缓存的旧版本函数。

既然咱们知道了useCallback有这样的特色,那在什么状况下能发挥出它的做用呢?

使用场景: 父组件中一个子组件,经过状况下,当父组件发生更新时,它的子组件也会随之更新,在多数状况下,子组件随着父组件更新而更新是没有必要的。这时就能够借助useCallback来返回函数,而后把这个函数做为props传递给子组件,这样,子组件就能够避免没必要要的更新。

import React, { useState, useCallback, useEffect } from "react";
export default function Parent() {
  const [count, setCount] = useState(1);
  const [value, setValue] = useState(1);

  const callback = useCallback(() => {
    return count;
  }, [count]);

  return (
    <div> <h1>Parent: {count}</h1> <h1>Value: {value}</h1> <Child callback={callback} /> <div> <button onClick={() => setCount(count + 1)}>Count + 1</button> <button onClick={() => setValue(value + 2)}>Value + 2</button> </div> </div>
  );
}

function Child({ callback }) {
  const [count, setCount] = useState(() => callback());
  useEffect(() => {
    setCount(callback());
  }, [callback]);

  return <h2>Child: {count}</h2>;
}
复制代码

对于这段代码,运行结果以下:

dz8qw-hdm40.gif

能够看到,当咱们点击Counte + 1按钮时,Parent和Child都会加一;当点击Value + 1按钮时,只有Value增大了,Child组件中的数据并无变化,因此就不会从新渲染。这样就避免了一些无关的操做而形成子组件随父组件而从新渲染。

除了上面的例子,全部依赖本地状态或props来建立函数,须要使用到缓存函数的地方,都是useCallback的应用场景。一般使用useCallback的目的是不但愿子组件进行屡次渲染,而不是为了对函数进行缓存。

6. useMemo:缓存计算结果

useMemo实际的目的也是为了进行性能的优化。

(1)使用场景

下面先来看一段代码:

import React, { useState } from "react";

export default function WithoutMemo() {
  const [count, setCount] = useState(1);
  const [value, setValue] = useState(1);

  function expensive() {
    console.log("compute");
    let sum = 0;
    for (let i = 0; i < count * 100; i++) {
      sum += i;
    }
    return sum;
  }

  return (
    <div> <h1>Count: {count}</h1> <h1>Value: {value}</h1> <h1>Expensive: {expensive()}</h1> <div> <button onClick={() => setCount(count + 1)}>Count + 1</button> <button onClick={() => setValue(value + 2)}>Value + 2</button> </div> </div>
  );
}
复制代码

这段代码很简单,expensive方法用来计算0到100倍count的和,这个计算是很昂贵的。当咱们点击页面的两个按钮时,expensive方法都是执行(能够在控制台看到),运行结果以下图所示: 523le-wbz6x.gif

咱们知道,这个expensive方法只依赖于count,只有当count发生变化时才须要从新计算。在这种状况下,咱们就能够 useMemo,只在count的值修改时,才去执行expensive的计算。

(2)使用示例

useMemo返回的也是一个记忆的值,在依赖不变的状况下,屡次定义时,返回的值是相同的。它的使用形式以下:

useCallback(callBack, [])
复制代码

它的使用形式和上面的useCallback相似,第一个参数是产生所需数据的计算函数,通常它会使用第二个参数中依赖数组的依赖项来生成一个结果,用来渲染最终的UI。

下面就使用useMemo来优化上面的代码:

import React, { useState, useMemo } from "react";

export default function WithoutMemo() {
  const [count, setCount] = useState(1);
  const [value, setValue] = useState(1);

  const expensive = useMemo(() => {
    console.log("expensive执行");
    let sum = 0;
    for (let i = 0; i < count * 100; i++) {
      sum += i;
    }
    return sum;
  }, [count]);

  return (
    <div> <h1>Count: {count}</h1> <h1>Value: {value}</h1> <h1>Expensive: {expensive}</h1> <div> <button onClick={() => setCount(count + 1)}>Count + 1</button> <button onClick={() => setValue(value + 2)}>Value + 2</button> </div> </div>
  );
}

复制代码

代码的运行结果以下图: 8av5y-l8o6c.gif

能够看到,当点解Count + 1按钮时,expensive方法才会执行;而当点击Value + 1按钮时,expensive方法是不执行的。这里咱们使用useMemo来执行昂贵的计算,而后将计算值返回,而且将count做为依赖值传递进去。这样只会在count改变时触发expensive的执行,在修改value时,返回的是上一次缓存的值。

因此,当某个数据是经过其它数据计算获得的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该须要从新计算。useMemo能够避免在用到的数据没发生变化时进行重复的计算。

除此以外,useMemo 还有一个很重要的用处:避免子组件的重复渲染, 这和上面的useCallback是很相似的,这里就不举例说明了。

能够看到,useMemo和useCallback是很相似的,它们之间是能够相互转化的:useCallback(fn, deps) 至关于 useMemo(() => fn, deps) 。

7. useRef:共享数据

函数组件虽然看起来很直观,可是到目前为止,它相对于类组件还缺乏一个很重要的能力,那就是组件屡次渲染之间共享数据。在类函数中,咱们能够经过对象属性来保存数据状态。可是在函数组件中,没有这样一个空间去保存数据。所以,useRef 就提供了这样的功能。

useRef的使用形式以下:

const myRefContainer = useRef(initialValue);
复制代码

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数。返回的 ref 对象在组件的整个生命周期内保持不变,也就是说每次从新渲染函数组件时,返回的 ref 对象都是同一个。

那在实际应用中,useRef有什么用呢?主要有两个应用场景:

(1)绑定DOM

有这样一个简单的场景:在初始化页面时,使得页面中的某个input输入框自动聚焦,使用类组件能够这样实现:

class InputFocus extends React.Component {
  refInput = React.createRef();
  componentDidMount() {
    this.refInput.current && this.refInput.current.focus();
  }
  render() {
    return <input ref={this.refInput} />;
  }
}
复制代码

那在函数组件中想要实现,能够借助useRef来实现:

function InputFocus() {
  const refInput = React.useRef(null);
  React.useEffect(() => {
    refInput.current && refInput.current.focus();
  }, []);

  return <input ref={refInput} />;
}
复制代码

这里,咱们将refInput和input输入框绑定在了一块儿,当咱们刷新页面后,鼠标仍然是聚焦在这个输入框的。

(2)保存数据

这样一个场景,就是咱们有一个定时器组件,这个组件能够开始和暂停,咱们可使用setInterval来进行计时,为了能暂停,咱们就须要获取到定时器的的引用,在暂停时清除定时器。那么这个计时器引用就能够保存在useRef中,由于它能够存储跨渲染的数据,代码以下:

import React, { useState, useCallback, useRef } from "react";

export default function Timer() {
  const [time, setTime] = useState(0);
  const timer = useRef(null);

  const handleStart = useCallback(() => {
    timer.current = window.setInterval(() => {
      setTime((time) => time + 1);
    }, 100);
  }, []);

  const handlePause = useCallback(() => {
    window.clearInterval(timer.current);
    timer.current = null;
  }, []);

  return (
    <div> <p>{time / 10} seconds</p> <button onClick={handleStart}>开始</button> <button onClick={handlePause}>暂停</button> </div>
  );
}
复制代码

能够看到,这里使用 useRef 建立了一个保存 setInterval 的引用,从而可以在点击暂停时清除定时器,达到暂停的目的。同时,使用 useRef 保存的数据通常是和 UI 的渲染无关的,当 ref 的值发生变化时,不会触发组件的从新渲染,这也是 useRef 区别于 useState 的地方。

8. useContext:全局状态管理

咱们知道,React提供了Context来管理全局的状态,当咱们在组件上建立一个 Context 时,这个组件树上的全部组件就都都能访问和修改这个 Context了。这个属性适用于类组件。在React Hooks中也提供了相似的属性,那就是useContext。

简单来讲就是 useContext 会建立一个上下文对象,而且对外暴露提供者和消费者,在上下文以内的全部子组件,均可以访问这个上下文环境以内的数据。

context 作的事情就是建立一个上下文对象,而且对外暴露提供者和消费者,在上下文以内的全部子组件,均可以访问这个上下文环境以内的数据,而且不用经过 props。 简单来讲, context 的做用就是对它所包含的组件树提供全局共享数据的一种技术。

首先,建立一个上下文,来提供两种不一样的页面主题样式:

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};
const ThemeContext = React.createContext(themes.light)
复制代码

接着,建立一个 Toolbar 组件,这个组件中包含了一个 ThemedButton 组件,这里先不关心 ThemedButton 组件的逻辑:

function Toolbar(props) {
  return (
    <div> <ThemedButton /> </div>
  );
}
复制代码

这时,须要提供者提供数据,提供者通常位于比较高的层级,直接放在 App 中。ThemeContext.Provider 就是这里的提供者,接收的 value 就是它要提供的上下文对象:

function App() {
  return (
    <ThemeContext.Provider value={themes.light}> <Toolbar /> </ThemeContext.Provider>
  );
}
复制代码

而后,消费者获取数据,这是在 ThemedButton 组件中使用:

function ThemedButton(props) {
  const theme = useContext(ThemeContext);
  const [themes, setthemes] = useState(theme.dark);

  return (
    <div> <div style={{ width: "100px", height: "100px", background: themes.background, color: themes.foreground }} ></div> <button onClick={() => setthemes(theme.light)}>Light</button> <button onClick={() => setthemes(theme.dark)}>Dark</button> </div>
  );
}
复制代码

到这里,整个例子就结束了,下面是总体的代码:

import React, { useContext, useState } from "react";

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

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

function ThemedButton(props) {
  const theme = useContext(ThemeContext);
  const [themes, setthemes] = useState(theme.dark);

  return (
    <div> <div style={{ width: "100px", height: "100px", background: themes.background, color: themes.foreground }} ></div> <button onClick={() => setthemes(theme.light)}>Light</button>&nbsp;&nbsp; <button onClick={() => setthemes(theme.dark)}>Dark</button> </div>
  );
}

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

export default function App() {
  return (
    <ThemeContext.Provider value={themes}> <Toolbar /> </ThemeContext.Provider>
  );
}
复制代码

这里经过使用useContext获取到了顶层上下文中的themes数据,运行效果以下: 1c3ot-2hkhi.gif 这里咱们的 useContext 看上去就是一个全局数据,那为何要设计这样一个复杂的机制,而不是直接用一个全局的变量去保存数据呢?其实就是为了可以进行数据的绑定。当 useContext 的数据发生变化时,使用这个数据的组件就可以自动刷新。但若是没有 useContext,而是使用一个简单的全局变量,就很难去实现数据切换了。

实际上,Context就至关于提供了一个变量的机制,而全局变量就意味着:

  • 会让调试变困难,由于很难跟踪某个 Context 的变化到底是如何产生的。
  • 让组件的复用变得困难,由于一个组件若是使用了某个 Context,它就必须确保被用到的地方必须有这个 Context 的 Provider 在其父组件的路径上。

因此,useContext是一把双刃剑,仍是要根据实际的业务场景去酌情使用。

9. useReducer:useState替代方案

在 Hooks 中提供了一个 API useReducer,它是 useState 的一种替代方案。

首先来看 useReducer 的语法:

const [state, dispatch] = useReducer((state, action) => {
    // 根据派发的 action 类型,返回一个 newState
}, initialArg, init)
复制代码

useReducer 接收 reducer 函数做为参数,reducer 接收两个参数,一个是 state,另外一个是 action,而后返回一个状态 statedispatchstate 是返回状态中的值,而 dispatch 是一个能够发布事件来更新 state 的函数。

既然它是 useState 的替代方案,那下面就来看看和 useState 有什么不一样: 1)使用useState实现:

import React, { useState } from 'react'
function App() {
  const [count, setCount] = useState(0) 
    
  return (
    <div> <h1>you click {count} times</h1> <input type="button" onClick={()=> setCount(count + 1)} value="click me" /> </div>
  ) 
}
export default App
复制代码

2)使用useReducer实现:

import React, { useReducer } from "react";

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    default:
      throw new Error();
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div> <h1>you click {state.count} times</h1> <input type="button" onClick={() => dispatch({ type: "increment" })} value="click me" /> </div>
  );
}
export default App;

复制代码

useState 对比发现改写后的代码变长了,其执行过程以下:

  • 点击 click me 按钮时,会触发 click 事件;
  • click 事件里是个 dispatch 函数,dispatch 发布事件告诉 reducer 我执行了 increment 动做;
  • reducer 会去查找 increment,返回一个新的 state 值。

下面是 useReducer 的整个执行过程:

其实 useReducer 执行过程就三步:

  • 第一步:事件发生;
  • 第二步:dispatch(action);
  • 第三步:reducer 根据 action.type 返回一个新的 state。

虽然使用useReducer时代码变长,可是理解起来好像更简单明了了,这是 useReducer 的优势之一。useReducer 主要有如下优势:

  • 更好的可读性;
  • reducer 可让咱们把作什么和怎么作分开,上面的 demo 中在点击了 click me 按钮时,咱们要作的就是发起加 1 操做,至于加 1 的操做要怎么去实现就都放在 reducer 中维护。组件中只须要考虑怎么作,使得咱们的代码能够像用户行为同样更加清晰;
  • state 处理都集中到 reducer,对 state 的变化更有掌控力,同时也更容易复用 state 逻辑变化代码,特别是对于 state 变化很复杂的场景。

当遇到如下场景时,能够优先使用 useReducer

  • state 变化很复杂,常常一个操做须要修改不少 state
  • 深层子组件里去修改一些状态;
  • 应用程序比较大,UI 和业务须要分开维护。

最后: 到这里就结束了,这篇文章只介绍了React Hooks的简单使用。最近在深刻学习Hooks,期待下一篇文章!

相关文章
相关标签/搜索