React 16.8 之 React Hook

2021-02-23 前端腦洞
(點擊上方 前端腦洞,可快速關注,感謝分享)

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

React Hook 為何而存在

1.在組件之間復用狀態邏輯很難

雖然我們可以用render props 和高階組件來解決這一問題,但是這不僅會容易形成「嵌套地獄」,還會面臨一個很尷尬的問題,那就是複雜邏輯難寫!

2.複雜組件難以理解

各種生命周期函數內充斥著各種狀態邏輯處理和副作用,且難以復用、零散,比如一個調用列表數據的接口方法getList分別要寫到componentDidMount 和componentDidUpdate中等等。

3.難以理解的class

this的指向問題(經常在某一處忘記bind(this)然後bug找半天)、組件預編譯技術(組件摺疊)會在class中遇到優化失效的case、class不能很好的壓縮、class在熱重載時會出現不穩定的情況。

有了 react hook 就不用寫class(類)了,組件都可以用function來寫,不用再寫生命周期鉤子,最關鍵的,不用再面對this了!

State Hook

首先我們先看下官方給出的簡單例子

這是一個簡單的累加計數器,我們先看class聲明的組件

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

  render() {
    return (
      <div>
        <p>你點擊了{this.state.count}次</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          點我
        </button>
      </div>
    );
  }
}

這是一個非常非常簡單的組件了,相信了解過react的人都能夠看懂

那麼我們再看一下使用hook的版本

import React, { useState } from 'react';

function Example() {
  // 聲明一個叫 "count" 的 state 變量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>你點擊了{count}次</p>
      <button onClick={() => setCount(count + 1)}>
        點我
      </button>
    </div>
  );
}

是不是hook要簡單了一點?可以看到,這是一個函數,與以往不同的是它擁有了自己的狀態(count),就像class中的this.state.count;同時它還可以通過setCount()來更新自己的狀態,就像class中的this.setState()。

為什麼會這樣呢?仔細看第一行我們引入了useState這個hook,就是這個hook讓我們的無狀態組件成為了一個有狀態的組件。

狀態值的聲明、讀取、更新

那麼我們分解一下看看到底這個hook都為我們做了什麼

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

首先useState的作用就是來聲明狀態變量,這個函數接收的參數是我們要為變量賦予的初始值,它返回了一個數組,索引[0]是當前的狀態值,[1]是用來更新這個狀態值的方法

所以這一句其實就是聲明了一個count變量,就好比

this.state = {
      count: 0
    };

並且通過useState(0)傳入參數0來為count賦了一個初始值0,同時提供了一個像this.setState一樣可以更新它的方法setCount

然後我們引用讀取這個變量的時候直接 {count}就好了,不用再this.state.count 這麼長了

<p>你點擊了{count}次</p>

當我們想更新這個值的時候,直接調用setCount(新的值)就可以了。

怎麼樣,是不是很簡單很容易理解?

但是,這個函數是怎麼記住之前的狀態的?通常來說我們在函數中聲明一個變量,函數運行完也就跟著銷毀了,重複調用的時候會重新聲明,那這個Example函數是怎麼做到記住之前聲明的狀態變量的?

State Hook 解決存儲持久化的方案

我所知道的可以通過js存儲持久化狀態的方法有: class類、全局變量、DOM、閉包

通過學習react hook源碼解析了解到是用閉包實現的~

function useState(initialState){
    let state = initialState;
    function dispatch = (newState, action)=>{
        state = newState;
    }
    return [state, dispatch]
}

像不像redux?給定一個初始state,然後通過dispatch一個action,經由reducer改變state,再返回新的state,觸發組件的重新渲染。

但是僅僅這樣還滿足不了要求,我們需要一個新的數據結構來保存上一次的state和這一次的state,以便可以在初始化流程調用useState和更新流程調用useState時可以取到對應的正確值。假定這個數據結構叫Hook:

type Hook = {
  memoizedState: any,   // 上一次完整更新之後的最終狀態值
  queue: UpdateQueue<any, any> | null, // 更新隊列
};

考慮到第一次組件mounting和後續的updating邏輯差異,定義兩個不同的useState函數來實現,分別叫做mountState和updateState

function useState(initialState){
    if(isMounting){
        return mountState(initialState);
    }
    
    if(isUpdateing){
        return updateState(initialState);
    }
}

// 第一次調用組件的 useState 時實際調用的方法
function mountState(initialState){
    let hook = createNewHook();
    hook.memoizedState = initalState;
    return [hook.memoizedState, dispatchAction]
}

