据说你还不懂React Hook?

Hook 是 React 16.8 的新增特性。它可让你在不编写 class 的状况下使用 state 以及其余的 React 特性javascript

从官网的这句话中,咱们能够明确的知道,Hook增长了函数式组件中state的使用,在以前函数式组件是没法拥有本身的状态,只能经过props以及context来渲染本身的UI,而在业务逻辑中,有些场景必需要使用到state,那么咱们就只能将函数式组件定义为class组件。而如今经过Hook,咱们能够轻松的在函数式组件中维护咱们的状态,不须要更改成class组件。html

React Hooks要解决的问题是状态共享,这里的状态共享是指只共享状态逻辑复用,并非指数据之间的共享。咱们知道在React Hooks以前,解决状态逻辑复用问题,咱们一般使用higher-order componentsrender-props,那么既然已经有了这两种解决方案,为何React开发者还要引入React Hook?对于higher-order componentsrender-propsReact Hook的优点在哪?java

React Hook例子

咱们先来看一下React官方给出的React Hookdemoajax

import { useState } from 'React';

function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);

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

咱们再来看看不用React Hook的话,如何实现npm

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

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

能够看到,在React Hook中,class Example组件变成了函数式组件,可是这个函数式组件却拥有的本身的状态,同时还能够更新自身的状态。这一切都得益于useState这个HookuseState 会返回一对值:当前状态和一个让你更新它的函数,你能够在事件处理函数中或其余一些地方调用这个函数。它相似 class 组件的 this.setState,可是它不会把新的 state 和旧的 state 进行合并redux

React复用状态逻辑的解决方案

Hook是另外一种复用状态逻辑的解决方案,React开发者一直以来对状态逻辑的复用方案不断提出以及改进,从Mixin到高阶组件到Render Props 到如今的Hook,咱们先来简单了解一下之前的解决方案设计模式

Mixin模式

React最先期,提出了根据Mixin模式来复用组件之间的逻辑。在Javascript中,咱们能够将Mixin继承看做是经过扩展收集功能的一种途径.咱们定义的每个新的对象都有一个原型,从中它能够继承更多的属性.原型能够从其余对象继承而来,可是更重要的是,可以为任意数量的对象定义属性.咱们能够利用这一事实来促进功能重用。数组

React中的mixin主要是用于在彻底不相关的两个组件中,有一套基本类似的功能,咱们就能够将其提取出来,经过mixin的方式注入,从而实现代码的复用。例如,在不一样的组件中,组件须要每隔一段时间更新一次,咱们能够经过建立setInterval()函数来实现这个功能,同时在组件销毁的时候,咱们须要卸载此函数。所以能够建立一个简单的 mixin,提供一个简单的 setInterval() 函数,它会在组件被销毁时被自动清理。性能优化

var SetIntervalMixin = {
  componentWillMount: function() {
    this.intervals = [];
  },
  setInterval: function() {
    this.intervals.push(setInterval.apply(null, arguments));
  },
  componentWillUnmount: function() {
    this.intervals.forEach(clearInterval);
  }
};

var createReactClass = require('create-React-class');

var TickTock = createReactClass({
  mixins: [SetIntervalMixin], // 使用 mixin
  getInitialState: function() {
    return {seconds: 0};
  },
  componentDidMount: function() {
    this.setInterval(this.tick, 1000); // 调用 mixin 上的方法
  },
  tick: function() {
    this.setState({seconds: this.state.seconds + 1});
  },
  render: function() {
    return (
      <p>
        React has been running for {this.state.seconds} seconds.
      </p>
    );
  }
});

ReactDOM.render(
  <TickTock />,
  document.getElementById('example')
);
复制代码

mixin的缺点

  1. 不一样mixin可能会相互依赖,耦合性太强,致使后期维护成本太高
  2. mixin中的命名可能会冲突,没法使用同一命名的mixin
  3. mixin即便开始很简单,它们会随着业务场景增多,时间的推移产生滚雪球式的复杂化

具体缺点能够看此连接Mixins是一种祸害bash

由于mixin的这些缺点存在,在React中已经不建议使用mixin模式来复用代码,React全面推荐使用高阶组件来替代mixin模式,同时ES6自己是不包含任何 mixin 支持。所以,当你在 React 中使用 ES6 class 时,将不支持 mixins

高阶组件

高阶组件(HOC)React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而造成的设计模式

高级组件并非React提供的API,而是React的一种运用技巧,高阶组件能够看作是装饰者模式(Decorator Pattern)在React的实现。装饰者模式: 动态将职责附加到对象上,若要扩展功能,装饰者提供了比继承更具弹性的代替方案.

具体而言,高阶组件是参数为组件,返回值为新组件的函数。

组件是将 props 转换为 UI,而高阶组件是将组件转换为另外一个组件

咱们能够经过高阶组件动态给其余组件增长日志打印功能,而不影响原先组件的功能

function logProps(WrappedComponent) {
  return class extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log('Current props: ', this.props);
      console.log('Next props: ', nextProps);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  }
}
复制代码

Render Propss

术语 “Render Props” 是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术

