Hooks

2020-05-12 17:47 更新
自 v1.3.0-beta-0 起支持

Hooks 是一套全新的 API,可以讓你在不編寫類,不使用 state 的情況下使用 Class 的狀態(tài)管理,生命周期等功能。

關(guān)于 Hooks 的概述、動(dòng)機(jī)和規(guī)則,我們強(qiáng)烈建議你閱讀 React 的官方文檔。和其它大部分 React 特性不同,Hooks 沒有 RFC 介紹,相反,所有說明都在文檔中:

本篇文檔只會(huì)介紹在 Taro 中可用的 Hooks API 和部分與 React 不一致的行為,其它內(nèi)容大體的內(nèi)容和 Hooks Reference 相同。

你還可以參考這兩個(gè)使用 Hooks 的 Demo:

  • V2EX,主要展示與服務(wù)器通信
  • TodoMVC,主要展示組件間通信

API

在 Taro 中使用 Hooks API 很簡單,只需要從 @tarojs/taro 中引入即可。

import { useEffect, useLayoutEffect, useReducer, useState, useContext, useRef, useCallback, useMemo } from '@tarojs/taro'

useState

const [state, setState] = useState(initialState);

返回一個(gè) state,以及更新 state 的函數(shù)。

在初始渲染期間,返回的狀態(tài) (state) 與傳入的第一個(gè)參數(shù) (initialState) 值相同。

setState 函數(shù)用于更新 state。它接收一個(gè)新的 state 值并將組件的一次重新渲染加入隊(duì)列。

setState(newState);

在后續(xù)的重新渲染中,useState 返回的第一個(gè)值將始終是更新后最新的 state。

注意Taro 會(huì)確保 setState 函數(shù)的標(biāo)識(shí)是穩(wěn)定的,并且不會(huì)在組件重新渲染時(shí)發(fā)生變化。這就是為什么可以安全地從 useEffect 或 useCallback 的依賴列表中省略 setState。

函數(shù)式更新

如果新的 state 需要通過使用先前的 state 計(jì)算得出,那么可以將函數(shù)傳遞給 setState。該函數(shù)將接收先前的 state,并返回一個(gè)更新后的值。下面的計(jì)數(shù)器組件示例展示了 setState 的兩種用法:

function Counter({initialCount}) {
  const [count, setCount] = useState(initialCount);
  return (
    <View>
      Count: {count}
      <Button onClick={() => setCount(initialCount)}>Reset</Button>
      <Button onClick={() => setCount(prevCount => prevCount + 1)}>+</Button>
      <Button onClick={() => setCount(prevCount => prevCount - 1)}>-</Button>
    </View>
  );
}

“+” 和 “-” 按鈕采用函數(shù)式形式,因?yàn)楸桓碌?state 需要基于之前的 state。但是“重置”按鈕則采用普通形式,因?yàn)樗偸前?count 設(shè)置回初始值。

注意與 class 組件中的 setState 方法不同,useState 不會(huì)自動(dòng)合并更新對象。你可以用函數(shù)式的 setState 結(jié)合展開運(yùn)算符來達(dá)到合并更新對象的效果。setState(prevState => { // 也可以使用 Object.assign return {...prevState, ...updatedValues}; }); useReducer 是另一種可選方案,它更適合用于管理包含多個(gè)子值的 state 對象。

惰性初始 state

initialState 參數(shù)只會(huì)在組件的初始渲染中起作用,后續(xù)渲染時(shí)會(huì)被忽略。如果初始 state 需要通過復(fù)雜計(jì)算獲得,則可以傳入一個(gè)函數(shù),在函數(shù)中計(jì)算并返回初始的 state,此函數(shù)只在初始渲染時(shí)被調(diào)用:

const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

useEffect

useEffect(didUpdate);

該 Hook 接收一個(gè)包含命令式、且可能有副作用代碼的函數(shù)。

在函數(shù)組件主體內(nèi)(這里指在 Taro 渲染或創(chuàng)建數(shù)據(jù)的階段)改變 DOM、添加訂閱、設(shè)置定時(shí)器、記錄日志以及執(zhí)行其他包含副作用的操作都是不被允許的,因?yàn)檫@可能會(huì)產(chǎn)生莫名其妙的 bug 并破壞 UI 的一致性。

使用 useEffect 完成副作用操作。賦值給 useEffect 的函數(shù)會(huì)在組件渲染到屏幕之后執(zhí)行。

默認(rèn)情況下,effect 將在每輪渲染結(jié)束后執(zhí)行,但你可以選擇讓它在只有某些值改變的時(shí)候才執(zhí)行。

清除 effect

通常,組件卸載時(shí)需要清除 effect 創(chuàng)建的諸如訂閱或計(jì)時(shí)器 ID 等資源。要實(shí)現(xiàn)這一點(diǎn),useEffect 函數(shù)需返回一個(gè)清除函數(shù)。以下就是一個(gè)創(chuàng)建訂閱的例子:

useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除訂閱
    subscription.unsubscribe();
  };
});