function dispatchAction(action){
    // 使用數據結構存儲所有的更新行為,以便在 rerender 流程中計算最新的狀態值
    storeUpdateActions(action);
    // 執行 fiber 的渲染
    scheduleWork();
}

// 第一次之後每一次執行 useState 時實際調用的方法
function updateState(initialState){
    // 根據 dispatchAction 中存儲的更新行為計算出新的狀態值,並返回給組件
    doReducerWork();
    
    return [hook.memoizedState, dispatchAction];
}   

function createNewHook(){
    return {
        memoizedState: null,
        baseUpdate: null
    }
}

以上就是基本的實現思路,內容參考自源碼解析React Hook構建過程

聲明多個state變量

useState是可以多次調用的

function ExampleWithManyStates() {
  // 聲明多個 state 變量
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: '學習 Hook' }]);
  // ...
}

useState也支持接收對象或者數組作為參數。與this.setState不同的是,this.setState是合併狀態後返回一個新的狀態,而useState只是直接替換老狀態後返回新狀態。

Hook 規則

hook本質也是javaScript函數,但是他在使用的時候需要遵循兩條規則,並且react要求強制執行這兩條規則,不然就會出現奇怪的bug。

1.只能在最頂層使用hook

不要在循環,條件或者嵌套函數中調用hook,確保總是在react函數的最頂層調用,目的是為了確保hook在每一次渲染中都按照同樣的順序被調用,這讓react能夠在多次的useState和useEffect(另一種hook,下面會說)調用之間保持狀態的正確,來保證多個state的相互獨立和一一對應的關係。

2.只在react函數中調用hook

不要在普通js函數中調用hook

Effect Hook

官方文檔中一句話說明 Effect Hook 可以讓你在函數組件中執行副作用操作,可能一句話並不能讓人很好的理解它是幹嘛用的,我們可以接著看上面那個最簡單的計數器的慄子,在它的基礎上加一個小功能

function Example() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    //設置瀏覽器標題內容
    document.title = `你點擊了${count} 次`;
  });

  return (
    <div>
      <p>你點擊了{count}次</p>
      <button onClick={() => setCount(count + 1)}>
        點我
      </button>
    </div>
  );
}

這段代碼增加了一個將document的title設置為包含了點擊次數的消息,如果通過class組件,應該怎麼寫呢?我們對比一下:

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

  componentDidMount() {
    document.title = `你點擊了 ${this.state.count} 次`;
  }

  componentDidUpdate() {
    document.title = `你點擊了 ${this.state.count} 次`;
  }

  render() {
    return (
      <div>
        <p>你點擊了{this.state.count}次</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          點我
        </button>
      </div>
    );
  }
}

通過對比,可以看出好像effect hook就相當於生命周期的componentDidMount,componentDidUpdate;其實,我們寫的有狀態組件,通常都會產生副作用,比如ajax請求,瀏覽器事件的綁定和解綁,手動修改dom,記錄日誌等等;副作用還分為需要清除的和不需要清除的,所以effect hook 還相當於一個componentWillUnmount(比如我們有個需求是需要論詢向伺服器請求最新數據,那麼我們就需要在組件卸載的時候來清理掉這個輪詢操作)

清除副作用
componentDidMount(){
    //輪詢獲取數據
    this.getNewData()
}
componentWillUnmount(){
    //組件卸載前清除輪詢操作
    this.unGetNewData()
}

我們完全可以在函數式組件中使用effect hook 來清除這個副作用,用法是在effect函數中return一個函數(清除操作)

useEffect(()=>{
    getNewData()
    return function cleanup() {
        unGetNewData()
    }
})

effect中返回一個函數,這是effect可選的清除機制。每個effect都可以返回一個清除函數,看你的需要。

react會在組件卸載的時候執行清除操作。effect在每次渲染的時候都會執行,並且是每次渲染之前都會去執行cleanup來清除上一個effect副作用。

這種解綁模式同componentWillUnmount不一樣。componentWillUnmount只會在組件被銷毀前執行一次,而effect hook裡的函數,每次組件渲染都會執行一遍,包括副作用函數和它return的清理操作

effect的性能優化

大家一看到「每一次」都會執行,首先就會想到跟性能有關的問題,那麼其實effect是可以跳過的,同樣通過對比class組件來理解effect hook,class組件中使用componentDidUpdate來進行前後邏輯的比較

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    //判斷狀態值count改變了,再觸發副作用操作
    document.title = `你點擊了${this.state.count}次`;
  }
}

