hooks 在微信小程式中的試驗
PS:首先,這不是一個成熟的東西,只是一個實現極其簡單的玩具而已。
前言
前段時間 react hooks 特性刷得沸沸揚揚的,看起來挺有意思的,估計不少其他框架也會逐步跟進,所以也來嘗試一下能不能用在小程式上。
react hooks 允許你在函式式元件中使用 state,用一段官方的簡單例子概括如下:
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
return (
<div>
<p> You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
複製程式碼
函式式元件本身非常簡潔,不維護生命週期和狀態,是一個可以讓效能得以優化的使用方式。但是在之前這種方式只能用於純展示元件或者高階元件等,它很難實現一些互動行為。但是在 hooks 出現之後,你就可以為所欲為了。
這裡有一份官方的文件,不明圍觀群眾有興趣的可以點進去了解一下:reactjs.org/docs/hooks-…
hooks 的使用目前有兩個限制:
- 只能在函式式元件內或其他自定義 hooks 內使用,不允許在迴圈、條件或普通 js 函式中呼叫 hooks。
- 只能在頂層呼叫 hooks 。
這個限制和 hooks 的實現方式有關,下面小程式 hooks 也會有同樣限制,原因應該也是類似的。為了能讓開發者更好的使用 hooks,react 官方也提供了一套 eslint 外掛來協助我們開發:reactjs.org/docs/hooks-…。
下面就來介紹下在小程式中的嘗試~
函式式元件
小程式沒有提供函式式元件,這倒是很好理解,小程式的架構是雙執行緒執行模式,邏輯層執行 js 程式碼,檢視層負責渲染。那麼宣告在邏輯層的自定義元件要渲染在檢視層必須保證來兩個執行緒都存在自定義元件例項並一一對應,這樣的架構已經成熟,目前對函式式元件並沒有強烈的需求。在基礎庫不大改的情況下,就算提供了函式式元件也只是提供了另一種新寫法而已,本質上的實現沒有區別也不能提升什麼效能。
不過也不排除以後小程式會提供一種只負責渲染不維護生命週期不做任何邏輯的特殊元件來優化渲染效能,這種的話本質上就和函式式元件類似了,不過函式式元件較為極端的是在理論上是有辦法做到無例項的,這個在小程式中怕是有點困難。
言歸正傳,小程式沒有提供函式式元件,那麼就強行封裝出一個寫法好了,假設我們有一個自定義元件,它的 js 和 wxml 內容分別是這樣的:
// component.js
const {useState, useEffect, FunctionalComponent} = require('miniprogram-hooks')
FunctionalComponent(function() {
const [count, setCount] = useState(1)
useEffect(() => {
console.log('count update: ', count)
}, [count])
const [title, setTitle] = useState('click')
return {
count,
title,
setCount,
setTitle,
}
})
複製程式碼
<!-- component.wxml -->
<view>{{count}}</view>
<button bindtap="setCount" data-arg="{{count + 1}}">{{title}}</button>
<button bindtap="setTitle" data-arg="{{title + '(' + count + ')'}}">update btn text</button>
複製程式碼
一個很奇葩的例子,但是能看明白就行。小程式裡檢視和邏輯分離,不像 react 可以將檢視和邏輯寫到一起,那麼小程式裡的函式式元件裡想返回一串渲染邏輯就不太科學了,這裡就改成返回要用於渲染的 state 和方法。
PS:wxml 裡不支援 bindtap="setCount(count + 1)" 這種寫法,所以引數就走 dataset 的方式傳入了。
FunctionComponent 函式其實就相當於封裝了小程式原有的 Component 構造器,它的實現類似這樣:
function FunctionalComponent(func) {
func = typeof func === 'function' ? func : function () {}
// 定義自定義元件
return Component({
attached() {
this._$state = {}
this._$effect = {}
this._$func = () => {
currentCompInst = this // 記錄當前的自定義元件例項
callIndex = 0 // 初始化呼叫序號
const newDef = func.call(null) || {}
currentCompInst = null
const {data, methods} = splitDef(newDef) // 拆分 state 和方法
// 設定 methods
Object.keys(methods).forEach(key => {
this[key] = methods[key]
})
// 設定 data
this.setData(data)
}
this._$func()
},
detached() {
this._$state = null
this._$effect = null
this._$func = null
}
})
}
複製程式碼
實現很簡單,就是在 attached 的時候跑一下傳入的函式,拿到 state 和方法後設置到自定義元件例項上就行。其中 currentCompInst 和 callIndex 在 useState 和 useEffect 的實現上會用到,下面來介紹。
useState 和 useEffect
這裡的一個難點是,useState 是沒有指定變數名的。初次渲染還好,二次渲染的話要找回這個變數就要費一段程式碼了。
PS:後續的實現除了參考了 react 的 hooks 外,也參考了 vue-hooks 的嘗試,有興趣的同學也可以去觀摩一下。
這裡上面提到的 currentCompInst 和 callIndex,將上一次的變數儲存在 currentCompInst 中,用 callIndex 記錄呼叫 useState 和 useEffect 的順序,這樣就可以在二次渲染的時候通過順序找回上一次使用的變數:
function useState(initValue) {
if (!currentCompInst) throw new Error('component instance not found!')
const index = callIndex++
const compInst = currentCompInst
if (compInst._$state[index] === undefined) compInst._$state[index] = initValue
const updater = function (evt) {
let value = evt
// wxml 事件回撥
if (typeof evt === 'object' && evt.target && evt.currentTarget) {
const dataset = evt.currentTarget.dataset
value = dataset && dataset.arg
}
// 存入快取
compInst._$state[index] = value
compInst._$func()
}
updater._isUpdater = true
return [compInst._$state[index], updater]
}
複製程式碼
useEffect 的實現邏輯也類似,這裡就不再貼程式碼了。小程式本身沒有提供 render 函式,調 FunctionalComponent 宣告函式式元件傳入的函式就作為 render 函式來用。每次調 setXXX 方法——也就是上面程式碼中返回的 updater 的時候,找到原本儲存這個 state 的地方儲存進去,然後再次執行 render 函式,進行元件的渲染。
到這裡應該就明白了,對於 hooks 使用為什麼會有一開始的那兩條限制。如果在一些條件、迴圈等語句內使用 hooks,就無法確保 state 的順序,再二次渲染時就不一定能找回對應的 state。
尾聲
完整的程式碼在 github.com/wechat-mini…,不過這終究只是個試驗性質的嘗試,並不推薦拿來實戰,寫在這裡是為與大家共享~