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 就可以了
以上。