據說你還不懂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
相關文章
相關標籤/搜索