同樣的在effect中,有第二個參數,起同樣的作用

useEffect(() => {
  document.title = `你點擊了${count}次`;
}, [count]); // 僅在 count 更改時更新

第二個參數為一個數組,如果數組中有多個元素,即使只有一個元素發生了改變,react也會執行effect(即只有數組中所有元素都未變化,react才會跳過這次effect);若為空數組[],則表示該effect只會執行一次(包括副作用和return的清除副作用操作)

自定義Hook

也就是我們最初的目的:邏輯復用!試想一下,加入我們現在要實現這樣一個功能組件,點擊button隨機切換背景顏色

用我們所熟悉的class組件是這樣實現的

import React, { Component } from "react";

export default class ChangeColor extends Component {
  constructor() {
    super();
    this.state = {
      color: "red"
    };
    this.colors = ["red", "bule", "green", "yellow", "black"];
  }

  changeColor() {
    const index = Math.floor(Math.random() * this.colors.length);
    this.setState({ color: this.colors[index] });
  }

  render() {
    return (
      <div>
        <div
          style={{
            width: 400,
            height: 100,
            border: "1px solid #ccc",
            background: this.state.color
          }}
        ></div>
        <button onClick={() => this.changeColor()}>隨機切換</button>
      </div>
    );
  }
}

那麼用hook是怎麼實現的呢

import React, { useState } from "react";

export default function ChangeColor() {
  const colors = ["red", "bule", "green", "yellow", "black"];
  const [color, setColor] = useState("red");
  function changeColor() {
    const index = Math.floor(Math.random() * colors.length);
    setColor(colors[index]);
  }
  return (
    <div>
      <div
        style={{
          width: 400,
          height: 100,
          border: "1px solid #ccc",
          background: color
        }}
      ></div>
      <button onClick={changeColor}>隨機切換</button>
    </div>
  );
}

需要注意的一點是,自定義hook是一個函數,其名稱以 "use" 開頭,它的內部可以調用其他的hook(無論是api提供的還是我們自定義的)

自定義 Hook 必須以 「use」 開頭嗎?

必須如此。這個約定非常重要。不遵循的話,由於無法判斷某個函數是否包含對其內部 Hook 的調用,React 將無法自動檢查你的 Hook 是否違反了 Hook 的規則。

在兩個組件中使用相同的 Hook 會共享 state 嗎?

不會。自定義 Hook 是一種重用狀態邏輯的機制(例如設置為訂閱並存儲當前值),所以每次使用自定義 Hook 時,其中的所有 state 和副作用都是完全隔離的。

還有哪些react提供的hook

這裡我只講述了useState 和 useEffect兩個最重要也是最常用的hook,其實react還提供了很多hook:

useContext
useReducer
useCallback
useMemo
useRef
useImperativeMethods
useMutationEffect
useLayoutEffect

