使用 React Hooks 重構你的小程序

做者:餘澈html

背景

一直關注小程序開發的朋友應該會注意到,最開始小程序就是爲了微型創新型業務打造的一個框架,最多隻能運行 1m 的包。但是後來發現不少廠商把愈來愈多的業務搬到了小程序上,小程序的能力也在不斷地開放,變得愈來愈強大。因而後來打包限制上升到了 2m,而後引入了分包,如今已經已經能夠上傳 8m 的小程序。其實這個體積已經能夠實現很是巨型很是複雜的業務了。就從 Taro 的用戶來看,例如京東購物小程序和 58 同城小程序無論從代碼的數量仍是複雜度都不亞於 PC 端業務,因此咱們能夠說前端開發的複雜度正在向小程序端轉移。前端

而小程序開發其實也是前端開發的一個子集,在整個前端業界,咱們又是怎麼解決複雜度這個問題的呢?vue

首先咱們看看 React:React Core Team 成員,同時也是 Redux 的做者 Dan Abramov 在 2018 年的 ReactConf 向你們首次介紹了 React Hooks。React Hooks 是爲了解決 Class Component 的一些問題而引入的:react

  • Class Component 組件間的邏輯難以複用。由於 JavaScript 不像 Go 或 C++ 同樣,Class 能夠多重繼承,類的邏輯的複用就成了一個問題;
  • 複雜組件難以理解。Class Component 常常會在生命週期作一些數據獲取事件監聽的反作用函數,這樣的狀況下咱們就很難把組件拆分爲更小的力度;
  • Class 使人迷惑。不少新手應該會被 Class 組件綁定事件的 this 迷惑過,綁定事件能夠用 bind,能夠直接寫箭頭函數,也能夠寫類屬性函數,但到底哪一種方法纔是最好的呢?而到了 ES 2018,class 還有多種語法,例如裝飾器,例如 private fileds 這些奇奇怪怪的語法也爲新手增長了更多的困惑。

而對於 Vue 而言也有相同的問題,Vue 的做者尤雨溪老師在 VueConf China 2019 也給 Vue 3.0 引入了一個叫 Functional-based API 的概念,它是受 React Hooks 啓發而增長的新 API。因爲 Vue 2.0 組件組合的模式是對象字面量形式,因此 Functional-based API 能夠做爲 Mixins 的替代,配合新的響應式 API 做爲新的組件組合模式。那麼對於 Vue 3.0 咱們還知之甚少,之後的 API 也有可能改變,但或許是英雄所見略同,React 和 Vue 對於下降前端開發複雜度這一問題都不約而同地選擇了 Hooks 這一方案,這究竟是爲何呢?vue-cli

why_hooks.png

咱們能夠一下以前的組件組合方案,首先是 Mixins,紅色圈的 Mixins,黃色的是組件,咱們知道 Mixins 其實就是把多個對象組合成一個對象,Mixins 的過程就有點像調用 Object.assgin 方法。那 Mixins 有什麼問題呢?首先是命名空間耦合,若是多個對象同名參數,這些參數就會耦合在一塊兒;其次因爲 Mixins 必須是運行時才能知道具體有什麼參數,因此是 TypeScript 是沒法作靜態檢查的;第三是組件參數不清晰,在 Mixins 中組件的 props 和其餘參數沒什麼兩樣,很容易被其它的 Mixins 覆蓋掉。typescript

爲了解決 Mixins 的問題,後來發展出了高階組件(HOC)的方式,高階組件就和圖裏同樣,一個組件嵌套着另外的組件。它的確解決了 Mixins 的一些問題,例如命名空間解耦,因爲每次都會生成新組件,就不存在命名空間問題了;其次它也能很好地作靜態檢查;但它依然沒有辦法處理組件 props 的問題,props 仍是有可能會在高階組件中被更改;並且它還有了新的問題,每多用一次高階組件,都會多出一個組件實例。redux