為防止內(nèi)存泄漏,清除函數(shù)會(huì)在組件卸載前執(zhí)行。另外,如果組件多次渲染(通常如此),則在執(zhí)行下一個(gè) effect 之前,上一個(gè) effect 就已被清除。在上述示例中,意味著組件的每一次更新都會(huì)創(chuàng)建新的訂閱。若想避免每次更新都觸發(fā) effect 的執(zhí)行,請參閱下一小節(jié)。

effect 的執(zhí)行時(shí)機(jī)

與 componentDidMount、componentDidUpdate 不同的是,Taro 會(huì)在 setData 完成之后的下一個(gè) macrotask 執(zhí)行 effect 的回調(diào)函數(shù),傳給 useEffect 的函數(shù)會(huì)延遲調(diào)用。這使得它適用于許多常見的副作用場景,比如如設(shè)置訂閱和事件處理等情況,因此不應(yīng)在函數(shù)中執(zhí)行渲染和更新。

然而,并非所有 effect 都可以被延遲執(zhí)行。例如,在容器執(zhí)行下一次繪制前,用戶可見的 DOM 變更就必須同步執(zhí)行,這樣用戶才不會(huì)感覺到視覺上的不一致。(概念上類似于被動(dòng)監(jiān)聽事件和主動(dòng)監(jiān)聽事件的區(qū)別。)Taro 為此提供了一個(gè)額外的 useLayoutEffect Hook 來處理這類 effect。它和 useEffect 的結(jié)構(gòu)相同,區(qū)別只是調(diào)用時(shí)機(jī)不同。

effect 的條件執(zhí)行

默認(rèn)情況下,effect 會(huì)在每輪組件渲染完成后執(zhí)行。這樣的話,一旦 effect 的依賴發(fā)生變化,它就會(huì)被重新創(chuàng)建。

然而,在某些場景下這么做可能會(huì)矯枉過正。比如,在上一章節(jié)的訂閱示例中,我們不需要在每次組件更新時(shí)都創(chuàng)建新的訂閱,而是僅需要在 source props 改變時(shí)重新創(chuàng)建。

要實(shí)現(xiàn)這一點(diǎn),可以給 useEffect 傳遞第二個(gè)參數(shù),它是 effect 所依賴的值數(shù)組。更新后的示例如下:

useEffect(
  () => {
    const subscription = props.source.subscribe();
    return () => {
      subscription.unsubscribe();
    };
  },
  [props.source],
);

此時(shí),只有當(dāng) props.source 改變后才會(huì)重新創(chuàng)建訂閱。

注意如果你要使用此優(yōu)化方式,請確保數(shù)組中包含了所有外部作用域中會(huì)發(fā)生變化且在 effect 中使用的變量,否則你的代碼會(huì)引用到先前渲染中的舊變量。如果想執(zhí)行只運(yùn)行一次的 effect(僅在組件掛載和卸載時(shí)執(zhí)行),可以傳遞一個(gè)空數(shù)組([])作為第二個(gè)參數(shù)。這就告訴 Taro 你的 effect 不依賴于 props 或 state 中的任何值,所以它永遠(yuǎn)都不需要重復(fù)執(zhí)行。這并不屬于特殊情況 —— 它依然遵循輸入數(shù)組的工作方式。如果你傳入了一個(gè)空數(shù)組([]),effect 內(nèi)部的 props 和 state 就會(huì)一直擁有其初始值。盡管傳入 [] 作為第二個(gè)參數(shù)有點(diǎn)類似于 componentDidMount 和 componentWillUnmount 的思維模式,但我們有 更好的 方式 來避免過于頻繁的重復(fù)調(diào)用 effect。除此之外,請記得 Taro 會(huì)等待渲染完畢之后才會(huì)延遲調(diào)用 useEffect,因此會(huì)使得額外操作很方便。Taro 會(huì)在自帶的 ESLint 中配置 eslint-plugin-react-hooks 中的 exhaustive-deps 規(guī)則。此規(guī)則會(huì)在添加錯(cuò)誤依賴時(shí)發(fā)出警告并給出修復(fù)建議。

