函數式編程看React Hooks(一)簡單React Hooks實現
函數式編程看React Hooks(二)事件綁定副作用深度剖析
前言
函數式編程介紹(摘自基維百科)
函數式編程(英語:functional programming)或稱函數程序設計、泛函編程,是一種編程範式,它將計算機運算視爲函數運算,並且避免使用程序狀態以及易變對象。其中,λ演算(lambda calculus)爲該語言最重要的基礎。而且,λ演算的函數可以接受函數當作輸入(引數)和輸出(傳出值)。
面向對象編程介紹(摘自基維百科)
面向對象程序設計(英語:Object-oriented programming,縮寫:OOP)是種具有對象概念的程序編程典範,同時也是一種程序開發的抽象方針。它可能包含數據、屬性、代碼與方法。對象則指的是類的實例。它將對象作爲程序的基本單元,將程序和數據封裝其中,以提高軟件的重用性、靈活性和擴展性,對象裏的程序可以訪問及經常修改對象相關連的數據。在面向對象程序編程裏,計算機程序會被設計成彼此相關的對象
函數式強調在邏輯處理中不變性。面向對象通過消息傳遞改變每個Object的內部狀態。兩者是截然不同的編程思想,都具有自己的優勢,也因爲如此,才使得我們從 class 組件
轉化到 函數組件式
,有一些費解。
從 react 的變化可以看出,react 走的道路越來越接近於函數式編程,輸入輸出一致性。當然也不是憑空地去往這個方面,而是爲了能夠解決更多的問題。以下 三點是 react 官網所提到的 hooks 的動機 https://zh-hans.reactjs.org/docs/hooks-intro.html#motivation
- 代碼重用:在hooks出來之前,常見的代碼重用方式是 HOC 和render props,這兩種方式帶來的問題是:你需要解構自己的組件,同時會帶來很深的組件嵌套
- 複雜的組件邏輯:在class組件中,有許多的lifecycle 函數,你需要在各個函數的裏面去做對應的事情。這種方式帶來的痛點是:邏輯分散在各處,開發者去維護這些代碼會分散自己的精力,理解代碼邏輯也很喫力
- class組件的困惑:對於初學者來說,需要理解class組件裏面的this是比較喫力的,同時,基於class的組件難以優化。
本文是爲了給後面一篇文章作爲鋪墊,因爲在之後文章的講解過程中,你如果了理解了 React Hooks 的原理,再加上一步一步地講解,你可能會對 React Hooks 中各種情況會恍然大悟。
一開始的時候覺得 hooks 非常地神祕,寫慣了 class 式的組件後,我們的思維就會定格在那裏,生命週期,state,this等的使用。 因此會以 class 編寫的模式去寫函數式組件,導致我們一次又一次地爬坑,接下來我們就開始我們的實現方式講解。(提示:以下是都只是一種簡單的模擬方法,與實際有一些差別,但是核心思想是一致的)
開始
我們先寫一個簡單的 react 函數式組件。
function Counter(count) {
return (
<div>
<div>{count}</div>
<button>
點擊
</button>
</div>
);
}
在 React Hooks 還未出現的時候,我們的組件大多用來直接渲染,不含有狀態存儲,Function組件沒有state,所以也叫SFC(stateless functional component),現在更新叫做FC(functional component)。
爲了使得一個函數內有狀態,react 使用了一個特別的方法就是 hooks, 其實這是利用閉包實現的一個類似作用域的東西去存儲狀態,我第一想到的就是利用對象引用存儲數據,就像是面向對象一樣的方式,存在一個對象中中,通過引用的方式來進行獲取。但是 react 爲了能夠儘可能地分離狀態,精妙地採用了閉包。
讓我們看看他是如何實現的。(爲了儘可能簡化,我進行了改編)
useState
let _state;
function useState(initialState) {
_state = _state || initialState; // 如果存在舊值則返回, 使得多次渲染後的依然能保持狀態。
function setState(newState) {
_state = newState;
render(); // 重新渲染,將會重新執行 Counter
}
return [_state, setState];
}
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>
點擊
</button>
</div>
);
}
演示地址: https://codesandbox.io/s/dawn-bash-rqqoh
以上,不管 Counter 重新渲染多少次,通過閉包,依然能夠訪問到最新的 state,從而達到了存儲狀態的效果。
useEffect
再看看 useEffect, 先來看看使用方法。 useEffect(callback, dep?)
, 以下是一個非常簡單的使用例子。
useEffect(() => {
console.log(count);
}, [count]);
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(count);
}, [count]);
return (
<div>
<div>{count}</div>
<button onClick={() => setCount(count + 1)}>
點擊
</button>
</div>
);
}
因爲函數式不像 class 那樣有複雜的生命週期,已經對 hooks 已經熟悉使用的你,可能會知道 useEffect
可以當做,componentdidmount
來使用。但是在這裏你直接將他按照順序執行。在 return
前他會執行。
let _deps = {
args: []
}; // _deps 記錄 useEffect 上一次的 依賴
function useEffect(callback, args) {
const hasChangedDeps = args.some((arg, index) => arg !== _deps.args[index]); // 兩次的 dependencies 是否完全相等
// 如果 dependencies 不存在,或者 dependencies 有變化
if (!_deps.args || hasChangedDeps) {
callback();
_deps.args = args;
}
}
演示地址: https://codesandbox.io/s/ecstatic-glitter-w9kq7
至此,我們也實現了單個 useEffect。
useMemo
我們再來看看, useMemo,其實他也以上實現的方式一樣,也是通過閉包來進行存儲數據, 從而達到緩存提高性能的作用。
function Counter() {
const [count, setCount] = useState(0);
const computed = () => {
console.log('我執行了');
return count * 10 - 2;
}
const sum = useMemo(computed, [count]);
return (
<div>
<div>{count} * 10 - 2 = {sum}</div>
<button onClick={() => setCount(count + 1)}>
點擊
</button>
</div>
);
}
接下來我們來進行實現
let _deps = {
args: []
}; // _deps 記錄 useMemo 上一次的 依賴
function useMemo(callback, args) {
const hasChangedDeps = args.some((arg, index) => arg !== _deps.args[index]); // 兩次的 dependencies 是否完全相等
// 如果 dependencies 不存在,或者 dependencies 有變化
if (!_deps.args || hasChangedDeps) {
_deps.args = args;
_deps._callback = callback;
_deps.value = callback();
return _deps.value;
}
return _deps.value;
}
演示地址: https://codesandbox.io/s/festive-platform-v04xw
useCallback
那麼 useCallback
呢? 其實就是 useMemo
的一個包裝,畢竟你緩存函數的返回值,那麼我我讓返回值爲一個函數不就行了?
function useCallback(callback, args) {
return useMemo(() => callback, args);
}
可以看到,以上我們也輕鬆地實現了 useMemo 。但是有一個問題,以上只是單個函數使用方式,所以接下來我們還需要處理一下多個函數的情況。
完整版
我們可以按照 preact 的方法來實現。即用數組來實現多個函數的處理邏輯。
核心邏輯就是
-
第一次聲明的時候將 useState, useEffect, useMemo, useCallback 等鉤子函數的狀態依次存入數組。
-
更新的時候,將前一次的函數狀態值依次取出。
也可以通過以下圖來理解
第一次渲染,將每個狀態都緩存到數組中。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Bl5mDlhK-1570871852164)(https://s3.qiufengh.com/blog/first-render.png)]
每次重新渲染,獲取數組中每個的緩存狀態。
以下爲了能夠清晰地讓大家明白原理,進行了一些刪減。但是核心邏輯不變。
let currentIndex = 0;
let currentComponent = {
__hooks: []
};
function getHookState(index) {
const hooks = currentComponent.__hooks;
if (index >= hooks.length) {
hooks.push({});
}
return hooks[index];
}
function argsChanged(oldArgs, newArgs) {
return !oldArgs || newArgs.some((arg, index) => arg !== oldArgs[index]);
}
function useState(initialState) {
const hookState = getHookState(currentIndex++);
hookState._value = [
hookState._value ? hookState._value[0] : initialState,
function setState(newState) {
hookState._value[0] = newState;
render(); // 重新渲染,將會重新執行 Counter
}
];
return hookState._value;
}
function useEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
callback();
state._args = args;
render();
}
}
function useMemo(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
state._args = args;
state._callback = callback;
state.value = callback();
return state.value;
}
return state.value;
}
現在用以上 43 行代碼實現了一個簡易的 React Hooks。
完整渲染過程
我們再通過一次整體的流程圖來講解完整版的實現。
https://codesandbox.io/s/loving-blackburn-o7g8g
function Counter() {
const [count, setCount] = useState(0);
const [firstName, setFirstName] = useState("Rudi");
const computed = () => {
return count * 10 - 2;
};
const sum = useMemo(computed, [count]);
useEffect(() => {
console.log("init");
}, []);
return (
<div>
<div>
{count} * 10 - 2 = {sum}
</div>
<button onClick={() => setCount(count + 1)}>點擊</button>
<div>{firstName}</div>
<button onClick={() => setFirstName("Fred")}>Fred</button>
</div>
);
}
初始化
第一次渲染
將所有的狀態都存進閉包中。
事件觸發
改變了第二個狀態的value值。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-W1JedfMK-1570871852168)(https://s3.qiufengh.com/blog/1-事件觸發.png)]
第二次渲染
將所有狀態依次取出,進行渲染。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-P4SagYxs-1570871852169)(https://s3.qiufengh.com/blog/1-第二次渲染.png)]
後記
通過以上的實現,我們也可以明白一些 React Hooks 中看似有點奇怪的規定了。例如爲什麼不要在循環、條件判斷或者子函數中調用? 因爲順序很重要,我們將緩存(狀態)按一定地順序壓入數組,所以取出上一次狀態,也必須以同樣的順序去獲取。否則的話,會導致獲取不一致的情況。。。當然我們可以試想一下,如果每個狀態單元,可以有唯一的名字,那麼將不會受到這些規定的約束。但是這樣會使得開發帶來額外的傳入參數,就是唯一的名字。也會帶來名字衝突等問題,因此採用這樣的方式來約定,一定程度上簡化了開發者的開發成本,並且也能夠消除不一致性。(ps: 如果有人有興趣,可以實現一版不依賴於順序,只依賴於名字的,當做小玩具~)
當然真實中的 react 是利用了單鏈表來代替數組的。略微有些不一樣,但是本質的思路是一致的,以及 useEffect 是每次渲染完成後運行的。
以上都是站在巨人的肩膀上(有很多優秀的文章,看參考),再加上查看一些源碼得出的整個過程。最後,留出一個小問題給大家,那麼每次 useEffect
中 return 函數
的邏輯又是怎麼樣的呢?歡迎評論區說出實現方式~ 如果文章有任何問題,也歡迎在評論區指出~
參考
https://github.com/brickspert/blog/issues/26
https://segmentfault.com/a/1190000019824818
https://www.zhihu.com/question/306916142
https://zh-hans.reactjs.org/docs/hooks-faq.html#can-i-skip-an-effect-on-updates
https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e
https://github.com/preactjs/
https://zh-hans.reactjs.org/docs/hooks-intro.html#motivation