最後咱們來看一下 Hooks,紫色的圈圈是 Hooks,就像圖裏同樣,Hooks 都在同一個組件裏,Hooks 之間還能夠相互調用。由於 Hooks 跑在一個普通函數式組件裏,因此他確定是沒有命名空間的問題,同時 TypeScript 也能對普通函數作很好的靜態檢查,並且 Hooks 也不能更改組件的 Props,傳入的是啥最後可用的就是啥;最後 Hooks 也不會生成新的組件,因此他是單組件實例。小程序

taroxhooks.png

在 Taro 1.3 版本,咱們實現了一大堆特性,其中的重頭戲就是 React Hooks 的支持。雖然 React Hooks 正式穩定的時間並不長,但咱們認爲這個特性能有效地簡化開發模式,提高開發效率和開發體驗。即使 Hooks 的生態和最佳實踐還還沒有完善,但咱們相信將來 Hooks 會成爲 React 開發模式的主流,也會深入地影響其它框架將來的 API 構成。因此在 Taro 的規劃中咱們也把 Hooks 放在了很重要的位置。後端

什麼是 Hooks?

相信筆者扯了那麼多,你們對 Hooks 應該產生了一些興趣,那什麼是 Hooks 呢?簡單來講,Hooks 就是一組在 React 組件中運行的函數,讓你在不編寫 Class 的狀況下使用 state 及其它特性。具體來講,Hooks 能夠表現爲如下的形式:數組

useState 與內部狀態

咱們能夠看一個原生小程序的簡單案例,一個簡單計數器組件,點擊按鈕就 + 1,相信每位前端開發朋友均可以輕鬆地寫一個計數器組件。但咱們能夠稍微改一下代碼,把事件處理函數改成箭頭函數。若是是這樣代碼就跑不了了。事實上在原生開發中 this 的問題是一以貫之的,因此咱們常常要開個新變量把 this 緩存起來,叫作 self 什麼的來避免相似的問題。咱們以前也提到過,若是採用 ES6 的 Class 來組織組件一樣也會遇到 this 指向不清晰的問題。

Page({
  data: {
    count: 0
  },
  increment: () => { // 這裏寫箭頭函數就跑不了了
    this.setData({
      count: this.data.count + 1
    })
  }
})
複製代碼

再來看看咱們的 hooks 寫法,咱們引入了一個叫 useState 的函數,它接受一個初始值參數,返回一個元組,若是是寫後端的同窗應該對這個模式比較熟悉,就像 Koa 或者 Go 同樣,一個函數返回兩個值或者說叫一個元組,不過咱們返回的第一個參數是當前的狀態,一個是設置這個狀態的函數,每次調用這個設置狀態的 setState 函數都會使得整個組件被從新渲染。而後用 ES6 的結構語法把它倆解構出來使用。

而後咱們在定義一個增長的函數,把他綁定到 onClick 事件上。

function Counter () {
  // 返回一個值和一個設置值的函數 
  // 每次設置值以後會從新渲染組件
  const [ count, setCount ] = useState(0)

  function increment () {
    setCount(count + 1)
  }

  return (
    <View> <Text>You clicked {count} times</Text> <Button onClick={increment}> Click me </Button> </View>
  )
}
複製代碼

一樣是很是簡單的代碼。若是你熟悉 Taro 以前的版本的話就會知道這樣的代碼在之前的 Taro 是跑不了的,不過 Taro 1.3 以後事件傳參能夠傳入任何合法值,你若是想直接寫箭頭函數或者寫一個柯里化的函數也是徹底沒有問題的。

你們能夠發現咱們使用的 Hooks 就是一個很是簡單很是 normal 的函數,沒有 this 沒有 class,沒有類構造函數,沒有了 this,不再會出現那種 thisself 傻傻分不清楚的狀況。

你們能夠記住這個簡單的計數器組件,之後以後講的不少案例是基於這個組件作的。

useEffect 與反作用

接下來咱們看一個稍微複雜一些的例子,一個倒計時組件,咱們點擊按鈕就開始倒計時,再點擊就中止倒計時。在咱們這個組件裏有兩個變量,start 用於控制是否開始計時,time 就是咱們的倒計時時間。這裏注意咱們須要屢次清除 interval,而在現實業務開發中,這個 touchStart 函數可能會複雜得多,一不當心就會提早清除 interval 或忘記清除。

