React Hook實戰(一)

React Hook實戰(一)

目錄:

  • 引-爲何用Hook
  • 基本使用
  • 自定義實現Hook
  • Hook-react的真正實現
  • Class 和 Hook對比
  • 總結-問題思考

引-爲何用Hook

在過去,咱們必須使用生命週期方法(如componentDidUpdate)的特殊函數的類組件和特殊狀態處理的方法以便處理狀態更改。React class中,尤爲是this.context的JavaScript對象,對於人和機器來講都很難閱讀和理解,由於它老是引用不一樣的東西,因此有時(例如,在事件處理程序中)咱們須要手動將它從新綁定到類對象。計算機不知道類中的哪些方法將被調用,以及如何修改這些方法,這使得性能優化和代碼優化變得困難。此外,clsss有時須要咱們一次在多個地方編寫代碼。 例如,若是咱們但願在組件初始化或數據更新時獲取數據,舉個例子:

首先,咱們經過擴展React.component類來定義咱們的類組件:

class Example extends React.Component {
複製代碼

而後,咱們定義componentDidMount生命週期方法,在該方法中,咱們從一個API中提取數據

componentDidMount () {
    fetch(`http://my.api/${this.props.name}`)
    .then(...)
  }
複製代碼

咱們還須要定義componentDidUpdate生命週期方法,當prop發生變化時判斷是否更新狀態。

componentDidUpdate (prevProps) {
    if (this.props.name !== prevProps.name) { 
      fetch(`http://my.api/${this.props.name}`)
      .then(...)
    }
  }
}
複製代碼

爲了減小代碼的重複性,咱們能夠定義一個名爲fetchData的單獨方法來獲取數據,以下所示:

fetchData () {
    fetch(`http://my.api/${this.props.name}`)
    .then(...)
  }
複製代碼

最後,咱們調用componentDidMount和ComponentDidUpdate中的方法

componentDidMount () {
    this.fetchData()
  }
  componentDidUpdate (prevProps) {
    if (this.props.name !== prevProps.name) { this.fetchData()
  }
}
複製代碼

然而,即便這樣,咱們仍然須要在兩個地方調用fetchData。每當咱們更新傳遞給方法的參數時,咱們須要在兩個地方更新它們,這使得這個模式很容易出現bug和未來的bug。

在Hook以前,若是咱們想封裝狀態管理邏輯,咱們必須使用高階組件和呈現道具。例如,咱們建立一個React組件,該組件使用上下文處理用戶身份驗證,以下所示:

咱們首先導入authenticateUser函數,以便用上下文包裝組件,而後導入AuthenticationContext組件,以便訪問上下文:

import authenticateUser, { AuthenticationContext } from './auth'
複製代碼

而後,咱們定義app組件,在其中咱們使用AuthenticationContext.Consumer組件

const App = () => (
  <AuthenticationContext.Consumer>
    {user =>
複製代碼

如今,咱們根據用戶是否登陸顯示不一樣的文本

user ? `${user} logged in` : 'not logged in'
複製代碼

最後咱們補充一下上下文

}

    </AuthenticationContext.Consumer>
  )

export default authenticateUser(App)

複製代碼

在前面的示例中,咱們使用高階authenticateUser組件向現有組件添加身份驗證邏輯。而後咱們用一個authenticationcontext.Consumer將user對象注入到組件中。能夠想象,使用許多上下文將致使一個包含許多子zu'jian的大型組件。例如,當咱們想要使用三個上下文時,wrapper hell以下所示:

<AuthenticationContext.Consumer>
  {user => (
    <LanguageContext.Consumer> {language => ( <StatusContext.Consumer> {status => ( ... )} </StatusContext.Consumer> )} </LanguageContext.Consumer>
  )}
</AuthenticationContext.Consumer>

複製代碼

這不是很容易閱讀和修改,並且若是咱們之後須要更改某些內容,它也容易出錯。此外,若是咱們查看一個大型組件樹,其中許多組件只是充當wrapper,這種傳統方式使調試變得困難。

React Hook基於React基本原理,Hook試圖經過使用現有的JavaScript特性來封裝狀態管理。所以,咱們再也不須要學習和理解專門的React特性;咱們能夠簡單地利用現有的JavaScript知識來使用Hook。