相關焦點

  • React Hook 入門教程
    React Hook 入門教程Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。react hook 產生的原因在組件之間復用狀態邏輯很難組件之間復用狀態邏輯很困難,我們可能會把組件連結到我們的 store 裡面。
  • React Hook起飛指南
    16.8目前放出來了10個內置hook,但僅僅基於以下兩個API,就能做很多事情。
  • 十一個優質React Hook庫, 收藏備用
    地址:https://github.com/diegohaz/constate使用案例:import React, { useState } from "react";import constate from "constate";// custom hook
  • react中關於hook介紹及其使用
    前言最近由於公司的項目開發,就學習了在react關於hook的使用,對其有個基本的認識以及如何在項目中去應用hook。hook首先介紹關於hook的含義,以及其所要去面對的一些場景含義:Hook 是 React 16.8 的新增特性。
  • React 16.8發布:hooks終於來了!
    React 16.8 終於帶來了穩定版的 Hooks。 hooks 可以讓你在不編寫類的情況下使用 state 和 React 的其他功能。你還可以構建自己的 hooks,在組件之間共享可重用的有狀態邏輯。
  • 手寫ReactHook核心原理,再也不怕面試官問我ReactHook原理
    import React ,{useState}from 'react';import ReactDOM from 'react-dom';import '.import React ,{useState,memo}from 'react';import ReactDOM from 'react-dom';import '.
  • 30 分鐘精通 React 新特性——React Hooks
    這個函數之所以這麼了不得,就是因為它注入了一個hook-- useState,就是這個hook讓我們的函數變成了一個有狀態的函數。除了 useState這個hook外,還有很多別的hook,比如 useEffect提供了類似於 componentDidMount等生命周期鉤子的功能, useContext提供了上下文(context)的功能等等。
  • React Status 中文周刊 #8 - 100 行代碼實現 Facebook 的 Recoil React 庫
    Bennett HardwickReact Hook 不好的一面 — 討論 hook 的文章數不勝數Robin Wieruch使用 `React.useImperativeHandle` 的具體示例 — 這個 hook
  • React Status 中文周刊 #19 - React Hook發布兩周年回顧
    React Hook 已發布兩年之久,讓我們跟隨推薦文章一起了解下 Hook 的前世今生。除 React 相關外,還有一個框架推薦,近來關於 Tailwind 和 CSS-in-JS 的話題不斷,與其糾結用哪個,不如全都要?應各位大佬要求,二維碼下添加了連結,但閱讀原文更佳哦~電腦閱讀,請閱讀原文。
  • 30 分鐘精通 React 今年最勁爆的新特性 —— React Hooks
    這個函數之所以這麼了不得,就是因為它注入了一個hook--useState,就是這個hook讓我們的函數變成了一個有狀態的函數。除了useState這個hook外,還有很多別的hook,比如useEffect提供了類似於componentDidMount等生命周期鉤子的功能,useContext提供了上下文(context)的功能等等。
  • React:useHooks小竅門
    (給前端大全加星標,提升前端技能)英文:usehooks  譯文:林林小輝https://zhuanlan.zhihu.com/p/66170210Hooks是React 16.8useSpring這個hook是react-spring的一部分,react-spring是一個可以讓你使用高性能物理動畫的庫。我試圖在這裡避免引入依賴關係,但是這一次為了暴露這個非常有用的庫,我要破例做一次。react-spring的優點之一就是允許當你使用動畫時完全的跳過React render的生命周期。這樣經常可以得到客觀的性能提升。
  • React Status 中文周刊 #26 - Aleph:基於 Deno 的 React 框架
    新年將近,提前預祝大家新年快樂~電腦閱讀,請訪問 https://docschina.org/weekly/react🔥 本周熱門基於 AWS Lambda 的 React 應用服務端渲染 — 通過使用 Lambda 或 Lambda@Edge
  • 你可能不知道的 React Hooks
    Hooks API Reference[6]: useEffect[7], Conditionally firing an effect[8].Hooks API Reference[14]: useState[15], Functional updates[16].
  • React系列二十一 - Hook(二)高級使用
    return {...state, counter: state.counter - 1}    default:      return state;  }}home.jsimport React, { useReducer } from 'react
  • JavaScript教程:React Hook之useState和useEffect
    如果你已經使用React(16.8之前的版本)一段時間了,你將會發現React沒有提供將可重用行為「附加」到組件的方法,雖然渲染組件和高階組件解決了一些模式上的問題,但這些模式要求你在原有項目上進行重構組件,如果設計的不合理的話,很容易造成「包裝器」的冗餘,這時候就需要一個更好的共享狀態邏輯。
  • React Status 中文周刊 #21 - 2020 年 React 大事記回顧
    原文連結:https://www.robinwieruch.de/react-librariesRobin Wieruch原文連結:https://www.robinwieruch.de/react-folder-structureRobin
  • React Hook | 必 學 的 9 個 鉤子
    ❝Hook 是 React 16.8 的新增特性。它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性。Hook 本質上就是一個函數,它簡潔了組件,有自己的狀態管理,生命周期管理,狀態共享。❞Hook 出現解決了什麼 ?
  • webpack4+react16+react-router-dom4從零配置到優化,實現路由按需...
    上一篇介紹了下webpack的一些配置,接下來講下reactRouter4裡面關於路由的一些配置,如何做到模塊的按需加載,這也是常用的一種優化網站性能的方式
  • 【React】853- 手摸手教你基於Hooks 的 Redux 實戰姿勢
    Redux 使您可以集中存放 JavaScript 應用程式的狀態(數據)它最常與 React 一起使用(通過 react-redux )這使您可以從樹中的任何組件訪問或更改狀態。4.要從 store 中取出數據,請使用 react-redux 提供的自定義 hook :useSelector 。selector 只是一個有趣的詞:「從 store 獲取數據的功能」然後,向 useSelector 中傳入回調,該回調中可獲取整個 redux 的狀態,您只需選擇該組件所需的內容
  • React + Redux + React-router 全家桶
    web端開發需要搭配React-dom使用import React from 'react';import ReactDOM from 'react-dom';const App = () => (  <div>      <