Page({
  data: {
    time: 60
  },
  start: false,
  toggleStart () {
    this.start = !this.start
    if (this.start) {
      this.interval = setInterval(() => {
        this.setData({
          time: this.data.time - 1
        })
      }, 1000)
    } else {
      clearInterval(this.interval)
    }
  },
  onUnload () {
    clearInterval(this.interval)
  }
})
複製代碼
<view>
  <button bindtap="toggleStart">
    {{time}} 
  </button>
</view>
複製代碼

而咱們 Hooks 的例子會是這樣:咱們引入了一個 useEffect 函數。以前咱們提到過,每次調用 useState 返回的 setState 函數都會從新調用整個函數,其實就包括了 useEffect 函數,useEffect 接受兩個參數。第一個就是反作用,也就是 effect 函數,他不接受也不返回任何參數。 第二個參數是依賴數組,當數組中的變量變化時就會調用,第一個參數 effect 函數。 Effect 函數還能夠返回一個函數,這個函數在下一次 effect 函數被調用時或每次組件被註銷時或者就會調用,咱們能夠在這裏清楚掉一些事件的訂閱或者 interval 之類可能會致使內存泄露的行爲。 在咱們這個例子中,當 start 每次變化就會從新跑一次 effect 函數,每隔一秒會設置一次 time 的值讓它減一,但這樣的寫法是有問題的。

function Counter () {
  const [ start, setStart ] = useState(false)
  const [ time, setTime ] = useState(60)
  
  useEffect(() => { // effect 函數,不接受也不返回任何參數
    let interval
    if (start) {
      interval = setInterval(() => {
        // setTime(time - 1) ❌ time 在 effect 閉包函數裏是拿不到準確值的
        setTime(t => t -1) // ✅ 在 setTime 的回調函數參數裏能夠拿到對應 state 的最新值
      }, 1000)
    }
    return () => clearInterval(interval) // clean-up 函數,當前組件被註銷時調用
  }, [ start ]) // 依賴數組,當數組中變量變化時會調用 effect 函數
  
  return (
    <View> <Button onClick={() => setStart(!start)}>{time}</Button> </View>
  )
}
複製代碼

由於咱們在 setInterval 這個函數的閉包中,咱們捕捉到 time 這個變量的值不能和最新的值對應得上,time 的值有可能在咱們意料以外地被更改了屢次。解決的方案也很簡單,以前咱們提到過 useState 返回的 setState 方法,能夠接受一個函數做爲參數,而這個函數的參數,就是 state 最新的值,因此只要咱們傳入一個函數就行了。這是其中一種方法。

還有另外一種方法是使用 useRef Hooks,useRef 能夠返回一個可變的引用,它會生成一個對象,對象裏這個有 current 屬性,而 current 的值是可變的。在咱們這個例子裏,每次更改 currentTime.current 都是同步的,並且 currentTime 是一個引用,因此 currentTime.current 必定是可控的。

function Counter () {
  const [ start, setStart ] = useState(false)
  const [ time, setTime ] = useState(60)
  const currentTime = useRef(time) // 生成一個可變引用
  
  useEffect(() => { // effect 函數,不接受也不返回任何參數
    let interval
    if (start) {
      interval = setInterval(() => {
        setTime(currentTime.current--) // currentTime.current 是可變的
      }, 1000)
    }
    return () => clearInterval(interval) // clean-up 函數,當前組件被註銷時調用
  }, [ start ]) // 依賴數組,當數組中變量變化時會調用 effect 函數
  
  return (
    <View> <Button onClick={() => setStart(!start)}>{time}</Button> </View>
  )
}
複製代碼

雖說咱們能夠 useRef 來解決這個問題,可是不必這樣作。由於 setTime 傳遞一個回調函數的方法顯然可讀性更高。真正有必要的是把咱們的 interval 變量做爲一個 ref,咱們在函數最頂層的做用域把 interval 做爲一個 ref,這樣咱們就能夠在這個函數的任何一個地方把他清除,而原來的代碼中咱們把 interval 做爲一個普通的變量放在 effect 函數裏,這樣若是咱們有一個事件也須要清除 interval,這就無法作到了。可是用 useRef 生成可變引用就沒有這個限制。