咱們可使用Hook解決前面提到的全部問題。咱們再也不須要使用類組件,由於Hook只是能夠在函數組件中調用的函數。咱們也再也不須要爲上下文使用高階組件和渲染props,由於咱們能夠簡單地使用Hook上下文來獲取所需的數據。此外,Hook容許咱們在組件之間重用有狀態邏輯,而無需建立高階組件。

例如,前面提到的生命週期方法的問題可使用Hook來解決:

function Example ({ name }) {
  useEffect(() => {
    fetch(`http://my.api/${this.props.name}`)
    .then(...)
}, [ name ])
// ...
}
複製代碼

這裏實現的效果爲Hook將在組件掛載時以及prop更改時自動觸發。此外,前面提到的wrapper hell也可使用Hook解決,以下所示

const user = useContext(AuthenticationContext)
const language = useContext(LanguageContext)
const status = useContext(StatusContext)
複製代碼

如今咱們知道了Hook能夠解決哪些問題,讓咱們開始使用吧。

Hook的基本使用:

React中組件能夠大致分爲類組件和函數組件,在React中若是須要更改一個組件狀態的時候,那麼這個組件必須是類組件,那麼可否讓函數組件擁有類組件的功能?這時候咱們就須要使用Hook讓咱們函數組件擁有了相似組件的特性。Hook是React16.8中新增得功能,他們容許咱們在不編寫類的狀況下使用狀態和其餘React功能。Hook又提供了一種寫組件的方法,使編寫一個組件更簡單更方便,同時能夠自定義hook把公共的邏輯提取出來,讓邏輯在多個組件之間共享。

咱們從一個請求數據的代碼示例demo開始切入:

import React, { useState } from 'react';
import "./Welcome.scss";

function Welcome() {
  const [data, setData] = useState({ hits: [{
    objectID:"001",
    url:"https://www.jd.com/",
    title:"JD"
  }] });

  return (
    <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul>
  );
}
export default Welcome;
複製代碼

該組件是一個項目列表,初始化的data和狀態更新函數來自useState這個Hook,經過調用useState,來建立App組件的內部狀態。初始狀態是一個object,其中的hits爲一個空數組。若是咱們要添加調用後端數據,咱們可使用axios來發起請求,一樣也可使用fetch。

function Welcome() {
  const [data, setData] = useState({ hits: [{
    objectID:"001",
    url:"https://www.jd.com/",
    title:"JD"
  }] });

  useEffect(async () => {
    const result = await axios(
      'http://localhost/api/v1/search?query=redux'
    );
    setData(result.data);
  });

  return (
    <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul>
  );
}
複製代碼

在useEffect中,咱們請求了後端的數據,還經過調用setData來更新了本地的狀態,這樣會觸發界面的更新。可是,運行這個程序的時候,會出現無限循環的狀況。假設咱們只但願在組件mount時請求數據,那麼咱們能夠傳遞一個空數組做爲useEffect的第二個參數,這樣就能避免在組件更新時執行useEffect,只會在組件mount時執行。useEffect的第二個參數可用於定義其依賴的全部變量,若是其中一個變量發生變化,則useEffect會再次運行,若是包含變量的數組爲空,則在更新組件時useEffect不會再執行,由於它不會監放任何變量的變動。

function Welcome() {
  const [data, setData] = useState({ hits: [{
    objectID:"001",
    url:"https://www.jd.com/",
    title:"JD"
  }] });

  useEffect(async () => {
    const result = await axios(
      'http://localhost/api/v1/search?query=redux'
    );
    setData(result.data);
  },[]);

  return (
    <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul>
  );
}
複製代碼

demo2

在代碼中,咱們使用async / await從第三方API獲取數據,因爲每一個async函數都會默認返回一個隱式的promise。可是,useEffect不但願返回任何內容,這就是爲何不能直接在useEffect中使用async函數,所以,咱們能夠不直接調用async函數,而是像下面這樣:

useEffect(() => {
    const fetchData = async () => {
      const result = await axios(
        'http://localhost/api/v1/search?query=redux',
      );

      setData(result.data);
    };

    fetchData();
  }, []);
