Hooks

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

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

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

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

你還可以參考這兩個使用 Hooks 的 Demo:

  • V2EX,主要展示與服務器通信
  • 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);

返回一個 state,以及更新 state 的函數。

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

setState 函數用于更新 state。它接收一個新的 state 值并將組件的一次重新渲染加入隊列。

setState(newState);

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

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

函數式更新

如果新的 state 需要通過使用先前的 state 計算得出,那么可以將函數傳遞給 setState。該函數將接收先前的 state,并返回一個更新后的值。下面的計數器組件示例展示了 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>
  );
}

“+” 和 “-” 按鈕采用函數式形式,因為被更新的 state 需要基于之前的 state。但是“重置”按鈕則采用普通形式,因為它總是把 count 設置回初始值。

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

惰性初始 state

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

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

useEffect

useEffect(didUpdate);

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

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

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

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

清除 effect

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

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

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

effect 的執(zhí)行時機

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

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

effect 的條件執(zhí)行

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

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

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

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

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

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

useReducer {#usereducer}

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

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

在某些場景下,useReducer 會比 useState 更適用,例如 state 邏輯較復雜且包含多個子值,或者下一個 state 依賴于之前的 state 等。并且,使用 useReducer 還能給那些會觸發(fā)深更新的組件做性能優(yōu)化,因為你可以向子組件傳遞 dispatch 而不是回調函數 。

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

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 會確保 dispatch 函數的標識是穩(wěn)定的,并且不會在組件重新渲染時改變。這就是為什么可以安全地從 useEffect 或 useCallback 的依賴列表中省略 dispatch。

指定初始 state

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

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

惰性初始化

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

這么做可以將用于計算 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],
);

返回一個 memoized 回調函數。

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

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

useMemo {#usememo}

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

返回一個 memoized 值。

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

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

如果沒有提供依賴項數組,useMemo 在每次渲染時都會計算新的值。

useRef

const refContainer = useRef(initialValue);

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

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

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>
  );
}

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

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

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

這是因為它創(chuàng)建的是一個普通 JavaScript 對象。而 useRef() 和自建一個 {current: ...} 對象的唯一區(qū)別是,useRef 會在每次渲染時返回同一個 ref 對象。

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

useLayoutEffect

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

盡可能使用標準的 useEffect 以避免阻塞視覺更新。

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

useContext

const value = useContext(MyContext)

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

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

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

正確: useContext(MyContext) 錯誤: useContext(MyContext.Consumer) 錯誤: useContext(MyContext.Provider) 調用了 useContext 的組件總會在 context 值變化時重新渲染。

如果你在接觸 Hook 前已經對 context API 比較熟悉,那應該可以理解,useContext(MyContext) 相當于 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') {
    // 來自頁面內轉發(fā)按鈕
    console.log(res.target)
  }
  return {
    title: '自定義轉發(fā)標題',
    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,等同于頁面為類時的 this.$router

useScope

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

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

頁面及組件中相關屬性設置

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

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

為頁面設置 config

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

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

為組件設置 options

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

Com.options = {
  addGlobalClass: true
}


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號