function Counter () {
  const [ start, setStart ] = useState(false)
  const [ time, setTime ] = useState(60)
  const interval = useRef() // interval 能夠在這個做用域裏任何地方清除和設置
  
  useEffect(() => { // effect 函數,不接受也不返回任何參數
    if (start) {
      interval.current = setInterval(() => {
        setTime(t => t - 1) // ✅ 在 setTime 的回調函數參數裏能夠拿到對應 state 的最新值
      }, 1000)
    }
    return () => clearInterval(interval.current) // clean-up 函數,當前組件被註銷時調用
  }, [ start ]) // 依賴數組,當數組中變量變化時會調用 effect 函數
  
  return (
    <View> <Button onClick={() => setStart(!start)}>{time}</Button> </View>
  )
}
複製代碼

useContext 與跨組件通訊

接下來咱們再來看一個跨組件通訊的例子,例如咱們有三個組件,page 組件有一個 child 組件,child 組件有一個 counter 組件,而咱們 counter 組件的 count 值和 setCount 函數,是由 page 組件傳遞下來的。這種狀況在一個複雜業務的開發中也常常能遇到,在原生小程序開發中咱們應該怎麼作呢?

咱們須要手動的把咱們 counter 的值和函數手動地依次地傳遞下去,而這樣的傳遞必須是顯式的,你須要在 JavaScript 中設置 props 的參數,也須要在 WXML 裏設置 props 的參數,一個也不能少,少了就跑不動。咱們還注意到即使 child 組件沒有任何業務邏輯,他也必需要設置一個 triggerEvent 的函數和 props 的類型聲明。這樣的寫法無疑是很是麻煩並且限制很大的。

<!-- page.wxml -->

<view>
  <child />
</view>

<!-- child.wxml -->
<view>
  <counter />
</view>

<!-- counter.wxml -->
<view>
  <text>
    You clicked {{count}} times
  </text>
  <butto bindtap="increment">
    Click me
  </button>
</view>
複製代碼
// page.js
Page({
  data: {
    count: 0
  },
  increment () {
    this.setData({
      count: this.data.count + 1
    })
  }
})

// child.js
Component({
  properties: {
    count: Number
  },
  methods: {
    increment () {
      this.triggerEvent('increment')
    }
  }
})

// counter.js
Component({
  properties: {
    count: Number
  },
  methods: {
    increment () {
      this.triggerEvent('increment')
    }
  }
})
複製代碼

而咱們能夠看看 Hooks 的寫法,首先咱們用 Taro.createContext 建立一個 context 對象,在咱們 page 組件裏把咱們的 countsetCount 函數做爲一個對象傳入到 Context.Providervalue 裏。而後在咱們的 Counter 組件,咱們可使用 useContext 這個 Hooks 把咱們的 countsetCount 取出來,就直接可使用了。

export const CounterContext = Taro.createContext(null);

// page.js
const Page = () => {
  const [ count, setCount ] = useState(0)

  return (
    <CounterContext.Provider value={{ count, setCount }}> <Child /> </CounterContext.Provider> ); }; // child.js const Child = () => ( <View> <Counter /> </View> ); // counter.js const Counter = () => { const { count, setCount } = useContext(CounterContext) return ( <View> <Text> You clicked {count} times </Text> <Button onClick={() => setCount(count + 1)}> Click me </Button> </View> ) } 複製代碼

你們能夠發現使用 Context 的代碼比原來的代碼精簡了不少,參數不須要一級一級地顯式傳遞,child 組件也和事實同樣,沒有一行多餘的邏輯。但精簡不是最大的好處。最大的好處是你們能夠發現咱們的 Context 能夠傳遞一個複雜的對象,熟悉小程序原生開發的同窗可能會知道,全部 props 的傳遞都會被小程序序列化掉,若是傳遞了一個複雜的對象最終會變成一個 JSON。可是用 Taro 的 context 則沒有這層限制,你能夠傳入一個帶有函數的對象,也能夠傳入像是 imutabale 或者 obserable 這樣複雜的對象。在 taro 1.3 咱們對 props 系統進行了一次重構,Taro 的 context 和 props 同樣,屬性傳遞沒有任何限制,想傳啥就傳啥。

