JavaScript – Pipeline Operator
介紹
Pipeline Operator (|>) 是一個很新的 JavaScript 語法,目前還在 TC39 stage 2。
它有啥用呢?我們一起來了解一下 pipe 的前世今生。
參考
YouTube – Javascript's New Pipeline Operator Is Awesome!
Pipe 的前世今生
我們直接看例子。
Single function
這是一個 sum 函數
function sum(numbers) { return numbers.reduce((total, number) => total + number, 0); }
沒什么特別的,就是把 array 的 number 累加起來,返回一個總數,使用方式是這樣
const numbers = [1, 2, 3, 4, 5, 6]; const total = sum(numbers); console.log(total); // 21
好,再一個 removeOdd 函數
function removeOdd(numbers) { return numbers.filter(number => number % 2 === 0); }
也沒什么特別的,就是把 array 里的單數刪除,只留下雙數,使用方式是這樣
const numbers = [1, 2, 3, 4, 5, 6]; const evenNumbers = removeOdd(numbers); console.log(evenNumbers); // [2, 4, 6]
Combine function
那如果我想要先 "刪除單數 removeOdd" 接著 "累加 sum" 該怎么寫呢?
const numbers = [1, 2, 3, 4, 5, 6]; const evenNumbers = removeOdd(numbers); // [2, 4, 6] const total = sum(evenNumbers); console.log(total); // 12
簡單,我們可以先調用 removeOdd 獲得雙數,接著再把雙數拿去 sum,這樣就獲得總數了。
等等...這代碼有點啰嗦丫。
讓我們把它們連寫在一起看看。
const numbers = [1, 2, 3, 4, 5, 6]; const total = sum(removeOdd(numbers)); // 12
呃...代碼雖然是正確,但看上去不好理解,因為我們的邏輯是先 removeOdd 然后才 sum,但代碼看上去的順序卻是顛倒的,先 sum 然后 removeOdd,一點都不直觀。
Method chaining
我們回過頭看看原生 Array 方法的調用方式
const numbers = [1, 2, 3, 4, 5, 6]; const total = numbers .filter(number => number % 2 === 0) // removeOdd .reduce((total, number) => total + number, 0); // sum console.log(total); // 12
看到嗎,代碼的順序和要執行的邏輯是一致的,這才符合直覺。
那我們有辦法也寫成這樣嗎?
有,擴展 Array prototype 就可以了。
function sum(numbers) { return numbers.reduce((total, number) => total + number, 0); } function removeOdd(numbers) { return numbers.filter(number => number % 2 === 0); } Array.prototype.sum = function() { return sum(this); } Array.prototype.removeOdd = function() { return removeOdd(this); } const numbers = [1, 2, 3, 4, 5, 6]; const total = numbers.removeOdd().sum(); console.log(total); // 12
RxJS 6.0 以前的寫法就采用了 method chaining 方式,像這樣
// Import the necessary operators from RxJS 5 import { Observable } from 'rxjs'; import 'rxjs/add/operator/map'; import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/reduce'; // Create an observable that emits numbers from 1 to 5 const numbers$ = Observable.of(1, 2, 3, 4, 5); // Chain operators: filter, map, reduce numbers$ .filter(num => num % 2 === 0) // Keep even numbers .map(num => num * 2) // Double the numbers .reduce((acc, num) => acc + num, 0) // Sum the numbers .subscribe(result => { console.log(result); // Output will be 12 (i.e., (2*2) + (4*2) = 4 + 8) });
Pipe function
method chaining 寫法有一些致命的問題,比如它不支持 tree shaking 等等。
RxJS 在 v6.0 版本中,把 method chaining 換成了 Pipe function 寫法。
這是一個非常嚴重的 breaking changes,對用戶來說,項目中每一個使用到 RxJS 的地方都需要改代碼??。
從這一點也可以看出 method chaining 的問題很大,所以 RxJS 才不惜代價也要改成 Pipe function。
Pipe function 的寫法是這樣的:
首先需要一個通用的 pipe 函數
function pipe(source, ...pipeFns) { let result = source; for (const pipeFn of pipeFns) { const fnReturn = pipeFn(result); if (fnReturn !== undefined) { result = fnReturn; } } return result; }
TypeScript 版
type PipeFn<T, R> = (param: T) => R; function pipe<T, R1>(source: T, fn1: PipeFn<T, R1>): R1; function pipe<T, R1, R2>(source: T, fn1: PipeFn<T, R1>, fn2: PipeFn<R1, R2>): R2; function pipe<T, R1, R2, R3>(source: T, fn1: PipeFn<T, R1>, fn2: PipeFn<R1, R2>, fn3: PipeFn<R2, R3>): R3; function pipe<T, R1, R2, R3, R4>( source: T, fn1: PipeFn<T, R1>, fn2: PipeFn<R1, R2>, fn3: PipeFn<R3, R4>, fn4: PipeFn<R3, R4>, ): R4; function pipe<T, R1, R2, R3, R4, R5>( source: T, fn1: PipeFn<T, R1>, fn2: PipeFn<R1, R2>, fn3: PipeFn<R3, R4>, fn4: PipeFn<R3, R4>, fn5: PipeFn<R4, R5>, ): R5; function pipe<T, R1, R2, R3, R4, R5, R6>( source: T, fn1: PipeFn<T, R1>, fn2: PipeFn<R1, R2>, fn3: PipeFn<R3, R4>, fn4: PipeFn<R3, R4>, fn5: PipeFn<R4, R5>, fn6: PipeFn<R5, R6>, ): R6; function pipe<T, R1, R2, R3, R4, R5, R6, R7>( source: T, fn1: PipeFn<T, R1>, fn2: PipeFn<R1, R2>, fn3: PipeFn<R3, R4>, fn4: PipeFn<R3, R4>, fn5: PipeFn<R4, R5>, fn6: PipeFn<R5, R6>, fn7: PipeFn<R6, R7>, ): R7; function pipe<T, R1, R2, R3, R4, R5, R6, R7, R8>( source: T, fn1: PipeFn<T, R1>, fn2: PipeFn<R1, R2>, fn3: PipeFn<R3, R4>, fn4: PipeFn<R3, R4>, fn5: PipeFn<R4, R5>, fn6: PipeFn<R5, R6>, fn7: PipeFn<R6, R7>, fn8: PipeFn<R7, R8>, ): R8; function pipe<T, R1, R2, R3, R4, R5, R6, R7, R8, R9>( source: T, fn1: PipeFn<T, R1>, fn2: PipeFn<R1, R2>, fn3: PipeFn<R3, R4>, fn4: PipeFn<R3, R4>, fn5: PipeFn<R4, R5>, fn6: PipeFn<R5, R6>, fn7: PipeFn<R6, R7>, fn8: PipeFn<R7, R8>, fn9: PipeFn<R8, R9>, ): R9; function pipe(source: unknown, ...pipeFns: PipeFn<unknown, unknown>[]): unknown; function pipe(source: unknown, ...pipeFns: PipeFn<unknown, unknown>[]): unknown { let result = source; for (const pipeFn of pipeFns) { const fnReturn = pipeFn(result); if (fnReturn !== undefined) { result = fnReturn; } } return result; }
接著
const numbers = [1, 2, 3, 4, 5, 6]; const total = pipe(numbers, removeOdd, sum); console.log(total); // 12
原理很簡單,就是拿上一個函數的 return,傳入下一個函數,以此類推。
pipe function 是獨立的,它不需要和 prototype 有關聯,所以完全支持 tree shaking。
pipe function with arguments
如果我們的函數需要參數也沒問題,
function removeOddOrEven(numbers, oddOrEven) { return numbers.filter(num => oddOrEven === 'odd' ? num % 2 === 0 : num % 2); }
調用時需要傳入參數 oddOrEven。
調用時 wrap 一層箭頭函數即可
const total = pipe(numbers, numbers => removeOddOrEven(numbers, 'odd'), sum);
或者寫一個通用的 wrapper
export function wrapToPipeFn(pipeFn) { return (...args) => value => pipeFn(value, ...args) } const removeOddOrEvenPipe = wrapToPipeFn(removeOddOrEven); const numbers = [1, 2, 3, 4, 5, 6]; const total = pipe(numbers, removeOddOrEvenPipe('odd'), sum); console.log(total); // 12
TypeScript 版
function wrapToPipeFn<TArguments extends unknown[], TValue, TReturn>( pipeFn: (value: TValue, ...args: TArguments) => TReturn, ) { return (...args: TArguments) => (value: TValue) => pipeFn(value, ...args); }
RxJS > v6.0
上一 part 我們看到的是 RxJS 5.0 的語法,采用的是 method chaining,而 v6.0 后就改成 pipe function 了。
// Import the necessary operators from RxJS 7 import { of } from 'rxjs'; import { filter, map, reduce } from 'rxjs/operators'; // Create an observable that emits numbers from 1 to 5 const numbers$ = of(1, 2, 3, 4, 5); // Chain operators using pipe numbers$ .pipe( filter(num => num % 2 === 0), // Keep even numbers map(num => num * 2), // Double the numbers reduce((acc, num) => acc + num, 0) // Sum the numbers ) .subscribe(result => { console.log(result); // Output will be 12 (i.e., (2*2) + (4*2) = 4 + 8) });
Pipeline Operator (|>)
主角登場????
了解了 pipe function,再來看 Pipeline Operator 就很簡單了。
這一句
const total = pipe(numbers, removeOdd, sum);
改成這樣
const total = numbers |> removeOdd |> sum;
效果一模一樣,這就是 JS 的 Pipeline Operator 語法。
去掉 pipe 函數調用,然后把逗號 (,) 換成 pipe 箭頭 (|>)。
這一句
const total = pipe(numbers, (numbers) => removeOddOrEven(numbers, 'odd'), sum);
改成這樣
const total = numbers |> numbers => removeOddOrEven(numbers, 'odd') |> sum;
效果一模一樣。
此外,Pipeline Operator 還支持 async return 等等,我目前還沒有用到就不給例子了,有興趣的讀友自己玩玩唄。
Babel for Pipeline Operator
Pipeline Operator 語法還很新,需要 Babel 做轉譯。(TypeScript 還不支持,說是要等到 stage 3)
這里簡單演示一下 Babel setup。
創建項目
yarn init
安裝 Babel
yarn add @babel/cli @babel/core @babel/preset-env --dev
安裝插件 for Pipeline Operator
yarn add @babel/plugin-proposal-pipeline-operator --dev
創建 Babel config file -- babel.config.json
{ "presets": [ "@babel/preset-env" ], "plugins": [ [ "@babel/plugin-proposal-pipeline-operator", { "topicToken": "^^", "proposal": "fsharp" } ] ] }
src/main.js
function sum(numbers) { return numbers.reduce((total, number) => total + number, 0); } function removeOdd(numbers) { return numbers.filter(number => number % 2 === 0); } function removeOddOrEven(numbers, oddOrEven) { return numbers.filter(num => oddOrEven === 'odd' ? num % 2 === 0 : num % 2); } function pipe(source, ...pipeFns) { let result = source; for (const pipeFn of pipeFns) { const fnReturn = pipeFn(result); if (fnReturn !== undefined) { result = fnReturn; } } return result; } function wrapToPipeFn(pipeFn) { return (...args) =>value => pipeFn(value, ...args) } const removeOddOrEvenPipe = wrapToPipeFn(removeOddOrEven); const numbers = [1, 2, 3, 4, 5, 6]; // const total = pipe(numbers, (numbers) => removeOddOrEven(numbers, 'odd'), sum); const total = numbers |> numbers => removeOddOrEven(numbers, 'odd') |> sum; console.log('total', total); // 12
src/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script src="/lib/main.js"></script> </body> </html>
執行 command
yarn run babel src -d lib --watch
用 Live Server 打開 src/index.html 就可以了

以上。

浙公網安備 33010602011771號