天天看點

tick meter 不明确的符号_聽說你還不懂React Hook?

tick meter 不明确的符号_聽說你還不懂React Hook?

React Hook簡介

Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性

從官網的這句話中,我們可以明确的知道,Hook增加了函數式元件中state的使用,在之前函數式元件是無法擁有自己的狀态,隻能通過props以及context來渲染自己的UI,而在業務邏輯中,有些場景必須要使用到state,那麼我們就隻能将函數式元件定義為class元件。而現在通過Hook,我們可以輕松的在函數式元件中維護我們的狀态,不需要更改為class元件。

React Hooks要解決的問題是狀态共享,這裡的狀态共享是指隻共享狀态邏輯複用,并不是指資料之間的共享。我們知道在React Hooks之前,解決狀态邏輯複用問題,我們通常使用

higher-order components

render-props

,那麼既然已經有了這兩種解決方案,為什麼React開發者還要引入React Hook?對于

higher-order components

render-props

,React Hook的優勢在哪?

React Hook例子

我們先來看一下React官方給出的React Hook的demo

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的話,如何實作

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

這個

Hook

,useState 會傳回一對值:目前狀态和一個讓你更新它的函數,你可以在事件處理函數中或其他一些地方調用這個函數。它類似 class 元件的 this.setState,但是它不會把新的 state 和舊的 state 進行合并

React複用狀态邏輯的解決方案

Hook是另一種複用狀态邏輯的解決方案,React開發者一直以來對狀态邏輯的複用方案不斷提出以及改進,從Mixin到高階元件到Render Propss 到現在的Hook,我們先來簡單了解一下以前的解決方案

Mixin模式

tick meter 不明确的符号_聽說你還不懂React Hook?

在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是一種禍害

因為mixin的這些缺點存在,在React中已經不建議使用mixin模式來複用代碼,React全面推薦使用高階元件來替代mixin模式,同時ES6 本身是不包含任何 mixin 支援。是以,當你在 React 中使用 ES6 class 時,将不支援 mixins 。

tick meter 不明确的符号_聽說你還不懂React Hook?

高階元件

高階元件(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的元件,它能夠動态決定什麼需要渲染,這樣就能對元件的邏輯以及狀态複用,而不用改變它的渲染結構。

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 ,又不是因為一定要用render對prop進行命名。我們也可以這樣來表示

<Mouse>
  {mouse => (
    <Cat mouse={mouse} />
  )}
</Mouse>
           

React Hook動機

React Hook是官網提出的又一種全新的解決方案,在了解React Hook之前,我們先看一下React Hook提出的動機 1. 在元件之間複用狀态邏輯很難 2. 複雜元件變得難以了解 3. 難以了解的 class

下面說說我對這三個動機的了解:

在元件之間複用狀态邏輯很難

,在之前,我們通過高階元件(Higher-Order Components)和渲染屬性(Render Propss)來解決狀态邏輯複用困難的問題。很多庫都使用這些模式來複用狀态邏輯,比如我們常用redux、React Router。高階元件、渲染屬性都是通過組合來一層層的嵌套共用元件,這會大大增加我們代碼的層級關系,導緻層級的嵌套過于誇張。從React的devtool我們可以清楚的看到,使用這兩種模式導緻的層級嵌套程度

tick meter 不明确的符号_聽說你還不懂React Hook?
複雜元件變得難以了解

,在不斷變化的業務需求中,元件逐漸會被狀态邏輯以及副作用充斥,每個生命周期常常會包含一些不相關的邏輯。我們寫代碼通常都依據函數的單一原則,一個函數一般隻處理一件事,但在生命周期鈎子函數中通常會同時做很多事情。比如,在我們需要在

componentDidMount

中發起ajax請求擷取資料,同時有時候也會把事件綁定寫在此生命周期中,甚至有時候需要在

componentWillReceiveProps

中對資料進行跟

componentDidMount

一樣的處理。

互相關聯且需要對照修改的代碼被進行了拆分,而完全不相關的代碼卻在同一個方法中組合在一起。如此很容易産生 bug,并且導緻邏輯不一緻。
難以了解的class

,個人覺得使用class元件這種還是可以的,隻要了解了class的this指向綁定問題,其實上手的難度不大。大家要了解,這并不是 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 }; 
 }
}
           
  1. useState參數

在代碼中,我們傳入了0作為useState的參數,這個參數的數值會被當成count初始值。當然此參數不限于傳遞數字以及字元串,可以傳入一個對象當成初始的state。如果state需要儲存多個變量的值,那麼調用多次useState即可

  1. 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 Hook 或 Effect Hook, React 靠的是 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 看做

componentDidMount

componentDidUpdate

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

由于篇幅原因,就不再此展開了,有興趣可以自行官網檢視

微信公衆号

希望大家多多支援,如有幫助,掃碼關注

tick meter 不明确的符号_聽說你還不懂React Hook?

參考文章

  1. 30分鐘精通React Hooks
  2. React hooks實踐
  3. 從Mixin到HOC再到Hook