另一個值得注意的點的是,context 的傳遞能夠無視父級組件的更新策略,在這個例子中即使咱們經過 shouldComponentUpdate() 禁止了 child 組件的更新,但 counter 做爲它的子組件依然是能夠更新的。這個特性可讓咱們作性能優化的時候更爲靈活一些。

Hooks 在小程序實戰

講完了 Hooks 的基本使用,有些同窗會以爲:咦,我怎麼以爲你這幾個東西感受平平無奇,沒什麼特別的。但實際上這些基礎的 Hooks 單獨拿出來看的確不能玩出什麼花樣,但他們組合起來卻能迸發強大的力量。

自定義 Hooks

你們在業務開發可能會遇到這樣的需求,實現一個雙擊事件,若是你是從 H5 開發過來的可能會直接寫 onDoubleClick,但很遺憾,小程序組件是沒有 doubleClick 這個事件的。固然,若是你使用 Taro 又用了 TypeScript 就不會犯這樣的錯誤,編輯器就回直接給你報錯 Text 組件沒有這個屬性。

因而你就本身實現了一個雙擊事件,代碼大概是這樣,有一個上次點擊的時間做爲狀態,每次觸發單機事件的時候和上次點擊的時間作對比,若是間隔太小,那他就是一個雙擊事件。代碼很是簡單,但咱們不由就會產生一個問題問題,每一次給一個組件加單擊事件,咱們就每次都加這麼一坨代碼嗎?

