4個問題帶你進階React Hooks

前言

相信大部分人都已經在使用 React hooks 了,可是在開發過程當中,咱們要 知其然知其因此然。整理了一下最近使用 React hooks 遇到的一些問題,若是有問題或更好的答案,歡迎一塊兒交流。html

目錄

  • 爲何要在 React 中引入Hook ? Hooks 解決了什麼問題
  • mixin、HOC 、render props、hooks
  • React Hooks 原理
  • Hooks 中閉包的坑

1、爲何要在React中引入Hook? Hooks解決了什麼問題

1. 組件複用邏輯難

沒有hooks以前使用 render props高階組件render props是接受一個組件做爲propsHOC是一個函數,接受一個組件做爲參數,返回另外一個組件。使用這些開發的組件會造成「嵌套地獄」,調試困難。前端

2. 複雜組件狀態邏輯多

不少組件在最開始寫的時候都是很簡單的,基本上就是隻作一件事,當你的業務邏輯變得複雜以後,組件也會變得複雜起來。大多數狀況下,咱們不大可能把組件拆分的更小,由於可能有不少共用的狀態邏輯,拆分後,組件之間的通訊也會很複雜,甚至須要引用 Redux 來管理組件之間的狀態。vue

3. class學習成本高

要想用好 class 組件,你必須瞭解 ES6 中的class,理解 JavaSript 中 this的工做方式,要注意綁定事件處理器,清楚當前this的指向。react

詳細查看react 官方文檔 Hook 簡介

2、mixin、HOC 、render props、hooks

咱們在日常開發中常常會遇到不少的頁面有一些公共的邏輯,咱們不能每次遇到的時候都直接把原來的代碼 copy 過來改扒改扒,改的時候又要全局搜索改掉(很難保證沒有漏的,費時費力)因此要想辦法去複用,mixinHOC , render props等都是實現邏輯複用的方式。git

mixin

vue和react中都曾用過mixin(react目前已經拋棄)
mixin(混入)本質上就是將對象複製到另外一個對象上。github

const mixin = function (obj, mixins) {
    const newObj = obj;
    newObj.prototype = Object.create(obj.prototype);
    
    for(let prop in mixins) {
        if(mixins.hasOwnProperty(prop)) {
            newObj.prototype[prop] = mixins[prop];
        }
    }
    return newObj;
}

const obj = {
    sayHello() {
        console.log('hello');
    }
};
const otherObj = function() {
    console.log('otherObj');
}
const Obj = mixin(otherObj, obj);

const a = new Obj(); // otherObj
a.sayHello(); //  hello

mixin存在的幾個問題:web

  • 相關依賴:mixin有可能去依賴其餘的mixin,當咱們修改其中一個的時候,可能會影響到其餘的mixin
  • 命名衝突:不一樣的人在寫的時候頗有可能會有命名衝突,好比像 handleChange等相似常見的名字
  • 增長複雜性:當咱們一個組件引入過多的mixin時,代碼邏輯將會很是複雜,由於在不停的引入狀態,和咱們最初想的每一個組件只作單一的功能背道而馳。

HOC

HOC是React社區提出的新的方式用來取代mixin的。
高階函數是函數式編程中一個基本的概念,它描述了一種這樣的函數:接受函數做爲輸入,或是返回一個函數,好比 map, reduce等都是高階函數。
高階組件( higher-order component),相似於高階組件接受一個組件做爲參數,返回另外一個組件。面試

function getComponent(WrappedComponent) {
  return class extends React.Component {
    render() {
      return <WrappedComponent {...this.props}/>;
    }
  };
}

HOC的優勢爲:編程

  • 不會影響組件內部的狀態

HOC的問題是:api

  • 須要在原組件上進行包裹和嵌套,若是大量使用 HOC,將會產生很是多的嵌套,這讓調試變得很是困難
  • HOC能夠劫持props,在不遵照約定的狀況下也可能形成衝突

Render Props

render props: 經過props接受一個返回react element 的函數,來動態決定本身要渲染的結果

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

React Router中就用到了 Render Props

<Router>
  <Route path="/home" render={() => <div>Home</div>} />
</Router>,

它有哪些問題呢

  • 很容易形成「嵌套地獄」

使用 hooks

具體實現就是經過一個函數來封裝跟狀態有關的邏輯,將這些邏輯從組件中抽取出來。而這個函數中咱們可使用其餘的Hooks,也能夠單獨進行測試,甚至將它貢獻給社區。

import { useState, useEffect } from 'react';

function useCount() {
  const [count, setCount] = useState(0);
  useEffect(() = {
    document.title = `You clicked ${count} times`;
  });
  
  return count
}

hooks的引入就是爲了解決上面提到的這麼問題,由於 使用函數式組件,咱們在開發組件的時候,能夠當作日常寫函數同樣自由。