具备 Render Props 的组件接受一个函数,该函数返回一个 React 元素并调用它而不是实现本身的渲染逻辑

如下咱们提供了一个带有prop<Mouse>组件,它可以动态决定什么须要渲染,这样就能对<Mouse>组件的逻辑以及状态复用,而不用改变它的渲染结构。

class Cat extends React.Component {
  render() {
    const mouse = this.props.mouse;
    return (
      <img src="/cat.jpg" style={{ position: 'absolute', left: mouse.x, top: mouse.y }} />
    );
  }
}

class Mouse extends React.Component {
  constructor(props) {
    super(props);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.state = { x: 0, y: 0 };
  }

  handleMouseMove(event) {
    this.setState({
      x: event.clientX,
      y: event.clientY
    });
  }

  render() {
    return (
      <div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
        {this.props.render(this.state)}
      </div>
    );
  }
}

class MouseTracker extends React.Component {
  render() {
    return (
      <div>
        <h1>移动鼠标!</h1>
        <Mouse render={mouse => (
          
        )}/>
      </div>
    );
  }
}
复制代码

然而一般咱们说的Render Props 是由于模式才被称为 Render Props ,又不是由于必定要用renderprop进行命名。咱们也能够这样来表示

<Mouse>
  {mouse => (
    <Cat mouse={mouse} />
  )}
</Mouse>
复制代码

React Hook动机

React Hook是官网提出的又一种全新的解决方案,在了解React Hook以前,咱们先看一下React Hook提出的动机

  1. 在组件之间复用状态逻辑很难
  2. 复杂组件变得难以理解
  3. 难以理解的 class

下面说说我对这三个动机的理解:

在组件之间复用状态逻辑很难,在以前,咱们经过高阶组件(Higher-Order Components)和渲染属性(Render Propss)来解决状态逻辑复用困难的问题。不少库都使用这些模式来复用状态逻辑,好比咱们经常使用reduxReact Router。高阶组件、渲染属性都是经过组合来一层层的嵌套共用组件,这会大大增长咱们代码的层级关系,致使层级的嵌套过于夸张。从Reactdevtool咱们能够清楚的看到,使用这两种模式致使的层级嵌套程度

复杂组件变得难以理解,在不断变化的业务需求中,组件逐渐会被状态逻辑以及反作用充斥,每一个生命周期经常会包含一些不相关的逻辑。咱们写代码一般都依据函数的单一原则,一个函数通常只处理一件事,但在生命周期钩子函数中一般会同时作不少事情。好比,在咱们须要在componentDidMount中发起ajax请求获取数据,同时有时候也会把事件绑定写在今生命周期中,甚至有时候须要在componentWillReceiveProps中对数据进行跟componentDidMount同样的处理。

相互关联且须要对照修改的代码被进行了拆分,而彻底不相关的代码却在同一个方法中组合在一块儿。如此很容易产生 bug,而且致使逻辑不一致。

难以理解的class,我的以为使用class组件这种仍是能够的,只要了解了classthis指向绑定问题,其实上手的难度不大。你们要理解,这并非 React 特有的行为;这其实与 JavaScript 函数工做原理有关。因此只要了解好JS函数工做原理,其实this绑定都不是事。只是有时候为了保证this的指向正确,咱们一般会写不少代码来绑定this,若是忘记绑定的话,就有会各类bug。绑定this方法:

1.this.handleClick = this.handleClick.bind(this);
2.<button onClick={(e) => this.handleClick(e)}>
   Click me
 </button>
复制代码

因而为了解决以上问题,React Hook就被提出来了

state Hook使用

咱们回到刚刚的代码中,看一下如何在函数式组件中定义state

import React, { useState } from 'React';
const [count, setCount] = useState(0);
复制代码
  1. useState作了啥

    咱们能够看到,在此函数中,咱们经过useState定义了一个'state变量',它与 class 里面的 this.state 提供的功能彻底相同.至关于如下代码

    class Example extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        };
      }
    复制代码
  2. useState参数

    在代码中,咱们传入了0做为useState的参数,这个参数的数值会被当成count初始值。固然此参数不限于传递数字以及字符串,能够传入一个对象当成初始的state。若是state须要储存多个变量的值,那么调用屡次useState便可

  3. useState返回值

    返回值为:当前 state 以及更新 state 的函数,这与 class 里面 this.state.count 和 this.setState 相似,惟一区别就是你须要成对的获取它们。看到[count, setCount]很容易就能明白这是ES6的解构数组的写法。至关于如下代码

    let _useState = useState(0);// 返回一个有两个元素的数组
    let count = _useState[0];// 数组里的第一个值
    let setCount = _useState[1];// 数组里的第二个值
    复制代码

读取状态值

只须要使用变量便可

之前写法

<p>You clicked {this.state.count} times</p>
复制代码

如今写法

<p>You clicked {count} times</p>
复制代码

更新状态

经过setCount函数更新

之前写法

<button onClick={() => this.setState({ count: this.state.count + 1 })}>
    Click me
 </button>
复制代码

如今写法

<button onClick={() => setCount(count + 1)}>
    Click me
  </button>
复制代码