function EditableText ({ title }) {
  const [ lastClickTime, setClickTime ] = useState(0)
  const [ editing, setEditing ] = useState(false)

  return (
    <View> { editing ? <TextInput editing={editing} /> : <Text onClick={e => { const currentTime = e.timeStamp const gap = currentTime - lastClickTime if (gap > 0 && gap < 300) { // double click setEditing(true) } setClickTime(currentTime) }} > {title} </Text> } </View> ) } 複製代碼

這個時候咱們就能夠寫一個自定義 Hooks,代碼和原來的代碼也差很少,useDoubleClick 不接受任何參數,但當咱們調用 useDoubleClick 時候返回一個名爲 textOnDoubleClick 的函數,在在 Text 組件的事件傳參中,咱們再在 textOnDoubleClick 函數中傳入一個回調函數,這個回調函數就是觸發雙擊條件時候的函數。當咱們給這個自定義 Hooks 作了柯里化以後,咱們就能夠作到知道 Hook 使用時才暴露回調函數:

function useDoubleClick () {
  const [ lastClickTime, setClickTime ] = useState(0)

  return (callback) => (e) => {
    const currentTime = e.timeStamp
    const gap = currentTime - lastClickTime
    if (gap > 0 && gap < 300) {
      callback && callback(e)
    }
    setClickTime(currentTime)
  }
}

function EditableText ({ title }) {
  const [ editing, setEditing ] = useState(false)
  const textOnDoubleClick = useDoubleClick()

  return (
    <View> { editing ? <TextInput editing={editing} /> : <Text onClick={textOnDoubleClick(() => setEditing(true) )} > {title} </Text> } </View> ) } 複製代碼

柯里化函數好像有一點點繞,但一旦咱們完成了這一步,這種咱們的自定義 hooks 就能夠像屢次調用:

function EditableText ({ title }) {
  const textOnDoubleClick = useDoubleClick()
  const buttonOnDoubleClick = useDoubleClick()
  // 任何實現單擊類型的組件都有本身獨立的雙擊狀態


  return (
    <View> <Text onClick={textOnDoubleClick(...)}> {title} </Text> <Button onClick={buttonOnDoubleClick(...)} /> </View> ) } 複製代碼

每個你們不妨試想若是按照咱們傳統的 render props 實現,每次都要多寫一個 container 組件,若是用 Mixins 或高階組件來實現就更麻煩,咱們須要基於每一個不一樣類型的組件創造一個新的組件。而使用 Hooks,任何一個實現了單機類型的組件均可以經過咱們的自定義 Hook 實現雙擊效果,無論從它的內部實現來看,仍是從它暴露的 API 來看都是很是優雅的。

性能優化

接下來咱們談一下性能優化,相信你們也有過這種狀況,有一個數組,他只需拿到他的 props 要渲染一次,今後以後他就不再須要更新了。對於傳統而言的 Class Component 咱們能夠設置 shouldComponentUpdate() 返回 false

class Numbers extends Component {
  shouldComponentUpdate () {
    return false
  }

  render () {
    return <View> { expensive(this.props.array).map(i => <View>{i}</View>) } </View>
  }
}
複製代碼

而對於函數式組件而言,咱們也能夠作同樣的事情。Taro 和 React 同樣提供 Taro.memo API,他的第一個參數接受一個函數式組件,第二個參數和咱們的 shouldComponentUpdate() 同樣,判斷組件在什麼樣的狀況下須要更新。若是第二個參數沒有傳入的話,Taro.memo 的效果就和 Taro.PureComponent 同樣,對新舊 props 作一層淺對比,若是淺對比不相等則更新組件。

function Numbers ({ array }) {
  return (
    <View> { expensive(array).map( i => <View>{i}</View> ) } </View>
  )
}

export default Taro.memo(Numbers, () => true)
複製代碼

第二種狀況咱們能夠看看咱們的老朋友,計數器組件。可是這個計數器組件和老朋友有兩點不同:第一是每次點擊 + 1,計數器須要調用 expensive 函數循環 1 億次才能拿到咱們想要的值,第二點是它多了一個 Input 組件。在咱們真實的業務開發中,這種狀況也很常見:咱們的組件可能須要進行一次昂貴的數據處理才能獲得最終想要的值,但這個組件又還有多個 state 控制其它的組件。在這種狀況下,咱們若是正常書寫業務邏輯是有性能問題的:

function Counter () {
  const [ count, setCount ] = useState(0)
  const [val, setValue] = useState('')
  function expensive() {
    let sum = 0
    for (let i = 0; i < count * 1e9; i++) {
      sum += i
    }
    return sum
  }

  return (
    <View> <Text>You clicked {expensive()} times}</Text> <Button onClick={() => setCount(count + 1)}> Click me </Button> <Input value={val} onChange={event => setValue(event.detail.value)} /> </View> ) } 複製代碼

由於咱們 count 的值跟 Input 的值沒有關係,但咱們每次改變 Input 的值,就會觸發這個組件從新渲染。也就是說這個循環一億次的 expensive() 函數就會從新調用。這樣狀況顯然是不能接受的。爲了解決這個問題,咱們可使用 useMemo API。useMemo 的簽名和 useEffect 有點像,區別就在於 useMemo 的第一個函數有返回值,這個函數返回的值同時也是 useMemo 函數的返回值。而第二個參數一樣是依賴數組,只有當這個數組的數據變化時候,useMemo 的函數纔會從新計算,若是數組沒有變化,那就直接從緩存中取數據。在咱們這個例子裏咱們只須要 count 變化才進行計算,而 Input value 變化無需計算。

function Counter () {
  const [ count, setCount ] = useState(0)
  const [val, setValue] = useState('')
  const expensive = useMemo(() => {
    let sum = 0
    for (let i = 0; i < count * 100; i++) {
      sum += i
    }
    return sum
  }, [ count ]) // ✅ 只有 count 變化時,回調函數纔會執行

  return (
    <View> <Text>You Clicked {expensive} times</Text> <Button onClick={() => setCount(count + 1)}> Click me </Button> <Input value={val} onChange={event => setValue(event.detail.value)} /> </View> ) } 複製代碼

咱們剛纔提到的兩個 memo 的 API ,他的全稱實際上是 Memoization。因爲 Hooks 都是在普通函數中運行的,因此咱們要作好性能優化,必定要好好利用緩存和記憶化這一技術。

在計算機科學中,記憶化(Memoization)是一種提升程序運行速度的優化技術。經過儲存大計算量函數的返回值,當這個結果再次被須要時將其從緩存提取,而不用再次計算來節省計算時間。

大規模狀態管理

提到狀態管理,React 社區最有名的工具固然是 Redux。在 react-redux@7 中新引用了三個 API:

  1. useSelector。它有點像 connect() 函數的第一個參數 mapStateToProps,把數據從 state 中取出來;
  2. useStore 。返回 store 自己;
  3. useDispatch。返回 store.dispatch

在 Taro 中其實你也可使用咱們以前提到過的 createContextuseContext 直接就把 useStoreuseDispatch 實現了。而基於 useStoreuseDispatch 以及 useStateuseMemouseEffect 也能夠實現 useSelector。也就是說 react-redux@7 的新 API 全都是普通 Hooks 構建而成的自定義 Hooks。固然咱們也把 react-redux@7 的新功能移植到了 @tarojs/redux,在 Taro 1.3 版本你能夠直接使用這幾個 API。

Hooks 的實現

咱們如今對 Hooks 已經有了如下的瞭解,一個合法的 Hooks ,必須知足如下需求才能執行:

  • 只能在函數式函數中調用
  • 只能在函數最頂層中調用
  • 不能在條件語句中調用
  • 不能在循環中調用
  • 不能在嵌套函數中調用

我想請你們思考一下,爲何一個 Hook 函數須要知足以上的需求呢?我想請你們以能夠框架開發者的角度去思考下這個問題,而不是以 API 的調用者的角度去逆向地思考。當一個 Hook 函數被調用的時,這個 Hook 函數的內部實現應該能夠訪問到當前正在執行的組件,可是咱們的 Hooks API 的入參卻沒有傳入這個組件,那到底是怎麼樣的設計纔可讓咱們的 hook 函數訪問到正在執行的組件,也可以準確地定位本身呢?

聰明的朋友或許已經猜到了,這些全部線索都指向一個結果,Hooks 必須是一個按順序執行的函數。也就是說,無論整個組件執行多少次,渲染多少次,組件中 Hooks 的順序都是不會變的。

咱們還知道另一條規則,Hooks 是 React 函數內部的函數,因而咱們就能夠知道,要實現 Hooks 最關鍵的問題在於兩個:

  1. 找到正在執行的 React 函數
  2. 找到正在執行的 Hooks 的順序。

咱們能夠設置一個全局的對象叫 CurrentOwner,它有兩個屬性,第一個是 current,他是正在執行的 Taro 函數,咱們能夠在組件加載和更新時設置它的值,加載或更新完畢以後再設置爲 null;第二個屬性是 index,它就是 CurrentOwner.current 中 Hooks 的順序,每次咱們執行一個 Hook 函數就自增 1。

const CurrentOwner: {
  current: null | Component<any, any>,
  index: number
} = {
  // 正在執行的 Taro 函數,
  // 在組件加載和從新渲染前設置它的值
  current: null,
  // Taro 函數中 hooks 的順序
  // 每執行一個 Hook 自增
  index: 0
}
複製代碼

在 React 中其實也有這麼一個對象,並且你還可使用它,它叫作 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,也就是說若是你想給 React 15 實現 Hooks,其實也能夠作到的。但也正如它的名字同樣,若是你用了說不定就被 fire 了,被優化了,因此更好的方案仍是直接使用咱們 taro。

接下來咱們來實現咱們的 getHook 函數,一樣很簡單,若是 CurrenOwner.currentnull,那這就不是一個合法的 hook 函數,咱們直接報錯。若是知足條件,咱們就把 hook 的 index + 1,接下來咱們把組件的 Hooks 都保存在一個數組裏,若是 index 大於 Hooks 的長度,說明 Hooks 沒有被創造,咱們就 push 一個空對象,避免以後取值發生 runtime error。而後咱們直接返回咱們的 Hook。

function getHook (): Hook {
  if (CurrentOwner.current === null) {
    throw new Error(`invalid hooks call: hooks can only be called in a taro component.`)
  }
  const index = CurrentOwner.index++ // hook 在該 Taro 函數中的 ID
  const hooks: Hook[] = CurrentOwner.current.hooks // 全部的 hooks
  if (index >= hooks.length) { // 若是 hook 尚未建立
    hooks.push({} as Hook) // 對象就是 hook 的內部狀態
  }
  return hooks[index] // 返回正在執行的 hook 狀態
}
複製代碼

既然咱們已經找到了咱們正在執行的 Hooks,完整地實現 Hooks 也就不難了。以前咱們討論過 useState 的簽名,如今咱們一步一步地看他的實現。

首先若是 initState 是函數,直接執行它。其次調用咱們咱們以前寫好的 getHook 函數,它返回的就是 Hook 的狀態。接下來就是 useState 的主邏輯,若是 hook 尚未狀態的話,咱們就先把正在執行的組件緩存起來,而後 useState 返回的,就是咱們的 hook.state, 其實就是一個數組,第一個值固然就是咱們 initState,第一個參數是一個函數,它若是是一個函數,咱們就執行它,不然就直接把參數賦值給咱們 的 hook.state 第一個值,賦值完畢以後咱們把當前的組件加入到更新隊列,等待更新。

function useState<S> (initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>] {
  if (isFunction(initialState)) { // 若是 initialState 是函數
    initialState = initialState() // 就直接執行
  }
  const hook = getHook() as HookState<S> // 找到該函數中對應的 hook
  if (isUndefined(hook.state)) { // 若是 hook 尚未狀態
    hook.component = Current.current! // 正在執行的 Taro 函數,緩存起來
    hook.state = [ // hook.state 就是咱們要返回的元組
      initialState,
      (action) => {
        hook.state[0] = isFunction(action) ? action(hook.state[0]) : action
        enqueueRender(hook.component) // 加入更新隊列
      }
    ]
  }
  return hook.state // 已經建立 hook 就直接返回
}
複製代碼

最後咱們把 hook.state 返回出去就大功告成了。

Taro 的 Hooks 總共有八個 API, useState 的實現你們能夠發現很是簡單,但其實它的代碼量和複雜度是全部 Hooks 的實現中第二高的。因此其實 Hooks 也沒有什麼黑科技,你們能夠放心大膽地使用。

總結與展望

在 2018 年 Ember.js 的做者提出過一個觀點,Compilers are the New Frameworks,編譯器即框架。什麼意思呢?就拿 React 來舉例,單單一個 React 其實沒什麼用,你還須要配合 create-react-app, eslint-plugin-react-hooks, prettier 等等編譯相關的工具最終才能構成一個框架,而這些工具也恰巧是 React Core Team 的人創造的。而這樣趨勢不只僅發生在 React 身上,你們能夠發如今2018年,尤雨溪老師的主要工做就是開發 vue-cli。而對一些更激進的框架,例如 svelte,它的框架就是編譯器,編譯器就是框架。

而到了 2019 年,我想提出一個新概念,叫框架即生態。就拿 Taro 來講,使用 Taro 你能夠複用 React 生態的東西,同時 Taro 還有 taro doctor,Taro 開發者社區,Taro 物料市場,還有騰訊小程序·雲開發等等多個合做夥伴一塊兒構成了 Taro 生態,而整個 Taro 生態纔是框架。在過去的半年,咱們持續改進並優化了 Taro 框架的表現,以上提到的特性與功能在 Taro 1.3 所有均可以正常使用。而在框架以外,咱們也深耕社區,推出了 Taro 物料市場和 Taro 開發者社區,並和騰訊小程序·雲開發合做舉辦了物料開發競賽。如今,咱們誠摯邀請你一塊兒來參與社區貢獻,讓小程序開發變得更好、更快、更方便:

官方物料市場,邀您參加「Taro x 小程序·雲開發」物料開發競賽

相關文章
相關標籤/搜索