實現 React Hooks UI 開發有兩個問題: 展示覆用 邏輯復用 展示覆用目前基本使用組件化來解決,邏輯復用一直以來都沒有特別好的解決方案。React 從一開始的 mixin ,到 高階組件 以及 Render Props ,都是在試圖解決這個問題,但是都引入了一些別的問題。 Mixins ...
實現 React Hooks
UI 開發有兩個問題:
- 展示覆用
- 邏輯復用
展示覆用目前基本使用組件化來解決,邏輯復用一直以來都沒有特別好的解決方案。React 從一開始的 mixin ,到 高階組件 以及 Render Props ,都是在試圖解決這個問題,但是都引入了一些別的問題。
Mixins
- 命名空間衝突
- 數據來源不清晰
Higher-order Components
- props 屬性來源不清晰
- props 上命名衝突
- 額外的組件渲染帶來性能問題
Render Props(Vue 中的 Renderless Components)
- 解決了 命名空間衝突、數據來源不清晰的問題,仍然會帶來額外組件實例的性能消耗
Hooks
在前段時間 Hooks 發佈後,我認為 React 找到了【有狀態】組件【函數式】【復用邏輯】的解決方案。
先說有狀態:一般來說,無狀態組件直接使用函數組件就行,省去了實例化的樣板代碼和性能消耗。不涉及到 state 的存取,可以直接寫個 helper 函數處理一下,方便又快捷。
再說函數式:class 組件是面向對象的,每一次聲明、聲明周期都逃不開 this,而 hooks 更加函數式,調用一個函數,傳入的是初始值,返回修改值,沒有副作用。
最後說復用邏輯:DRY,一般來說,相同的代碼不寫第二次,在 class 組件中,通過生命周期方法對 state 修改,然後 rerender,在使用了 hooks 以後,我們可以通過 hooks 觸發 render,render 調用 hooks 時,根據傳入值的比較,來決定是否觸發 render,然後把 hooks 返回的值填充到頁面上。
實現
hooks 給人最直接的印象就是可以在 function 組件中使用 state 了。但它也有著不同於 class 組件的心智模型,從生命周期的思維跳到 update circle,剛開始 useEffect 總會寫一些無限迴圈。接下來我們先來寫一個簡易版的 useState,來模仿 React。
let state;
function useState(init) {
function setState(newState) {
if (typeof newState === "function") {
// 支持舊值傳入更新
state = newState(state);
} else {
state = newState;
}
// setState 後調用組件 render
// render();
}
if (!state) {
state = init;
}
return [state, setState];
}
上面就是一個 useState 了,接下來繼續寫 useEffect.
let _deps;
function useEffect(callback, deps) {
if (
deps === undefined || // 就是不傳 deps,就是每次都執行副作用
(deps !== undefined && _deps === undefined) || // 就是初始化,要執行一次副作用
!deps.every((dep, i) => dep === _deps[i]) // 如果不是每一項都相等,就執行
) {
callback();
}
}
這就是 useEffect 基本實現了,當然還有一個問題,callback 的 return 函數問題。
let _deps;
function useEffect(callback, deps) {
if (
deps === undefined || // 就是不傳 deps,就是每次都執行副作用
(deps !== undefined && _deps === undefined) || // 就是初始化,要執行一次副作用
!deps.every((dep, i) => dep === _deps[i]) // 如果不是每一項都相等,就執行
) {
// 如果有 cleanUp 就執行清理
if (typeof _deps._cleanUp === "function") {
_deps._cleanUp();
}
_deps._cleanUp = callback();
}
}
接下來,還有個重要問題,hooks 的順序問題,其實就是把 state 和 deps 存到數組裡。
let _arr = [];
let cursor = 0;
function useState(init) {
const current = cursor;
function setState(newState) {
if (typeof newState === "function") {
// 支持舊值傳入更新
_arr[current] = newState(_arr[current]);
} else {
_arr[current] = newState;
}
// setState 後調用組件 render
render();
}
if (!_arr[current]) {
_arr[current] = init;
}
cursor++;
return [_arr[current], setState];
}
function useEffect(callback, deps) {
const effect = _arr[cursor] || {};
const { _deps, _cleanUp } = effect;
if (
deps === undefined || // 就是不傳 deps,就是每次都執行副作用
(deps !== undefined && _deps === undefined) || // 就是初始化,要執行一次副作用
!deps.every((dep, i) => dep === _deps[i]) // 如果不是每一項都相等,就執行
) {
// 如果有 cleanUp 就執行清理
if (typeof _cleanUp === "function") {
_cleanUp();
}
effect._cleanUp = callback();
effect._deps = deps;
_arr[cursor] = effect;
}
cursor++;
}
全部代碼在 CodeSandBox。
完。