这里setCount接收的参数是修改过的新状态值

声明多个state变量

咱们能够在一个组件中屡次使用state Hook来声明多个state变量

function ExampleWithManyStates() {
  // 声明多个 state 变量!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
  // ...
}
复制代码

React 假设当你屡次调用 useState 的时候,你能保证每次渲染时它们的调用顺序是不变的

为何React要规定每次渲染它们时的调用顺序不变呢,这个是一个理解Hook相当重要的问题

Hook 规则

Hook 本质就是 JavaScript 函数,可是在使用它时须要遵循两条规则。而且React要求强制执行这两条规则,否则就会出现异常的bug

  1. 只在最顶层使用 Hook

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

  1. 只在 React 函数中调用 Hook

不要在普通的 JavaScript 函数中调用 Hook

这两条规则出现的缘由是,咱们能够在单个组件中使用多个State HookEffect HookReact 靠的是 Hook 调用的顺序来知道哪一个 state 对应哪一个useState

function Form() {
  const [name1, setName1] = useState('Arzh1');
  const [name2, setName2] = useState('Arzh2');
  const [name3, setName3] = useState('Arzh3');
  // ...
}
// ------------
// 首次渲染
// ------------
useState('Arzh1')       // 1. 使用 'Arzh1' 初始化变量名为 name1 的 state
useState('Arzh2')       // 2. 使用 'Arzh2' 初始化变量名为 name2 的 state
useEffect('Arzh3')     	// 3. 使用 'Arzh3' 初始化变量名为 name3 的 state

// -------------
// 二次渲染
// -------------
useState('Arzh1')        // 1. 读取变量名为 name1 的 state(参数被忽略)
useState('Arzh2')        // 2. 读取变量名为 name2 的 state(参数被忽略)
useEffect('Arzh3')       // 3. 读取变量名为 name3 的 state(参数被忽略)

复制代码

若是咱们违反React的规则,使用条件渲染

if (name !== '') {
    const [name2, setName2] = useState('Arzh2');
}
复制代码

假设第一次(name !== '')true的时候,执行此Hook,第二次渲染(name !== '')false时,不执行此Hook,那么Hook的调用顺序就会发生变化,产生bug

useState('Arzh1')        // 1. 读取变量名为 name1 的 state
//useState('Arzh2') // 2. Hook被忽略
useEffect('Arzh3')       // 3. 读取变量名为 name2(以前为name3) 的 state
复制代码

React 不知道第二个 useState 的 Hook 应该返回什么。React 会觉得在该组件中第二个 Hook 的调用像上次的渲染同样,对应的是 arzh2 的 useState,但并不是如此。因此这就是为何React强制要求Hook使用必须遵循这两个规则,同时咱们可使用 eslint-plugin-React-Hooks来强制约束

Effect Hook使用

咱们在上面的代码中增长Effect Hook的使用,在函数式组件中增长反作用,修改网页的标题

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

若是你熟悉 React class 的生命周期函数,你能够把 useEffect Hook 看作 componentDidMountcomponentDidUpdate 和 componentWillUnmount 这三个函数的组合。

也就是咱们彻底能够经过useEffect来替代这三个生命钩子函数

咱们来了解一下一般须要反作用的场景,好比发送请求,手动变动dom,记录日志等。一般咱们都会在第一次dom渲染完成以及后续dom从新更新时,去调用咱们的反作用操做。咱们能够看一下之前生命周期的实现

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

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

这也就是咱们上面提到的React Hook动机的第二个问题来源之一,须要在第一次渲染以及后续的渲染中调用相同的代码

Effect在默认状况下,会在第一次渲染以后每次更新以后都会执行,这也就让咱们不须要再去考虑是componentDidMount仍是componentDidUpdate时执行,只须要明白Effect在组件渲染后执行便可

清除反作用

有时候对于一些反作用,咱们是须要去清除的,好比咱们有个需求须要轮询向服务器请求最新状态,那么咱们就须要在卸载的时候,清理掉轮询的操做。

componentDidMount() {
    this.pollingNewStatus()
  }

  componentWillUnmount() {
    this.unPollingNewStatus()
  }
复制代码

咱们可使用Effect来清除这些反作用,只须要在Effect中返回一个函数便可

useEffect(() => {
    pollingNewStatus()
    //告诉React在每次渲染以前都先执行cleanup()
    return function cleanup() {
      unPollingNewStatus()
    };
  });
复制代码

有个明显的区别在于useEffect实际上是每次渲染以前都会去执行cleanup(),而componentWillUnmount只会执行一次。

Effect性能优化

useEffect实际上是每次更新都会执行,在某些状况下会致使性能问题。那么咱们能够经过跳过 Effect 进行性能优化。在class组件中,咱们能够经过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}
复制代码

Effect中,咱们能够经过增长Effect的第二个参数便可,若是没有变化,则跳过更新

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
复制代码

其余Hooks

因为篇幅缘由,就再也不此展开了,有兴趣能够自行官网查看

参考文章

  1. 30分钟精通React Hooks
  2. React hooks实践
  3. 从Mixin到HOC再到Hook
相关文章
相关标签/搜索