從Immutable.js到Redux函數(shù)式編程
基本概念
函數(shù)式編程(英語:functional programming)或稱函數(shù)程序設(shè)計(jì)、泛函編程,是一種編程范式。它將電腦運(yùn)算視為函數(shù)運(yùn)算,并且避免使用程序狀態(tài)以及易變對(duì)象。其中,λ 演算為該語言最重要的基礎(chǔ)。而且,λ 演算的函數(shù)可以接受函數(shù)作為輸入?yún)?shù)和輸出返回值。
以上是維基百科對(duì)于函數(shù)式編程的定義,用簡(jiǎn)單的話總結(jié)就是“強(qiáng)調(diào)以函數(shù)使用為主的軟件開發(fā)風(fēng)格”。
在抽象的定義之外,從實(shí)際出發(fā),JS 的函數(shù)式編程有以下幾個(gè)特點(diǎn):
- 函數(shù)是一等公民
- 擁抱純函數(shù),拒絕副作用
- 使用不可變值
函數(shù)式編程要素
函數(shù)是一等公民
我們經(jīng)常聽到這句話,”在 JS 中函數(shù)是一等公民“,其具體的含義是,函數(shù)具有以下特征:
- 可以被當(dāng)作參數(shù)傳遞給其他函數(shù)
- 可以作為另一個(gè)函數(shù)的返回值
- 可以被賦值給一個(gè)變量
函數(shù)式一等公民的特點(diǎn)是所有函數(shù)式編程語言所必須具有的,另一個(gè)必備特點(diǎn)則是支持閉包(上面的第二點(diǎn)其實(shí)很多時(shí)候都利用了閉包)
純函數(shù)
有且僅有顯示數(shù)據(jù)流:
- 輸入:參數(shù)
- 輸出:返回值
一個(gè)函數(shù)要是純函數(shù),要符合以下幾點(diǎn):
函數(shù)內(nèi)部不能有副作用
對(duì)于同樣的輸入(參數(shù)),必定得到同樣的輸出。
這意味著純函數(shù)不能依賴外部作用域的變量
副作用
參考純函數(shù)“僅有顯示數(shù)據(jù)流”的定義,副作用的定義即擁有“隱式數(shù)據(jù)流”。或者說:
- 會(huì)對(duì)函數(shù)作用域之外的執(zhí)行上下文、宿主環(huán)境產(chǎn)生影響,如修改全局變量
- 依賴了隱式輸入,如使用全局變量
- 進(jìn)行了與外界的隱式數(shù)據(jù)交換,如網(wǎng)絡(luò)請(qǐng)求
不可變值
當(dāng)函數(shù)參數(shù)為引用類型時(shí),對(duì)參數(shù)的改變將作用將映射到其本身。
const arr = [1, 2, 3]; const reverse = (arr) => { arr.reverse(); }; reverse(arr); console.log(arr); // [3,2,1]
這種操作符合“副作用”的定義:修改了外部變量。破壞了純函數(shù)的顯示數(shù)據(jù)流。
如果真的需要設(shè)計(jì)對(duì)數(shù)據(jù)的修改,則應(yīng)該:
- 拷貝原始數(shù)據(jù)
- 修改拷貝結(jié)果,返回新的數(shù)據(jù)
const reverse = (arr) => { const temp = JSON.parse(JSON.stringify(arr)); return temp.reverse(); }; arr = reverse(arr);
拷貝帶來的問題
通過拷貝實(shí)現(xiàn)對(duì)外部數(shù)據(jù)的只讀直觀且簡(jiǎn)單,代價(jià)則是性能。
對(duì)于一個(gè)大對(duì)象,每次的修改可能只是其中的一個(gè)屬性,那么每次的拷貝會(huì)帶來大量的冗余操作。當(dāng)數(shù)據(jù)規(guī)模大,操作頻率高時(shí),會(huì)帶來嚴(yán)重的性能問題。
解決拷貝的性能問題: 持久化數(shù)據(jù)結(jié)構(gòu)
拷貝模式的問題根源在于:一個(gè)大對(duì)象只有一小部分有改變,卻要對(duì)整個(gè)對(duì)象做拷貝。
這個(gè)情況其實(shí)和另一個(gè)場(chǎng)景很相似,就是 Git。一個(gè)項(xiàng)目有很多文件,但我一次可能只修改了其中一個(gè)。那么我本次的提交記錄是怎樣的呢?其處理邏輯就是:將改變部分和不變部分進(jìn)行分離。
**Git 快照保存文件索引,而不會(huì)保存文件本身。變化的文件將擁有新的存儲(chǔ)空間+新的索引,不變的文件將永遠(yuǎn)呆在原地。**而在持久化數(shù)據(jù)結(jié)構(gòu)中,則是變化的屬性的索引,和不變的屬性的索引
持久化數(shù)據(jù)結(jié)構(gòu)最常用的庫是 Immutable.js,其詳解見下文。
JS 中三種編程范式
JS 是一種多范式語言,而從前端的發(fā)展歷史來看,各時(shí)段的主流框架,也正對(duì)應(yīng)了三種編程范式:
- JQuery:命令式編程
- React 類組件:面向?qū)ο?/li>
- React Hooks、 Vue3:函數(shù)式編程
函數(shù)式編程的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 利于更好的代碼組織。因?yàn)榧兒瘮?shù)不依賴于上下文所以天然具有高內(nèi)聚低耦合的特點(diǎn)
- 利于邏輯復(fù)用。純函數(shù)的執(zhí)行是與上下文無關(guān)的,因此可以更好的在不同場(chǎng)景中復(fù)用
- 便于單元測(cè)試。純函數(shù)對(duì)于相同輸入一定得到相同輸出的特點(diǎn),便于自動(dòng)化測(cè)試
缺點(diǎn)
- 相比于命令式編程,往往會(huì)包裝更多的方法,產(chǎn)生更多的上下文切換帶來的開銷。
- 更多的使用遞歸,導(dǎo)致更高的內(nèi)存開銷。
- 為了實(shí)現(xiàn)不可變數(shù)據(jù),會(huì)產(chǎn)生更多的對(duì)象,對(duì)垃圾回收的壓力更大。
偏函數(shù)
偏函數(shù)的定義簡(jiǎn)單來說就是,將函數(shù)轉(zhuǎn)換為參數(shù)更少的函數(shù),也就是為其預(yù)設(shè)參數(shù)。
從 fn(arg1, arg2) 到 fn(arg1)
柯里化(curry)函數(shù)
柯里化函數(shù)在偏函數(shù)的基礎(chǔ)上,不僅減少了函數(shù)入?yún)€(gè)數(shù),還改變了函數(shù)執(zhí)行次數(shù)。其含義就是將一個(gè)接收 N 個(gè)入?yún)⒌暮瘮?shù),改寫為接受一個(gè)入?yún)?,并返回接受剩?N-1 個(gè)參數(shù)的函數(shù)。也就是:
fn(1,2,3) => fn(1)(2)(3)
實(shí)現(xiàn)一個(gè)柯里化函數(shù)也是面試高頻內(nèi)容,其實(shí)如果規(guī)定了函數(shù)入?yún)€(gè)數(shù),那么是很容易實(shí)現(xiàn)的。例如對(duì)于入?yún)€(gè)數(shù)為 3 的函數(shù),實(shí)現(xiàn)如下
const curry = (fn) => (arg1) => (arg2) => (arg3) => fn(arg1, arg2, arg3); const fn = (a, b, c) => console.log(a, b, c); curry(fn)(1)(2)(3); // 1 2 3
那么實(shí)現(xiàn)通用的 curry 函數(shù)的關(guān)鍵就在于:
- 自動(dòng)判斷函數(shù)入?yún)?/li>
- 自我遞歸調(diào)用
const curry = (fn) => { const argLen = fn.length; // 原函數(shù)的入?yún)€(gè)數(shù) const recursion = (args) => args.length >= argLen ? fn(...args) : (newArg) => recursion([...args, newArg]); return recursion([]); };
compose & pipe
compose 和 pipe 同樣是很常見的工具,一些開源庫中也都有自己針對(duì)特定場(chǎng)景的實(shí)現(xiàn)(如 Redux、koa-compose)。而要實(shí)現(xiàn)一個(gè)通用的 compose 函數(shù)其實(shí)很簡(jiǎn)單,借助數(shù)組的 reduce 方法就好
const compose = (funcs) => { if (funcs.length === 0) { return (arg) => arg; } if (funcs.length === 1) { return funcs[0]; } funcs.reduce( (pre, cur) => (...args) => pre(cur(...args)) ); }; const fn1 = (x) => x * 2; const fn2 = (x) => x + 2; const fn3 = (x) => x * 3; const compute = compose([fn1, fn2, fn3]); // compute = (...args) => fn1(fn2(fn3(...args))) console.log(compute(1)); // 10
而 pipe
函數(shù)與 compose
的區(qū)別則是其執(zhí)行順序相反,正如其字面含義,就像 Linux 中的管道操作符,前一個(gè)函數(shù)的結(jié)果流向下一個(gè)函數(shù)的入?yún)?,所以?reduce
方法改為 reduceRight
即可:
const pipe = (funcs) => { if (funcs.length === 0) { return (arg) => arg; } if (funcs.length === 1) { return funcs[0]; } funcs.reduceRight( (pre, cur) => (...args) => pre(cur(...args)) ); }; const compute = pipe([fn1, fn2, fn3]); // compute = (...args) => fn3(fn2(fn1(...args))) console.log(compute(1)); // 12
函數(shù)式在常見庫中的應(yīng)用
React
在最新的 React 文檔中,函數(shù)式組件 + hook 寫法已經(jīng)成為官方的首推風(fēng)格。而這正是基于函數(shù)式編程的理念。React 的核心特征是“數(shù)據(jù)驅(qū)動(dòng)視圖”,即UI = render(data)
。
UI 的更新是一定需要副作用的,那么如何保證組件函數(shù)的“純”呢?答案是將副作用在組件之外進(jìn)行管理,所有的副作用都交由 hooks,組件可以使用 state,但并不擁有 state。
Hooks 相比類組件的優(yōu)點(diǎn):
- 關(guān)注點(diǎn)分離。在類組件中,邏輯代碼放在生命周期中,代碼是按照生命周期組織的。而在 hooks 寫法中,代碼按業(yè)務(wù)邏輯組織,更加清晰
- 寫法更簡(jiǎn)單。省去了類組件寫法中基于繼承的各種復(fù)雜設(shè)計(jì)模式
Immutable.js
Immutable
是用于達(dá)成函數(shù)式編程三要素中的“不可變值”。我的初次接觸是在 Redux 中使用到,Redux 要求 reducer 中不能修改 state 而是應(yīng)該返回新的 state,但這僅是一種“規(guī)范上的約定”,而不是“代碼層面的限制”,而 Immutable 正是用于提供 JS 原生不存在的不可修改的數(shù)據(jù)結(jié)構(gòu)。
Immutable 提供了一系列自定義數(shù)據(jù)結(jié)構(gòu),并提供相應(yīng)的更新 API,而這些 API 將通過返回新值的方式執(zhí)行更新。
let map1 = Immutable.Map({}); map1 = map1.set("name", "youky"); console.log(map1);
Immutable 內(nèi)部的存儲(chǔ)參考 字典樹(Trie)
實(shí)現(xiàn),在每次修改時(shí),不變的屬性將用索引指向原來的值,只對(duì)改變的值賦值新的索引。這樣更新的效率會(huì)比整體拷貝高很多。
Redux
Redux 中體現(xiàn)函數(shù)式編程模式的也有很多地方:
- reducer 要是純函數(shù)(如果需要副作用,則使用 redux-saga 等中間件)
- reducer 中不直接修改 state,而是返回新的 state
- 中間件的高階函數(shù)與柯里化
- 提供了一個(gè)
compose
函數(shù),這是函數(shù)式編程中非?;镜墓ぞ吆瘮?shù)
Redux 源碼中的 compose 函數(shù)實(shí)現(xiàn)如下:
export default function compose(): <R>(a: R) => R; export default function compose<F extends Function>(f: F): F; /* two functions */ export default function compose<A, T extends any[], R>( f1: (a: A) => R, f2: Func<T, A> ): Func<T, R>; /* three functions */ export default function compose<A, B, T extends any[], R>( f1: (b: B) => R, f2: (a: A) => B, f3: Func<T, A> ): Func<T, R>; /* four functions */ export default function compose<A, B, C, T extends any[], R>( f1: (c: C) => R, f2: (b: B) => C, f3: (a: A) => B, f4: Func<T, A> ): Func<T, R>; /* rest */ export default function compose<R>( f1: (a: any) => R, ...funcs: Function[] ): (...args: any[]) => R; export default function compose<R>(...funcs: Function[]): (...args: any[]) => R; export default function compose(...funcs: Function[]) { if (funcs.length === 0) { // infer the argument type so it is usable in inference down the line return <T>(arg: T) => arg; } if (funcs.length === 1) { return funcs[0]; } return funcs.reduce( (a, b) => (...args: any) => a(b(...args)) ); }
首先是用函數(shù)重載來進(jìn)行類型聲明。
在實(shí)現(xiàn)其實(shí)非常簡(jiǎn)單:
- 傳入數(shù)組為空,返回一個(gè)自定義函數(shù),這個(gè)函數(shù)返回接收到的參數(shù)
- 如果傳入數(shù)組長(zhǎng)度為 1,返回唯一的一個(gè)元素
- 使用 reduce 方法組裝數(shù)組元素,返回一個(gè)包含元素嵌套執(zhí)行的新函數(shù)
Koa
在 Koa 的洋蔥模型中,通過 app.use
添加中間件,會(huì)將中間件函數(shù)存儲(chǔ)于this.middleware
use (fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!') debug('use %s', fn._name || fn.name || '-') this.middleware.push(fn) return this }
通過 koa-compose
模塊將所有的中間件組合為一個(gè)函數(shù) fn,在每次處理請(qǐng)求時(shí)調(diào)用
// callback 就是 app.listen 時(shí)綁定的處理函數(shù) callback () { const fn = this.compose(this.middleware) if (!this.listenerCount('error')) this.on('error', this.onerror) const handleRequest = (req, res) => { const ctx = this.createContext(req, res) return this.handleRequest(ctx, fn) } return handleRequest }
這里的 compose 決定了多個(gè)中間件之間的調(diào)用順序,用戶可以通過 option 傳入自定義的 compose 函數(shù),或默認(rèn)使用 koa-compose
模塊。其源碼如下:
function compose(middleware) { if (!Array.isArray(middleware)) throw new TypeError("Middleware stack must be an array!"); for (const fn of middleware) { if (typeof fn !== "function") throw new TypeError("Middleware must be composed of functions!"); } /** * @param {Object} context * @return {Promise} * @api public */ return function (context, next) { // last called middleware # let index = -1; return dispatch(0); function dispatch(i) { if (i <= index) return Promise.reject(new Error("next() called multiple times")); index = i; let fn = middleware[i]; if (i === middleware.length) fn = next; if (!fn) return Promise.resolve(); try { return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err); } } }; }
同樣是先對(duì)參數(shù)進(jìn)行判斷。與 redux 中的 compose 不同的是,koa 中的中間件是異步的,需要手動(dòng)調(diào)用 next 方法將執(zhí)行權(quán)交給下一個(gè)中間件。通過代碼可知,中間件中接收的 next 參數(shù)實(shí)際就是 dispatch.bind(null, i + 1))
也就是 dispatch 方法,以達(dá)到遞歸執(zhí)行的目的。
這里使用 bind
實(shí)際上就是創(chuàng)建了一個(gè)偏函數(shù)。根據(jù) bind 的定義,在 this 之后傳入的若干個(gè)參數(shù)會(huì)在返回函數(shù)調(diào)用時(shí)插入?yún)?shù)列表的最前面。也就是說
const next = dispatch.bind(null, i + 1)) next() // 等價(jià)于dispatch(i+1)
附:函數(shù)式編程與數(shù)學(xué)原理
函數(shù)并不是計(jì)算機(jī)領(lǐng)域的專有名詞。實(shí)際上,函數(shù)一詞最早由萊布尼茲在 1694 年開始使用。
函數(shù)式編程的思想背后,其實(shí)蘊(yùn)含了范疇論、群論等數(shù)學(xué)原理的思想。
以上就是從Immutable.js到Redux函數(shù)式編程的詳細(xì)內(nèi)容,更多關(guān)于Immutable.js Redux函數(shù)式編程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Echarts折線圖實(shí)現(xiàn)一條折線顯示不同顏色的方法
這篇文章主要給大家介紹了關(guān)于Echarts折線圖實(shí)現(xiàn)一條折線顯示不同顏色的相關(guān)資料,Echarts的折線圖可以通過設(shè)置series中的itemStyle屬性來改變折線的顏色,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-02-02JS localStorage實(shí)現(xiàn)本地緩存的方法
JS localStorage實(shí)現(xiàn)本地緩存的方法,需要的朋友可以參考一下2013-06-06JavaScript數(shù)據(jù)類型轉(zhuǎn)換簡(jiǎn)單方法舉例
JavaScript是一種無類型語言,但同時(shí)JavaScript提供了一種靈活的自動(dòng)類型轉(zhuǎn)換的處理方式,下面這篇文章主要給大家介紹了關(guān)于JavaScript數(shù)據(jù)類型轉(zhuǎn)換的相關(guān)資料,需要的朋友可以參考下2023-12-12JavaScript中遍歷對(duì)象的property的3種方法介紹
這篇文章主要介紹了JavaScript中遍歷對(duì)象的property的3種方法介紹,本文先是講解了3種方法并用一張圖片加深理解,然后給出代碼實(shí)例,需要的朋友可以參考下2014-12-12JavaScript動(dòng)態(tài)插入script的基本思路及實(shí)現(xiàn)函數(shù)
偶爾需要?jiǎng)討B(tài)插入javascript代碼的需求,基本思路是動(dòng)態(tài)創(chuàng)建一個(gè)script標(biāo)簽,設(shè)置其src屬性,type屬性等,需要的朋友可以參考下2013-11-11javascript實(shí)現(xiàn)數(shù)組內(nèi)值索引隨機(jī)化及創(chuàng)建隨機(jī)數(shù)組的方法
這篇文章主要介紹了javascript實(shí)現(xiàn)數(shù)組內(nèi)值索引隨機(jī)化及創(chuàng)建隨機(jī)數(shù)組的方法,涉及javascript數(shù)組索引及隨機(jī)數(shù)的相關(guān)實(shí)現(xiàn)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-08-08Javascript 實(shí)現(xiàn)放大鏡效果實(shí)例詳解
這篇文章主要介紹了Javascript 實(shí)現(xiàn)放大鏡效果實(shí)例詳解的相關(guān)資料,這里附有實(shí)現(xiàn)實(shí)例代碼,具有參考價(jià)值,需要的朋友可以參考下2016-12-12