React Hooks

React Hooks

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

React 設計原理

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

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

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

組件類的缺點

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

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

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

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

同時,如果你的組件內事件多,那么你的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內部是當做Javascript Function類來處理的,代碼很難被壓縮,比如方法名稱

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

總結一下就是:

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

Hooks

State Hook

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

this.state = {
    n: 0
}

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

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

在上面代碼中,useState就是Hook

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

走進useState

示例代碼:

import React, { useState } from 'react';

function App() {
    // 聲明一個叫 "n" 的 state 變量
    // useState接收一個參數作為初始值
    // useState返回一個數組,[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. 調用App函數,得到虛擬DOM對象,創建真實DOM
  3. 點擊buttno調用setN(n + 1),因為要更新頁面的n,所以再次render<App />
  4. 重復第二步,從控制臺打印看出每次執行setN都會觸發App函數運行,得到一個新的虛擬DOM,DOM Diff更新真實DOM

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

分析:

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

嘗試實現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咋整?由于所有數據都放在_state,產生沖突:

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

解決:

  • 把_state做成對象
    • 不可行,沒有key,useState(0)只傳入了一個參數0,并不知道是n還是m
  • 把_state做成數組
    • 可行,_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調用順序必須一致!

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

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樹,每個節點都有一個虛擬DOM對象(Fiber),將_state,index存儲在對象上

額外擴展一下Fiber對象,它的數據結構如下:

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過程中創建
    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應該返回的結果
    this.memoizedState = null;
    this.firstContextDependency = null;

    // ...others
}

總結:

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

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

  • 先+1,后log => 1
  • 先log,后+1 => 0
  • 為什么log出了舊數據

分析:

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

結論:因為有多個nsetN并不會改變n,React函數式編程決定了n的值不會被改變,只會被回收

注意事項:

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

useReducer

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

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

上面是useReducer基本用法

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

似曾相識的感覺

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

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

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

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

有沒有代替Redux的方法呢?

Reducer + Context

useContext

什么是上下文?

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

使用方法:

// 創建上下文
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返回的是對象,不是數組
    const {n, setN} = useContext(c)
    const onClick = () => {
        setN( i => i + 1)
    }
    return (
        <div>我是兒子,我可以拿到n:{n}
            <button onClick={onClick}>我也可以更新n</button>
        </div>
        
    )
}

注意事項:

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

useEffect

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

應用:

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

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

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

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

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

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

特點:

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

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

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

useMemo

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

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

const onClickChild = () => {}

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

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

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

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

特點:

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

注意:

  • 如果你的value是個函數,那么你要寫成useMemo(() => x => console.log(x))
  • 這是一個返回函數的函數
  • 這么難用的話,用用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 執行');
    const [n, setN] = useState(0)
    const onClick = () => {
        setN(i => i + 1)
    }

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

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

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

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

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

為什么需要current?

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

講了useRef就不得不講講forwardRef

在函數組件中怎么使用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屬性
所以,函數組件用ref的話,需要用forwardRef包裝做一下轉發,才能拿到ref

自定義Hook

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

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容