React Hooks

React Hooks

Hook是React v16.8的新特性,可以用函數(shù)的形式代替原來的繼承類的形式,可以在不編寫class的情況下使用state以及其他React特性

React 設(shè)計原理

  • React認為,UI視圖是數(shù)據(jù)的一種視覺映射,UI = F(Data),這里的F主要負責對輸入數(shù)據(jù)進行加工,對數(shù)據(jù)變更做出相應
  • 公式里的F在React里抽象成組件,React是以組件為粒度編排應用的,組件是代碼復用的最小單元
  • 在設(shè)計上,React采用props屬性來接收外部數(shù)據(jù),使用state屬性來管理組件自身產(chǎn)生的數(shù)據(jù),而為了實現(xiàn)(運行時)對數(shù)據(jù)變更做出相應需要,React采用基于類的組件設(shè)計
  • 除此之外,React認為組件是有生命周期的,因此提供了一系列API供開發(fā)者使用

我們所熟悉的React組件長這樣

import React, { Component } from "react";
// React基于Class設(shè)計組件
export default class Button extends Component {
    constructor() {
        super();
        // 組件自身數(shù)據(jù)
        this.state = { buttonText: "Click me, please" };
        this.handleClick = this.handleClick.bind(this);
    }
    // 響應數(shù)據(jù)變更
    handleClick() {
        this.setState({ buttonText: "Thanks, been clicked!" });
    }
    // 編排數(shù)據(jù)呈現(xiàn)UI
    render() {
        const { buttonText } = this.state;
        return <button onClick={this.handleClick}>{buttonText}</button>;
    }
}

組件類的缺點

上面實例代碼只是一個按鈕組件,但是可以看到,它的代碼已經(jīng)很重了。真實的React App由多個類按照層級,一層層構(gòu)成,復雜度成倍增長。再加入 Redux + React Router,就變得更復雜

很可能隨便一個組件最后export出去就是醬紫的:

export default withStyle(style)(connect(/*something*/)(withRouter(MyComponent)))

一個4層嵌套HOC,嵌套地獄

同時,如果你的組件內(nèi)事件多,那么你的constructor就是醬紫的

class MyComponent extends React.Component {
  constructor() {
    // initiallize
    this.handler1 = this.handler1.bind(this)
    this.handler2 = this.handler2.bind(this)
    this.handler3 = this.handler3.bind(this)
    this.handler4 = this.handler4.bind(this)
    this.handler5 = this.handler5.bind(this)
    // ...more
  }
}

而Function Component編譯后就是一個普通的function,function對js引擎是友好的,而Class Component在React內(nèi)部是當做Javascript Function類來處理的,代碼很難被壓縮,比如方法名稱

還有this啦,稍微不注意就會出現(xiàn)因this指向報錯的問題等。。。

總結(jié)一下就是:

  • 很難復用邏輯,會導致組件樹層級很深
  • 會產(chǎn)生巨大的組件(很多代碼必須寫在類里面)
  • 類組件很難理解,比如方法需要bind,this的指向不明確
  • 編譯size,性能問題

Hooks

State Hook

Hook是什么?
可以先通過一個例子來看看,在class中,我們通過在構(gòu)造函數(shù)中設(shè)置this.state初始化組件的state:

this.state = {
    n: 0
}

而在函數(shù)組件中,我們沒有this,所以我們不能分配或讀取this.state,但是可以在組件中調(diào)用useStateHook

import React, {useState} from 'react';
function xxx() {
    const [n, setN] = useState(0);
}

在上面代碼中,useState就是Hook

Hook是一個特殊的函數(shù),它可以讓你“鉤入”React的特性。例如useState是允許你在React函數(shù)組件中添加state的Hook。
如果你在編寫函數(shù)組件并意識到需要向其添加一些state,以前的做法是必須將其轉(zhuǎn)化為Class。現(xiàn)在你可以在現(xiàn)有的函數(shù)組件中使用Hook
讓函數(shù)組件自身具備狀態(tài)處理能力,且自身能夠通過某種機制觸發(fā)狀態(tài)的變更并引起re-render,這種機制就是Hooks

走進useState

示例代碼:

import React, { useState } from 'react';