useReducer {#usereducer}

const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的替代方案。它接收一個(gè)形如 (state, action) => newState 的 reducer,并返回當(dāng)前的 state 以及與其配套的 dispatch 方法。(如果你熟悉 Redux 的話,就已經(jīng)知道它如何工作了。)

在某些場景下,useReducer 會(huì)比 useState 更適用,例如 state 邏輯較復(fù)雜且包含多個(gè)子值,或者下一個(gè) state 依賴于之前的 state 等。并且,使用 useReducer 還能給那些會(huì)觸發(fā)深更新的組件做性能優(yōu)化,因?yàn)槟憧梢韵蜃咏M件傳遞 dispatch 而不是回調(diào)函數(shù) 。

以下是用 reducer 重寫 useState 一節(jié)的計(jì)數(shù)器示例:

const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter({initialState}) {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <View>
      Count: {state.count}
      <Button onClick={() => dispatch({type: 'increment'})}>+</Button>
      <Button onClick={() => dispatch({type: 'decrement'})}>-</Button>
    </View>
  );
}
注意Taro 會(huì)確保 dispatch 函數(shù)的標(biāo)識(shí)是穩(wěn)定的,并且不會(huì)在組件重新渲染時(shí)改變。這就是為什么可以安全地從 useEffect 或 useCallback 的依賴列表中省略 dispatch。

指定初始 state

有兩種不同初始化 useReducer state 的方式,你可以根據(jù)使用場景選擇其中的一種。將初始 state 作為第二個(gè)參數(shù)傳入 useReducer 是最簡單的方法:

  const [state, dispatch] = useReducer(
    reducer,
    {count: initialCount}
  );
注意Taro 不使用 state = initialState 這一由 Redux 推廣開來的參數(shù)約定。有時(shí)候初始值依賴于 props,因此需要在調(diào)用 Hook 時(shí)指定。如果你特別喜歡上述的參數(shù)約定,可以通過調(diào)用 useReducer(reducer, undefined, reducer) 來模擬 Redux 的行為,但我們不鼓勵(lì)你這么做。

惰性初始化

你可以選擇惰性地創(chuàng)建初始 state。為此,需要將 init 函數(shù)作為 useReducer 的第三個(gè)參數(shù)傳入,這樣初始 state 將被設(shè)置為 init(initialArg)。

這么做可以將用于計(jì)算 state 的邏輯提取到 reducer 外部,這也為將來對重置 state 的 action 做處理提供了便利:

function init(initialCount) {
  return {count: initialCount};
}

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    case 'reset':
      return init(action.payload);
    default:
      throw new Error();
  }
}

function Counter({initialCount}) {
  const [state, dispatch] = useReducer(reducer, initialCount, init);
  return (
    <View>
      Count: {state.count}
      <Button
        onClick={() => dispatch({type: 'reset', payload: initialCount})}>
        Reset
      </Button>
      <Button onClick={() => dispatch({type: 'increment'})}>+</Button>
      <Button onClick={() => dispatch({type: 'decrement'})}>-</Button>
    </View>
  );
}

useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

返回一個(gè) memoized 回調(diào)函數(shù)。

把內(nèi)聯(lián)回調(diào)函數(shù)及依賴項(xiàng)數(shù)組作為參數(shù)傳入 useCallback,它將返回該回調(diào)函數(shù)的 memoized 版本,該回調(diào)函數(shù)僅在某個(gè)依賴項(xiàng)改變時(shí)才會(huì)更新。當(dāng)你把回調(diào)函數(shù)傳遞給經(jīng)過優(yōu)化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子組件時(shí),它將非常有用。