複製代碼

在useEffect中,咱們能夠把請求數據前將loading置爲true,在請求完成後,將loading置爲false.

loading處理完成後,還須要處理錯誤,這裏的邏輯是同樣的,使用useState來建立一個新的state,而後在useEffect中特定的位置來更新這個state。因爲咱們使用了async/await,可使用一個try-catch, 每次useEffect執行時,將會重置error;在出現錯誤的時候,將error置爲true;在正常請求完成後,將error置爲false。javascript

function Welcome() {
  const [data, setData] = useState({ hits: [] });
  const [query, setQuery] = useState('redux');
  const [url, setUrl] = useState(
    'http://localhost/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);
      try {
        const result = await axios(url);

        setData(result.data);
      } catch (error) {
        setIsError(true);
      }
      setIsLoading(false);
    };

    fetchData();
  }, [url]);

}
複製代碼

Hook是能夠在函數組件中調用的函數。咱們也再也不須要爲上下文使用高階組件和傳統的class的方式,由於咱們能夠簡單地使用Hook上下文來獲取所需的數據。此外,hook容許咱們在組件之間重用有狀態的邏輯,而無需建立高階組件。咱們來簡單看一下Hook提供的其餘方法:

方法名 用法 示例 思考
useRef 該方法返回一個可變的ref對象,其中.current屬性初始化爲傳遞的參數initialValue import { useRef } from 'react'; const refContainer = useRef(initialValue) useRef用於處理對React中的元素和組件的引用。咱們能夠經過將ref屬性傳遞給元素或組件來設置引用。
useReducer 這個是useState的替代方案,其工做方式與Redux庫相似 import { useReducer } from 'react';
const [ state, dispatch ] = useReducer(reducer, initialArg, init)
useReducer經常使用於處理複雜的狀態邏輯。
useMemo Memoization是一種優化技術,它緩存函數調用的結果,useMemo容許咱們計算一個值並將其記錄下來 import { useMemo } from 'react';
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
當咱們但願避免從新執行費時的操做時,useMemo對於性能優化很是有用。
useCallback 這個方法容許咱們傳遞一個內聯回調函數和一組依賴項,並將返回回調函數的記憶版本。 import { useCallback } from 'react'; const memoizedCallback = useCallback(() => {doSomething(a, b) }, [a, b]) 當將回調函數傳遞給子組件時,useCallback很是有用。它的工做方式相似於useMemo,但用於回調函數。
useLayoutEffect useLayoutEffect與useffect相同,但它只在全部的文檔對象模型(Document Object Model,DOM)改變以後才觸發。 import { useLayoutEffect } from 'react'; useLayoutEffect(didUpdate) useLayoutEffect可用於從DOM讀取信息。(最好使用useffect,useLayoutEffect將阻止試圖更新並減慢應用程序的渲染速度)
useDebugValue useDebugValue可用於在建立自定義Hook時在React DevTools中顯示標籤。 import { useDebugValue } from 'react'; useDebugValue(value) 在自定義Hook中可使用useDebugValue來顯示Hook的當前狀態,這樣能夠更容易地調試組件。

除了React官方提供的全部語法糖以外,社區已經發布了不少庫。這些庫還提供了一些方法,咱們能夠看一下其中很是受歡迎的幾個:

useInput

useInput用於輕鬆實現輸入處理,並將輸入字段的狀態與變量同步。它能夠以下使用:

import { useInput } from 'react-hookedup'
function App () {
  const { value, onChange } = useInput('')
  return <input value={value} onChange={onChange} />
}
複製代碼

如咱們所見,useInput極大地簡化了React中輸入字段的處理。

useResource

useResource可用於經過應用程序中的請求實現異步數據加載。咱們可使用它以下