function App() {
    // 聲明一個叫 "n" 的 state 變量
    // useState接收一個參數(shù)作為初始值
    // useState返回一個數(shù)組,[state, setState]
    const [n, setN] = useState(0);

    return (
        <div>
        {/* 讀取n,等同于this.state.n */}
        <p>{n}</p>
        {/* 通過setN更新n,等同于this.setN(n: this.state.n + 1) */}
        <button onClick={() => setN(n + 1)}>
            +1
        </button>
        </div>
    );
}

運行一下(代碼1

  1. 首次渲染 render <App />
  2. 調(diào)用App函數(shù),得到虛擬DOM對象,創(chuàng)建真實DOM
  3. 點擊buttno調(diào)用setN(n + 1),因為要更新頁面的n,所以再次render<App />
  4. 重復第二步,從控制臺打印看出每次執(zhí)行setN都會觸發(fā)App函數(shù)運行,得到一個新的虛擬DOM,DOM Diff更新真實DOM

那么問題來了,首次運行App函數(shù)和setN時都調(diào)用了App,兩次運行useState是一樣的嗎?setN改變n的值了嗎?為什么得到了不一樣的n,useState的時候做了什么?

分析:

  • setN
    • setN一定會修改數(shù)據(jù)x,將n+1存入x
    • setN一定會觸發(fā)<App />重新渲染(re-render)
  • useState
    • useState肯定會從x讀取n的最新值
  • x
    • 每個組件都有自己的數(shù)據(jù)x,我們將其命名為state

嘗試實現(xiàn)React.useState(代碼2

// 和useState一樣,myUseState接收一個初始值,返回state和setState方法
const myUseState = initialValue => {
    let state = initialValue
    const setState = newValue => {
        state = newValue
        // 重新渲染
        render()
    }
    return [state, setState]
}

const render = () => {
    // 雞賊暴力渲染法
    ReactDOM.render(<App />, rootElement)
}

function App() {
    const [n, setN] = myUseState(0)
    ...
}

點擊button,n沒有任何變化
原來每次state都變成了初始值0,因為myUseState會將state重置
我們需要一個不會被myUseState重置的變量,那么這個變量只要聲明在myUseState外面即可

let _state;
const myUseState = initialValue => {
    // 如果state是undefined,則賦給初始值,否則就賦值為保存在外面的_state
    _state = _state === undefined ? initialValue : _state;
    const setState = newValue => {
        _state = newValue;
        render();
    };
    return [_state, setState];
};

還有問題,如果一個組件有倆state咋整?由于所有數(shù)據(jù)都放在_state,產(chǎn)生沖突:

function App() {
    const [n, setN] = myUseState(0)
    const [m, setM] = myUseState(0)
    ...
}

解決:

  • 把_state做成對象
    • 不可行,沒有key,useState(0)只傳入了一個參數(shù)0,并不知道是n還是m
  • 把_state做成數(shù)組
    • 可行,_state = [0, 0]
let _state = [];
let index = 0;
const myUseState = (initialValue) => {
    const currentIndex = index;
    _state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
    const setState = (newValue) => {
        _state[currentIndex] = newValue;
        render();
    };
    index += 1;
    return [_state[currentIndex], setState];
};

const render = () => {
    // 重新渲染要重置index
    index = 0;
    ReactDOM.render(<App />, rootElement);
};

解決了存在多個state的情況,但是還有問題,就是useState調(diào)用順序必須一致!

  • 如果第一次渲染時n是第一個,m是第二個,k是第三個
  • 則第二次渲染時必須保證順序一致,因為數(shù)組根據(jù)調(diào)用順序存儲值
  • re-render時會從第一行代碼開始重新執(zhí)行整個組件
  • 所以React不允許出現(xiàn)如下代碼

React Hook "useState" is called conditionally. React Hooks must be called in the exact same order in every component render.

只在最頂層使用 Hook

最后一個問題:
App用了_state和index,那其他組件用什么?放在全局作用域重名怎么解決?

運行App后,React會維護一個虛擬DOM樹,每個節(jié)點都有一個虛擬DOM對象(Fiber),將_state,index存儲在對象上

額外擴展一下Fiber對象,它的數(shù)據(jù)結(jié)構(gòu)如下:

function FiberNode(
    tag: WorkTag,
    pendingProps: mixed,
    key: null | string,
    mode: TypeOfMode,
    ) {
    // Instance 實例
    this.tag = tag;
    this.key = key;
    // JSX翻譯過來之后是React.createElement,他最終返回的是一個ReactElement對象
    // 就是ReactElement的`?typeof`
    this.elementType = null;
    // 就是ReactElement的type,他的值就是<MyClassComponent />這個class,不是實例,實例是在render過程中創(chuàng)建
    this.type = null;
    this.stateNode = null;

    // Fiber
    this.return = null;
    this.child = null;
    this.sibling = null;
    this.index = 0;

    this.ref = null;

    this.pendingProps = pendingProps;
    this.memoizedProps = null;
    this.updateQueue = null;
    // 用來存儲state
    // 記錄useState應該返回的結(jié)果
    this.memoizedState = null;
    this.firstContextDependency = null;

    // ...others
}

總結(jié):

  • 每個函數(shù)組件對應一個React節(jié)點(FiberNode)
  • 每個節(jié)點保存著_state(memorizedState)和index(實際是鏈表)
  • useState會讀取對應節(jié)點的state[index]
  • index是由useState的調(diào)用順序決定
  • setState會修改_state,并觸發(fā)更新

搞清楚useState干了啥以后,回過頭再看setN改變n了嗎,為什么得到了不一樣的n代碼3

  • 先+1,后log => 1
  • 先log,后+1 => 0
  • 為什么log出了舊數(shù)據(jù)

分析:

  • 先點擊log,log(0)三秒后執(zhí)行,此時n0,n不會變
  • 再點擊+1,此時調(diào)用的是一個新的函數(shù),生成了新的n,re-render
  • n=0n=1同時存在內(nèi)存中

結(jié)論:因為有多個n,setN并不會改變n,React函數(shù)式編程決定了n的值不會被改變,只會被回收

注意事項:

  • 不可局部更新(代碼4
  • 地址要變:setState(obj)如果obj地址不變,那么React就認為數(shù)據(jù)沒有變化
  • useState接受函數(shù):函數(shù)返回初始state,且只執(zhí)行一次
  • setState接收函數(shù):setN(i => i + 1),優(yōu)先使用這種形式

useReducer

React本身不提供狀態(tài)管理功能,通常需要使用外部庫,最常用的庫是Redux
Redux的核心概念是,將需要修改的state都存入到store里,發(fā)起一個action用來描述發(fā)生了什么,用reducers描述action如何改變state,真正能改變store中數(shù)據(jù)的是store.dispatch API
Reducer是一個純函數(shù),只承擔計算 State 的功能,函數(shù)的形式是(state, action) => newState
Action是消息的載體,只能被別人操作,自己不能進行任何操作
useReducer()鉤子用來引入Reducer功能(代碼5

const [state, dispatch] = useReducer(reducer, initial)

上面是useReducer基本用法

  • 接受Reducer函數(shù)和一個初始值作為參數(shù)
  • 返回一個數(shù)組,數(shù)組[0]位是狀態(tài)當前值,第[1]位是dispatch函數(shù),用來發(fā)送action

似曾相識的感覺

const [n, setN] = useState(0)
//   n:讀
//   setN:寫

總的來說useReducer就是復雜版本的useState,那么什么時候使用useReducer,什么時候又使用useState呢?
看一個代碼6
當你需要維護多個state,那么為什么不用一個對象來維護呢,對象是可以合并的

需要注意的是,由于Hooks可以提供狀態(tài)管理和Reducer函數(shù),所以在這方面可以取代Redux。但是,它沒法兒提供中間件(midddleware)和時間旅行(time travel),如果你需要這兩個功能,還是要用Redux。

中間件原理:封裝改造store.dispatch,將其指向中間件,以實現(xiàn)在dispatch和reducer之間處理action數(shù)據(jù)的邏輯,也可以將中間件看成是dispatch方法的封裝器

有沒有代替Redux的方法呢?

Reducer + Context

useContext

什么是上下文?

  • 全局變量是全局的上下文
  • 上下文是局部的全局變量

使用方法:

// 創(chuàng)建上下文
const c = createContext(null)

function App() {
    const [n, setN] = useState(0)
    return (
        // 使用<c.Provider>圈定作用域
        <c.Provider value={n, setN}>
            <Father />
        </ c.Provider>
    )
}

function Father() {
    return (
        <div>我是爸爸
            <Son />
        </div>
    )
}

function Son() {
    // 在作用域中使用useContext(c)來獲取并使用上下文
    // 要注意這里useContext返回的是對象,不是數(shù)組
    const {n, setN} = useContext(c)
    const onClick = () => {
        setN( i => i + 1)
    }
    return (
        <div>我是兒子,我可以拿到n:{n}
            <button onClick={onClick}>我也可以更新n</button>
        </div>
        
    )
}

注意事項:

  • 使用useContext時,在一個模塊改變數(shù)據(jù),另一個模塊是感知不到的
  • setN會重新渲染<App />,自上而下逐級通知更新,并不是響應式,因為響應式是監(jiān)聽數(shù)據(jù)變化通知對應組件進行更新

useEffect

useEffect鉤子會在每次render后運行
React保證了每次運行useEffect的同時,DOM 都已經(jīng)更新完畢

應用:

  • 作為componentDidMount使用,[]作第二個參數(shù)
  • 作為componentDidUpdate使用,可指定依賴
  • 作為componentWillUnmount使用,通過return
  • 以上三種可同時存在
function App() {
    const [n, setN] = useState(0)
    const onClick = () => {
        setN(i => i + 1)
    }

    const afterRender = useEffect;
    // componentDidMount
    useEffect(() => {
        console.log('第一次渲染之后執(zhí)行這句話')
    }, [])
    // componentDidUpdate
    useEffect(() => {
        console.log('每次次都會執(zhí)行這句話')
    })

    useEffect(() => {
        console.log('n變化就會執(zhí)行這句話,包含第一次')
    }, [n])
    // componentWillUnmount
    useEffect(() => {
        const id = setInterval(() => {
            console.log('每一秒都打印這句話')
        }, 1000)
        return () =>{
            // 如果組件多次渲染,則在執(zhí)行下一個 effect 之前,上一個 effect 就已被清除
            console.log('當組件要掛掉了,打印這句話')
            window.clearInterval(id)
        }
    }, [])
    return (
        <div>
            n: {n}
            <button onClick={onClick}>+1</button>
        </div>
    )
}

Hook 允許我們按照代碼的用途分離他們,而不是像生命周期函數(shù)那樣
React將按照effect聲明的順序依次調(diào)用組件中的每一個effect

對應的,另一個effect鉤子,useLayoutEffect

  • useEffect在瀏覽器渲染之后執(zhí)行,useLayoutEffect在渲染前執(zhí)行(代碼7
  • useLayoutEffect在渲染前執(zhí)行,使用它來讀取 DOM 布局并同步觸發(fā)重渲染
// 偽代碼
App() -> 執(zhí)行 -> VDOM -> DOM -> useLayoutEffect -> render -> useEffect

特點:

  • useLayoutEffect性能更好,但是會影響用戶看到頁面變化的時間(代碼7
  • useLayoutEffect總是比useEffect先執(zhí)行
  • useLayoutEffect里的任務最好是影響了layout
  • 還是推薦優(yōu)先使用useEffect(如果不涉及操作dom的操作)

為什么建議將修改DOM的操作放到useLayoutEffect里,而不是useEffect呢,是因為當DOM被修改時,瀏覽器的線程處于被阻塞階段(js線程和瀏覽器線程互斥),所以還沒有發(fā)生回流、重繪。由于內(nèi)存中的DOM已經(jīng)被修改,通過useLayoutEffect可以拿到最新的DOM節(jié)點,并且在此時對DOM進行樣式上的修改。這樣修改一次性渲染到屏幕,依舊只有一次回流、重繪的代價。

注意:
由于useEffect是在render之后執(zhí)行,瀏覽器完成布局和繪制后,不應在函數(shù)中執(zhí)行阻塞瀏覽器更新屏幕的操作

useMemo

React默認有多余的render(修改n,但是依賴m的組件卻自動刷新了),如果props不變就沒有必要再執(zhí)行一次函數(shù)組件,先從一個例子來理解memo(代碼8

這里有一個問題,如果給子組件一個方法,即使prop沒有變化,子組件還是會每一次都執(zhí)行

const onClickChild = () => {}

<Child data={m} onClick={onClickChild} />

這是因為在App重新渲染時,生成了新的函數(shù),就像一開始講的多個n的道理一樣,新舊函數(shù)雖然功能一樣,但是地址不一樣,這就導致props還是變化了

那么對于子組件的方法,如何重用?
使用useMemo鉤子(代碼9)

const onClickChild = useMemo(() => {
    return () => {
        console.log(m)
    }
}, [m])

特點:

  • useMemo第一個參數(shù)是() => value(value可以是函數(shù)、對象之類的),第二個參數(shù)是依賴數(shù)組[m]
  • 只有當依賴變化時,才會重新計算新的value
  • 如果依賴沒有變化,就重用之前的value
  • 這不就是vue中的computed嗎?

注意:

  • 如果你的value是個函數(shù),那么你要寫成useMemo(() => x => console.log(x))
  • 這是一個返回函數(shù)的函數(shù)
  • 這么難用的話,用用useCallback
// useMemo
const onClickChild = useMemo(() => {
    return () => {
        console.log(m)
    }
}, [m])

// useCallback
const onClickChild = useCallback(() => {
    console.log(m)
})

// 偽代碼
useCallback(x => log(x), [m]) 等價于 useMemo(() => x => log(x), [m])

useMemouseCallback作用完全一樣,語法糖而已

useRef

一直用到的這個例子,每點擊一下就會重新渲染一下App

function App() {
    console.log('App 執(zhí)行');
    const [n, setN] = useState(0)
    const onClick = () => {
        setN(i => i + 1)
    }

    return (
        <div>
            <button onClick={onClick}>update n {n}</button>
        </div>
    )
}

假如我要知道這個App執(zhí)行了多少次,我怎么記錄?
如果我需要一個值,在組件不斷render的時候也能夠保持不變怎么做?

function App() {
    // count的值通過useRef記錄了下來
    // 初始化
    const count = useRef(0)

    useEffect(() => {
        // 讀取 count.current
        count.current += 1
    })
}

同樣的,useRef也是通過它所對應的fiberNode對象來保存

為什么需要current?

  • 為了保證兩次useRef是同一個值,只有引用才能做到
  • useRef存儲的實際上是一個對象{currnt: 0},對象對應的是同一個地址(內(nèi)存)
  • 每次改變只是改變對象中的值,而不是改變對象,新舊組件必須引用同一個對象

講了useRef就不得不講講forwardRef

在函數(shù)組件中怎么使用ref,嘗試一下(代碼10

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

說明,props無法傳遞ref屬性
所以,函數(shù)組件用ref的話,需要用forwardRef包裝做一下轉(zhuǎn)發(fā),才能拿到ref

自定義Hook

通過自定義Hook,可以將組件邏輯提取到可重用的函數(shù)中
自定義Hook是一個函數(shù),其名稱以 “use” 開頭(符合 Hook 的規(guī)則),函數(shù)內(nèi)部可以調(diào)用其他的Hook
每次使用自定義 Hook 時,其中的所有 state 和副作用都是完全隔離的(每次調(diào)用 Hook,它都會獲取獨立的 state)
代碼

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 這篇文檔將通過一個 demo,介紹 React 幾個 Hook 的使用方法和場景。包括: useState use...
    Evelynzzz閱讀 1,097評論 0 2
  • useState 1.基本使用 等價于 2. 復雜的state 3.使用狀態(tài) 4. 注意事項 1). 如果stat...
    sweetBoy_9126閱讀 3,233評論 0 6
  • 對應的代碼倉庫:https://codesandbox.io/s/learning-react-hooks-7ss...
    井潤閱讀 1,820評論 0 0
  • React Hooks 原理[https://github.com/brickspert/blog/issues/...
    Yong_bcf4閱讀 891評論 0 1
  • 推薦指數(shù): 6.0 書籍主旨關(guān)鍵詞:特權(quán)、焦點、注意力、語言聯(lián)想、情景聯(lián)想 觀點: 1.統(tǒng)計學現(xiàn)在叫數(shù)據(jù)分析,社會...
    Jenaral閱讀 5,950評論 0 5

友情鏈接更多精彩內(nèi)容