useCallback(fn, deps) 相當(dāng)于 useMemo(() => fn, deps)。

useMemo {#usememo}

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一個(gè) memoized 值。

把“創(chuàng)建”函數(shù)和依賴項(xiàng)數(shù)組作為參數(shù)傳入 useMemo,它僅會(huì)在某個(gè)依賴項(xiàng)改變時(shí)才重新計(jì)算 memoized 值。這種優(yōu)化有助于避免在每次渲染時(shí)都進(jìn)行高開銷的計(jì)算。

記住,傳入 useMemo 的函數(shù)會(huì)在渲染期間執(zhí)行。請不要在這個(gè)函數(shù)內(nèi)部執(zhí)行與渲染無關(guān)的操作,諸如副作用這類的操作屬于 useEffect 的適用范疇,而不是 useMemo。

如果沒有提供依賴項(xiàng)數(shù)組,useMemo 在每次渲染時(shí)都會(huì)計(jì)算新的值。

useRef

const refContainer = useRef(initialValue);

useRef 返回一個(gè)可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)。返回的 ref 對象在組件的整個(gè)生命周期內(nèi)保持不變。

一個(gè)常見的用例便是命令式地訪問子組件:

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已掛載到 DOM 上的文本輸入元素
    inputEl.current.focus();
  };
  return (
    <View>
      <Input ref={inputEl} type="text" />
      <Button onClick={onButtonClick}>Focus the input</Button>
    </View>
  );
}

本質(zhì)上,useRef 就像是可以在其 .current 屬性中保存一個(gè)可變值的“盒子”。

你應(yīng)該熟悉 ref 這一種訪問 DOM 的主要方式。如果你將 ref 對象以 <View ref={myRef} /> Taro 都會(huì)將 ref 對象的 .current 屬性設(shè)置為相應(yīng)的 DOM 節(jié)點(diǎn)。

然而,useRef() 比 ref 屬性更有用。它可以很方便地保存任何可變值,其類似于在 class 中使用實(shí)例字段的方式。

這是因?yàn)樗鼊?chuàng)建的是一個(gè)普通 JavaScript 對象。而 useRef() 和自建一個(gè) {current: ...} 對象的唯一區(qū)別是,useRef 會(huì)在每次渲染時(shí)返回同一個(gè) ref 對象。

請記住,當(dāng) ref 對象內(nèi)容發(fā)生變化時(shí),useRef 并不會(huì)通知你。變更 .current 屬性不會(huì)引發(fā)組件重新渲染。如果想要在 Taro 綁定或解綁 DOM 節(jié)點(diǎn)的 ref 時(shí)運(yùn)行某些代碼,則需要使用回調(diào) ref 來實(shí)現(xiàn)。

useLayoutEffect

其函數(shù)簽名與 useEffect 相同,但它會(huì)在所有的 DOM 變更之后同步調(diào)用 effect。可以使用它來讀取 DOM 布局并同步觸發(fā)重渲染。在瀏覽器執(zhí)行繪制之前,useLayoutEffect 內(nèi)部的更新計(jì)劃將被同步刷新。

盡可能使用標(biāo)準(zhǔn)的 useEffect 以避免阻塞視覺更新。

提示如果你正在將代碼從 class 組件遷移到使用 Hook 的函數(shù)組件,則需要注意 useLayoutEffect 與 componentDidMount、componentDidUpdate 的調(diào)用階段是一樣的。但是,我們推薦你一開始先用 useEffect,只有當(dāng)它出問題的時(shí)再嘗試使用 useLayoutEffect。

useContext

const value = useContext(MyContext)

接收一個(gè) context (Taro.createContext 的返回值)并返回該 context 的當(dāng)前值。當(dāng)前的 context 值由上層組件中最先渲染的 <MyContext.Provider value={value}> 的 value決定。

當(dāng)組件上層最近的 <MyContext.Provider> 更新時(shí),該 Hook 會(huì)觸發(fā)重渲染,并使用最新傳遞給 MyContext provider 的 context value 值。

