前段時間 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。
|