函數複用是比較容易的,直接傳不一樣的參數就能夠渲染不一樣的組件,複雜組件實現,咱們徹底能夠多封裝幾個函數,每一個函數只單純的負責一件事。並且不少公用的代碼邏輯和一些場景咱們能夠抽出來,封裝成自定義hooks使用,好比 Umi Hooks庫封裝了不少共用的邏輯,好比 useSearch,封裝了異步搜索場景的邏輯;好比 useVirtualList,就封裝了虛擬列表的邏輯。

3、React Hooks原理

在使用hooks的時候,你可能會對它的規則有不少疑問,好比:

  1. 只在最頂層使用 Hook,不能在循環、條件判斷或嵌套函數中調用hook。
  2. 只在 React 函數中調用 Hook,不能再普通函數中調用hook
  3. React怎麼知道哪一個state對應哪一個useState

...

咱們先來看一下官方文檔給出的解釋

每一個組件內部都有一個「記憶單元格」列表。它們只不過是咱們用來存儲一些數據的 JavaScript 對象。當你用 useState() 調用一個 Hook 的時候,它會讀取當前的單元格(或在首次渲染時將其初始化),而後把指針移動到下一個。這就是多個 useState() 調用會獲得各自獨立的本地 state 的緣由。

React中是經過相似單鏈表的形式來實現的,經過 next 按順序串聯全部的 hook。能夠看下 源碼部分

export type Hook = {|
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null, 
|};

export type Effect = {|
  tag: HookEffectTag,
  create: () => (() => void) | void,
  destroy: (() => void) | void,
  deps: Array<mixed> | null,
  next: Effect,
|};
更詳細的推薦查看 React Hooks 原理Under the hood of React’s hooks system

4、Hooks中閉包的坑

咱們先來看一下使用 setState 的更新機制:

ReactsetState函數實現中,會根據一個變量isBatchingUpdates 判斷是直接更新this.state仍是放到 隊列中回頭再說。而isBatchingUpdates 默認是false,也就表示setState會同步更新this.state。可是,有一個函數 batchedUpdates, 這個函數會把isBatchingUpdates修改成true,而當React在調用事件處理函數以前就會調用這個batchedUpdates,形成的後果,就是由React控制的事件處理程序過程setState不會同步更新this.state

知道這些,咱們下面來看兩個例子。

下面的代碼輸出什麼?

class Example extends React.Component {
   constructor() {
     super();
     this.state = {
       val: 0
     };
   }
   
   componentDidMount() {
     this.setState({val: this.state.val + 1});
     console.log(this.state.val);    // 第 1 次 log
 
     this.setState({val: this.state.val + 1});
     console.log(this.state.val);    // 第 2 次 log
 
     setTimeout(() => {
       this.setState({val: this.state.val + 1});
       console.log(this.state.val);  // 第 3 次 log 1 
 
       this.setState({val: this.state.val + 1});
       console.log(this.state.val);  // 第 4 次 log 2
     }, 0);
   }
 
   render() {
     return null;
   }
 };

打印結果是: 0, 0, 2, 3。

  1. 第一次和第二次都是在react自身生命週期內,觸發 isBatchingUpdates 爲true, 因此並不會直接執行更新state, 而是加入了 dirtyComponents,因此打印時獲取的都是更新前的狀態 0
  2. 兩次setState時,獲取到 this.state.val 都是 0,因此執行時都是將0設置爲1,在react內部會被合併掉,只執行一次。設置完成後 state.val值爲1。
  3. setTimeout中的代碼,觸發時 isBatchingUpdates爲false,因此可以直接進行更新,因此連着輸出 2, 3

上面代碼改用react hooks的話

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

const MyComponent = () => {
    const [val, setVal] = useState(0);

    useEffect(() => {
        setVal(val+1);
        console.log(val);

        setVal(val+1);
        console.log(val);

        setTimeout(() => {
            setVal(val+1);
            console.log(val);

            setVal(val+1);
            console.log(val);
        }, 0)
    }, []);
    return null
};

export default MyComponent;

打印輸出: 0, 0, 0, 0。

更新的方式沒有改變。首先是由於 useEffect 函數只運行一次,其次setTimeout是個閉包,內部獲取到值val一直都是 初始化聲明的那個值,因此訪問到的值一直是0。以例子來看的話,並無執行更新的操做。

在這種狀況下,須要使用一個容器,你能夠將更新後的狀態值寫入其中,並在之後的 setTimeout中訪問它,這是useRef的一種用例。能夠將狀態值與refcurrent屬性同步,並在setTimeout中讀取當前值。

關於這部分詳細內容能夠查看 React useEffect的陷阱

參考

相關文章
相關標籤/搜索