別忘記 useContext 的參數(shù)必須是 context 對象本身:

正確: useContext(MyContext) 錯(cuò)誤: useContext(MyContext.Consumer) 錯(cuò)誤: useContext(MyContext.Provider) 調(diào)用了 useContext 的組件總會(huì)在 context 值變化時(shí)重新渲染。

如果你在接觸 Hook 前已經(jīng)對 context API 比較熟悉,那應(yīng)該可以理解,useContext(MyContext) 相當(dāng)于 class 組件中的 static contextType = MyContext 或者 。 useContext(MyContext) 只是讓你能夠讀取 context 的值以及訂閱 context 的變化。你仍然需要在上層組件樹中使用 來為下層組件提供 context。

useDidShow

自 1.3.14 開始支持
useDidShow(() => {
  console.log('componentDidShow')
})

useDidShow 是 Taro 專有的 Hook,等同于 componentDidShow 頁面生命周期鉤子

useDidHide

自 1.3.14 開始支持
useDidHide(() => {
  console.log('componentDidHide')
})

useDidHide 是 Taro 專有的 Hook,等同于 componentDidHide 頁面生命周期鉤子

usePullDownRefresh

自 1.3.14 開始支持
usePullDownRefresh(() => {
  console.log('onPullDownRefresh')
})

usePullDownRefresh 是 Taro 專有的 Hook,等同于 onPullDownRefresh 頁面生命周期鉤子

useReachBottom

自 1.3.14 開始支持
useReachBottom(() => {
  console.log('onReachBottom')
})

useReachBottom 是 Taro 專有的 Hook,等同于 onReachBottom 頁面生命周期鉤子

usePageScroll

自 1.3.14 開始支持
usePageScroll(res => {
  console.log(res.scrollTop)
})

usePageScroll 是 Taro 專有的 Hook,等同于 onPageScroll 頁面生命周期鉤子

useResize

自 1.3.14 開始支持
useResize(res => {
  console.log(res.size.windowWidth)
  console.log(res.size.windowHeight)
})

useResize 是 Taro 專有的 Hook,等同于 onResize 頁面生命周期鉤子

useShareAppMessage

自 1.3.14 開始支持
useShareAppMessage(res => {
  if (res.from === 'button') {
    // 來自頁面內(nèi)轉(zhuǎn)發(fā)按鈕
    console.log(res.target)
  }
  return {
    title: '自定義轉(zhuǎn)發(fā)標(biāo)題',
    path: '/page/user?id=123'
  }
})

useShareAppMessage 是 Taro 專有的 Hook,等同于 onShareAppMessage 頁面生命周期鉤子

useTabItemTap

自 1.3.14 開始支持
useTabItemTap(item => {
  console.log(item.index)
  console.log(item.pagePath)
  console.log(item.text)
})

useTabItemTap 是 Taro 專有的 Hook,等同于 onTabItemTap 頁面生命周期鉤子

useRouter

自 1.3.14 開始支持
const router = useRouter() // { path: '', params: { ... } }

useRouter 是 Taro 專有的 Hook,等同于頁面為類時(shí)的 this.$router

useScope

自 1.3.20 開始支持
const scope = useScope()

useScope 是 Taro 專有的 Hook,等同于頁面為類時(shí)的 this.$scope

頁面及組件中相關(guān)屬性設(shè)置

在 Taro 中,你可以為頁面及組件設(shè)置一些屬性來達(dá)到一些特殊的目的,例如 config 設(shè)置配置等等,在前面章節(jié)你已經(jīng)學(xué)會(huì)如何在類中進(jìn)行相關(guān)設(shè)置,同樣的,使用 Hooks 時(shí)你也可以進(jìn)行相關(guān)設(shè)置來達(dá)到和使用類一樣的效果。

不同于使用類的寫法,使用 Hooks 時(shí),你需要將 config 或 options 等配置直接掛載在 Hooks 函數(shù)上,即可以達(dá)到想要的效果,例如

為頁面設(shè)置 config

export default function Index () {
  return <View></View>
}

Index.config = {
  navigationBarTitleText: '首頁'
}

為組件設(shè)置 options

export default function Com () {
  return <View></View>
}

Com.options = {
  addGlobalClass: true
}


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)