import { useResource } from 'react-request-hook'
  const [profile, getProfile] = useResource(id => ({ url: `/user/${id}`,
  method: 'GET'
})
複製代碼

如咱們所見,使用useResource來處理獲取數據功能是很是簡單的。

Navigation Hooks

Navigation是Navi庫的一部分,用於經過React中的Hook實現路由功能。Navi庫提供了更多與路由相關的Hook。咱們可使用它們以下

import { useCurrentRoute, useNavigation } from 'react-navi'
const { views, url, data, status } = useCurrentRoute()
const { navigate } = useNavigation()
複製代碼

Navigation Hooks使得路由更容易處理。

Life cycle Hooks

react hookedup庫提供各類Hooks,包括react的全部生命週期偵聽器。(請注意,在使用Hook進行開發時,不建議考慮組件的生命週期。這些鉤子只是提供了一種將現有組件重構爲Hook的方法。)在這裏,咱們列出了其中的兩個,以下所示

import { useOnMount, useOnUnmount } from 'react-hookedup'
useOnMount(() => { ... })
useOnUnmount(() => { ... })
複製代碼

react hookedup能夠直接替換類組件中的生命週期方法。

Timer Hooks

react hookedup庫還爲setInterval和setTimeout提供了方法。這些工做方式相似於直接調用setTimeout或setInterval。但做爲一個React Hook,它將在從新渲染的實惠保持執行,若是咱們在函數組件中直接定義計時器而不使用Hook,那麼每次組件從新渲染時,咱們都將重置計時器。咱們能夠將時間以毫秒爲單位做爲第二個參數傳遞。咱們能夠以下使用:

import { useInterval, useTimeout } from 'react-hookedup'
useInterval(() => { ... }, 1000)
useTimeout(() => { ... }, 1000)
複製代碼

實現一個Hook

在實現一個Hook前咱們先來深刻了解State Hook吧,咱們先從State Hook如何在內部工做開始,咱們將本身從新實現它。接下來,咱們將瞭解鉤子的一些侷限性,以及它們存在的緣由。而後,咱們將瞭解可能的替代Hook api及其相關問題。最後,咱們將學習如何解決因爲Hook的限制而致使的常見問題。最後,咱們將探討一下如何使用Hook來實現React中的有狀態函數組件。

咱們將須要ReactDOM,以便在useState Hook的從新實現中渲染組件。若是咱們使用實際的React Hook,這將在內部處理。

import React from 'react'
import ReactDOM from 'react-dom'
複製代碼

如今,咱們定義本身的useState函數。useState函數將initialState做爲參數:

function useState (initialState) {
複製代碼

而後,咱們定義一個值,在其中存儲咱們的狀態。首先,該值將設置爲initialState,該值做爲參數傳遞給函數:

let value = initialState
複製代碼

接下來,咱們定義setState函數,在該函數中,咱們將把值設置爲不一樣的值,並渲染咱們的MyName組件

function setState (nextValue) {
    value = nextValue
    ReactDOM.render(<MyName />,
    document.getElementById('root'))
  }
複製代碼

最後,咱們將value和setState函數做爲數組返回:

return [ value, setState ]
}
複製代碼

咱們使用數組而不是對象的緣由是,咱們一般但願重命名value和setState變量。使用數組能夠方便地經過解構重命名變量。

const [ name, setName ] = useState('')
複製代碼

咱們的Hook函數使用閉包來存儲當前值。閉包是變量存在和存儲的環境。在咱們的例子中,函數提供閉包,value變量存儲在閉包中。setState函數也在同一個閉包中定義,這就是爲何咱們能夠訪問該函數中的value變量。在useState函數以外,除非從函數返回value變量,不然沒法直接訪問該value變量。那麼咱們實現的簡單Hook有什麼問題呢?

若是如今運行咱們的Hook demo,咱們會注意到當咱們的組件從新渲染時,狀態被重置。這是因爲在每次呈現組件時都從新初始化value變量,這是由於每次渲染組件時都調用useState方法。接下來,咱們將使用一個全局變量來解決這個問題,而後將value放到一個數組,而後咱們定義多個Hook。正如咱們所瞭解到的,該value存儲在useState函數定義的閉包中。每次組件從新提交時,閉包都會從新初始化,這意味着咱們的value將被重置。要解決這個問題,咱們須要將值存儲在函數外部的全局變量中。這樣,值變量將位於函數外部的閉包中,這意味着當再次調用函數時,閉包將不會從新初始化。咱們能夠定義全局變量以下:

首先,咱們在useState函數定義上方添加如下一行

let value
function useState (initialState) {
複製代碼

而後,用如下代碼替換函數中的第一行

if (typeof value === 'undefined') value = initialState
複製代碼

如今,咱們的useState函數使用全局值變量,而不是在它的閉包中定義值變量,所以當函數再次被調用時,它不會被從新初始化。

咱們的Hook功能是可使用的,可是,若是咱們想添加另外一個hook,咱們會遇到另外一個問題:全部Hook都寫入同一個全局值變量,讓咱們經過在組件中添加第二個Hook來仔細研究這個問題。

假設咱們要添加lastName狀態,以下所示:

咱們首先在當前Hook以後建立一個新的Hook,

const [ name, setName ] = useState('')
const [ lastName, setLastName ] = useState('')
複製代碼

而後,咱們定義另外一個handleChange函數

function handleLastNameChange (evt) {
  setLastName(evt.target.value)
}
複製代碼

接下來,咱們將lastName變量放在名字後面:

<h1>My name is: {name} {lastName}</h1>
複製代碼

最後,咱們添加另外一個input輸入框:

<input type="text" value={lastName} onChange= 
{handleLastNameChange}
/>
複製代碼

當咱們這樣寫時,咱們會注意到咱們從新實現的Hook函數對兩個狀態使用相同的值,所以咱們老是同時更改兩個字段。爲了實現多個Hook,而不是隻有一個全局變量,咱們應該有一個存放Hook的數組。咱們如今要將value變量重構爲value數組,以即可以定義多個Hook。

咱們刪除如下代碼行

let value
複製代碼

替換爲如下代碼段

let values = []
let currentHook = 0
複製代碼

而後,編輯useState函數的第一行,咱們如今在其中初始化values數組的currentHook索引處的值:

if (typeof values[currentHook] === 'undefined')
values[currentHook] = initialState
複製代碼

咱們還須要更新setter函數,以便更新相應的狀態值。在這裏,咱們須要將currentHook值存儲在一個單獨的hookIndex變量中,由於currentHook值稍後會更改。這能夠確保在useState函數的閉包中建立currentHook變量的副本。不然,useState函數將從外部閉包訪問currentHook變量,該閉包在每次調用useState時都會被修改。

let hookIndex = currentHook
function setState (nextValue) {
  values[hookIndex] = nextValue
  ReactDOM.render(<MyName />,
  document.getElementById('root'))
}

複製代碼

編輯useState函數的最後一行,以下所示

return [ values[currentHook++], setState ]

複製代碼

使用values[currentHook++],咱們將currentHook的當前值做爲索引傳遞給values數組,而後將currentHook增長1。這意味着從函數返回後currentHook將增長。在開始渲染組件時,仍須要重置currentHook計數器。在組件定義以後添加如下:

function Name () {
  currentHook = 0
複製代碼

最後,咱們簡單地從新實現useState Hook。如咱們所見,使用全局數組存儲Hook state解決了咱們在定義多個Hook時遇到的問題。咱們若是想添加一個複選框來切換first name字段的使用呢?

首先,咱們添加一個新的Hook來存儲複選框的狀態:

const [ enableFirstName, setEnableFirstName ] = useState(false)
複製代碼

而後,咱們定義一個處理函數

function handleEnableChange (evt) {
  setEnableFirstName(!enableFirstName)
}
複製代碼

接下來,咱們渲染一個複選框

<input type="checkbox" value={enableFirstName} onChange= {handleEnableChange} />
複製代碼

添加對enableFirstName變量的檢查

<h1>My name is: {enableFirstName ? name : ''} {lastName}
</h1>
複製代碼

咱們是否能夠將Hook定義放入if條件或三元表達式中,就像咱們在下面的代碼片斷中同樣?

const [ name, setName ] = enableFirstName ? useState('')
:  [ '', () => {} ]
複製代碼

最新版本的react-scripts在定義條件Hooks時實際上會拋出一個錯誤,所以咱們須要經過運行如下命令來降級本例中的庫:

>  npm install --save react-scripts@^2.1.8
複製代碼

在這裏,若是名字被禁用,咱們會返回初始狀態和一個空的setter函數,這樣編輯輸入字段就不起做用。咱們會注意到編輯last name仍然有效,可是編輯first name 不起做用,在下面的截圖中咱們能夠看到,如今只能編輯 last name。

react

當咱們單擊複選框時程序會執行如下操做:

  1. 複選框已選中
  2. 啓用name輸入字段
  3. last name字段的值如今是first name字段的值

咱們能夠在如下屏幕截圖中看到單擊複選框的結果:

react

咱們能夠看到 last name狀態如今在first name字段中。這些值已經交換,由於Hook的順序很重要。正如咱們從實現中瞭解到的,咱們使用currentHook索引來知道每一個Hook的狀態存儲在哪裏。可是,當咱們在兩個現有的Hook之間插入一個附加Hook時,順序就會混亂。

在選中複選框以前,values數組以下所示:

  • [false, '']
  • Hook: enableFirstName, lastName

而後,咱們在lastName字段中輸入了一些文本:

  • [false, 'Hook']
  • Hook: enableFirstName, lastName

接下來,咱們切換了複選框,它激活了咱們的新Hook

  • [true, 'Hook', '']
  • Hook order: enableFirstName, name, lastName

如咱們所見,在兩個現有Hook之間插入一個新Hook會使name hook獲取下一個Hook(lastName)的狀態,由於它如今具備與lastName鉤子之前相同的索引。如今,lastName Hook沒有值,這致使它設置初始值爲空字符串。所以,切換複選框會將lastName字段的值放入name字段。

Hook-react的真正實現

咱們簡單的Hook實現已經讓咱們瞭解了Hook是如何在內部工做的。然而,hook不使用全局變量。相反,它們將狀態存儲在React component中。它們也在內部處理Hook計數器,所以咱們不須要手動重置函數組件中的計數。此外,當狀態發生變化時,真正的Hook會自動觸發component的從新渲染。然而,要作到這一點,須要從React函數組件調用Hook。不能在React外部或React class組件內部調用React Hook。咱們應該始終在函數組件的開頭定義Hook,而且永遠不要將它們嵌套在if或其餘構造函數中。咱們應該在React函數內部調用React hook組件,React hook不能有條件地定義,也不能在循環中定義。

那麼,咱們如何實現條件的Hook呢?咱們能夠定義Hook並在須要時使用它,而不是使Hook成爲條件的Hook。咱們能夠從新分組咱們的組件。解決有條件的hook的另外一種方法是將一個組件拆分爲多個組件,而後有條件地渲染這些組件。例如,假設咱們但願在用戶登陸後從數據庫中獲取用戶信息。

咱們不能執行如下操做,由於使用if條件能夠更改Hook的順序

function UserInfo ({ username }) {
  if (username) {
    const info = useFetchUserInfo(username)
    return <div>{info}</div>
  }
  return <div>Not logged in</div>
}

複製代碼

咱們必須爲用戶登陸時建立一個單獨的組件,以下所示:

function LoggedInUserInfo ({ username }) { const info = useFetchUserInfo(username) 
  return <div>{info}</div>
}

function UserInfo ({ username }) {
  if (username) {
    return <LoggedInUserInfo username={username} />
  }
  return <div>Not logged in</div>
}
複製代碼

對非登陸和登陸狀態使用兩個獨立的組件是有意義的,由於咱們但願每一個組件都有一個單一的功能的。至於循環中的Hook,咱們可使用包含數組的單個狀態Hook,也能夠拆分組件。例如,假設咱們想顯示全部在線用戶。

咱們可使用數組包含全部用戶數據,以下所示:

function OnlineUsers ({ users }) {
  const [ userInfos, setUserInfos ] = useState([])
  // ... fetch & keep userInfos up to date ...
  return ( <div> {users.map(username => { const user = userInfos.find(u => u.username === username) return <UserInfo {...user} /> })} </div>
  )
}
複製代碼

然而,這多是有問題的。例如,咱們可能不想經過OnlineUsers組件更新全部用戶狀態,由於咱們必須從數組中選擇須要修改的用戶的狀態,而後修改數組。更好的解決方案是在UserInfo組件中使用Hook。這樣,咱們就可使每一個用戶的狀態保持最新,而沒必要處理數組邏輯:

function OnlineUsers ({ users }) {
  return (
    <div> {users.map(username => <UserInfo username={username} />)} </div>
  )
}
function UserInfo ({ username }) {
  const info = useFetchUserInfo(username)
  // ... keep user info up to date ...
複製代碼

如咱們所見,爲每一個功能模塊使用一個單獨的組件能夠保持代碼的簡單和簡潔,同時也避免了React Hook的限制。以上咱們首先從新實現useState函數,使用全局狀態和閉包。而後咱們瞭解到,爲了實現多個Hook,咱們須要使用狀態數組來代替。然而,經過使用狀態數組,咱們必須在函數調用之間保持hook順序的一致性。這個限制使得咱們不能使用條件中的Hook和循環中的Hook。而後,咱們瞭解了Hook的可能替代方案。那麼真正的react是怎麼實現Hook的呢,咱們來看一段react官方的源碼:

react react

在 React 中,實現方式卻有一些差別的。React 中是經過相似單鏈表的形式來代替數組的。(以下圖所示)咱們知道,react 會生成一棵組件樹(或Fiber 單鏈表)[Fiber 本質上是一個虛擬的堆棧幀,新的調度器會按照優先級自由調度這些幀,從而將以前的同步渲染改爲了異步渲染,在不影響體驗的狀況下去分段計算更新。],樹中每一個節點對應了一個組件。hooks 的數據就做爲組件的一個信息,存儲在這些節點上,伴隨組件一塊兒出生,一塊兒死亡。memoizedState 數組是按hook定義的順序來放置數據的,若是 hook 順序變化,memoizedState 並不會感知到。咱們只能在函數最外層調用 Hook自定義的共享同一個memoizedState,共享同一個順序。每一次從新渲染的時候,都是從新去執行函數組件了,對於以前已經執行過的函數組件,並不會作任何操做。

react

type Hooks = {
  memoizedState: any, // 指向當前渲染節點 Fiber
  baseState: any, // 初始化 initialState, 已經每次 dispatch 以後 newState
  baseUpdate: Update<any> | null,// 當前須要更新的 Update ,每次更新完以後,會賦值上 一個 
  update,方便 react 在渲染錯誤的邊緣,數據回溯
  queue: UpdateQueue<any> | null,// UpdateQueue 經過
  next: Hook | null, // link 到下一個 hooks,經過 next 串聯每一 hooks
}
type Effect = {
  tag: HookEffectTag, // effectTag 標記當前 hook 做用在 life-cycles 的哪個階段
  create: () => mixed, // 初始化 callback
  destroy: (() => mixed) | null, // 卸載 callback
  deps: Array<mixed> | null,
  next: Effect, // 同上
};
複製代碼

Hook函數組件在第一次渲染時和再次渲染時的實現是不一樣的,組件所調用的 Hook 實際上指向的是不一樣的 Hook。函數組件在第一次渲染時所使用的 Hook 指向的是對應的 mountXXX,而在更新時,Hook 指向的是對應的 updateXXX,以下圖所示:

react

Class 和 Hook對比

從生命週期上看

react

咱們來對比彙總一個表格

class 組件 Hooks 組件
constructor useState
getDerivedStateFromProps useState 裏面 update 函數
shouldComponentUpdate useMemo
render 函數自己
componentDidMount useEffect
componentDidUpdate useEffect
componentWillUnmount useEffect 裏面返回的函數
componentDidCatch
getDerivedStateFromError

從編碼上看

class 組件 Hooks 組件
代碼邏輯清晰(構造函數、componentDidMount等) 須要配合註釋和變量名
不容易內存泄漏 容易發生內存泄漏

總結-問題思考:

  • React 是如何把對 Hook 的調用和組件聯繫起來的。

Hook 本質就是 JavaScript 函數,不要在循環,條件或嵌套函數中調用 Hook, 確保老是在你的 React 函數的最頂層調用他們。

  • React 怎麼知道哪一個 state 對應哪一個 useState?

React 靠的是 Hook 調用的順序。只要 Hook 的調用順序在屢次渲染之間保持一致,React 就能正確地將內部 state 和對應的 Hook 進行關聯。

  • Hook 會由於在渲染時建立函數而變慢嗎

不會。在現代瀏覽器中,閉包和類的原始性能只有在極端場景下才會有明顯的差異。除此以外,能夠認爲 Hook 的設計在某些方面更加高效:Hook 避免了 class 須要的額外開支,像是建立類實例和在構造函數中綁定事件處理器的成本。符合語言習慣的代碼在使用 Hook 時不須要很深的組件樹嵌套。這個現象在使用高階組件、render props、和 context 的代碼庫中很是廣泛。組件樹小了,React 的工做量也隨之減小。

  • 使用useMemo ?

demo7

這行代碼會調用 computeExpensiveValue(a, b)。但若是依賴數組 [a, b] 自上次賦值以來沒有改變過,useMemo 會跳過二次調用,只是簡單複用它上一次返回的值。能夠把 useMemo 做爲一種性能優化的手段,但不要把它當作一種語義上的保證。將來,React 可能會選擇「忘掉」一些以前記住的值並在下一次渲染時從新計算它們,好比爲離屏組件釋放內存。建議本身編寫相關代碼以便沒有 useMemo 也能正常工做 —— 而後把它加入性能優化。

  • 如何實現 shouldComponentUpdate

demo7

能夠用 React.memo 包裹一個組件來對它的 props 進行淺比較。這不是一個 Hook 由於它的寫法和 Hook 不一樣。React.memo 等效於 PureComponent,但它只比較 props。

  • effect 的依賴頻繁變化該怎麼處理?

demo7

傳入空的依賴數組 [],意味着該 hook 只在組件掛載時運行一次,並不是從新渲染時。但如此會有問題,在 setInterval 的回調中,count 的值不會發生變化。由於當 effect 執行時,咱們會建立一個閉包,並將 count 的值被保存在該閉包當中,且初值爲 0。每隔一秒,回調就會執行 setCount(0 + 1),所以count 永遠不會超過 1。

demo7

指定 [count] 做爲依賴列表就能修復這個 Bug,但會致使每次改變發生時定時器都被重置。事實上,每一個 setInterval 在被清除前(相似於 setTimeout)都會調用一次。但這並非咱們想要的。要解決這個問題,咱們可使用 setState 的函數式更新形式。它容許咱們指定state該如何改變而不用引用當前state。

  • 和DOM的交互

demo7

獲取 DOM 節點的位置或是大小的基本方式是使用 callback ref。每當 ref 被附加到一個另外一個節點,React 就會調用 callback。在這個案例中,咱們沒有選擇使用 useRef,由於當 ref 是一個對象時它並不會把當前 ref 的值的變化通知到咱們。使用 callback ref 能夠確保即使子組件延遲顯示被測量的節點 (好比爲了響應一次點擊),咱們依然可以在父組件接收到相關的信息,以便更新測量結果。

  • 如何獲取上一輪的 props 或 state

demo7

能夠經過 ref 來手動實現,考慮到這是一個相對常見的使用場景,極可能在將來 React 會自帶一個 usePrevious Hook。

  • Hook 可否覆蓋 class 的全部使用場景

官方給 Hook 設定的目標是儘早覆蓋 class 的全部使用場景。目前暫時尚未對應不經常使用的 getSnapshotBeforeUpdate、getDerivedStateFromError 和 componentDidCatch 生命週期的 Hook 等價寫法,但官方計劃儘早把它們加進來。目前 Hook 還處於早期階段,一些第三方的庫可能還暫時沒法兼容 Hook。

  • Hook,class,二者混用?

咱們不能在 class 組件內部使用 Hook,但咱們能夠在組件樹裏混合使用 class 組件和使用了 Hook 的函數組件。不論一個組件是 class 仍是一個使用了 Hook 的函數,都只是這個組件的實現細節而已。長遠來看,官方指望 Hook 可以成爲咱們編寫 React 組件的主要方式。

參考文獻

一、官方文檔css

二、useEffect 完整指南html

三、React 高階組件java

四、簡書 React Hooksreact

五、探索 React 的內在 — Fiber & Algebraic Effectsios

六、使用React.memo優化React 應用數據庫