Callback
首发于Callback

从链式调用到管道组合

写在前面的

在开始正文之前我想谈些与此文相关性很低的话题。对这部分不感兴趣的读者可直接跳过。

在我发表上一篇文章或许我们在 JavaScript 中不需要 this 和 class之后,看到一种评论比较有代表性。此评论认为我们应该以 MDN 文档为指南。MDN 推荐的写法,应当是无需质疑的写法。

我不这么看。

MDN 文档不是用来学习的,它是你在不确定某个具体语法时的参考指南。它是 JavaScript 使用说明书。说明书一般都不带主观思辨,它无法提供指引。就比如你光是把市面上的所有调味料买来,看它们的说明书,你还是学不会怎么做菜的……

很碰巧我看的比较多的一些 JS 教程,都比较主观,与 MDN 有很多偏差。比如 Kyle Simpson 认为 JS 里面根本没有继承,提供 new 操作符以及 class 语法糖是在误导开发者。JS 原型链的正确用法应该是代理而不是继承。(我同意他)

更明显的例子是 Douglas Crockford,他认为 JS 中处理异步编程的主流方案—— callback hell, promise, async/await 全都错了。你在看他的论述之前有十足把握断定他在胡说吗?他在 How JavaScript Works 里面论述了他对事件编程(Eventual Programming)的看法,并写了个完整的库,提供他的解决方案。

批判和辩证地看问题,我们才能进步。

引言

我之前有两篇文章写过 JS 里面惰性求值的实现,但都是浅尝辄止,没有过横向扩展的打算。相关工作已经有人做了(如 lazy.js),我再做意义就不大了。这周在 GitHub 上看到有人写了个类似的库,用原生 Generator/Iterator 实现,人气还很高。我一看还是有人在写,我也试试吧。然后我就用 Douglas Crockford 倡导的一种编程风格去写这个库,想验证下这种写法是否可行。

Crockford 倡导的写法是,不用 this 和原型链,不用 ES6 Generator/Iterator,不用箭头函数…… 数据封装则用工厂函数来实现。

Douglas Functions

首先,如果不用 ES6 Generator 的话,我们得自己实现一个 Generator,这个比较简单:

function getGeneratorFromList(list) {
  let index = 0;
  return function next() {
    if (index < list.length) {
      const result = list[index];
      index += 1;
      return result;
    }
  };
}

// 例子:
const next = getGeneratorFromList([1, 2, 3]);
next(); // 1
next(); // 2
next(); // 3
next(); // undefined

ES6 给数组提供了 [Symbol.Iterator] 属性,给数据赋予了行为,很方便我们进行惰性求值操作。而抛弃了 ES6 提供的这个便利之后,我们就只有手动将数据转换成行为了。来看看怎么做:

function Sequence(list) {
  const iterable = Array.isArray(list)
    ? { next: getGeneratorFromList(list) }
    : list;
}

如果给 Sequence 传入原生数组的话,它会将数组传给 getGeneratorFromList,生成一个 Generator,这样就完成了数据到行为的转换

最核心的这两个功能写完之后,我们来实现一个 map

function createMapIterable(mapping, { next }) {
  function map() {
    const value = next();
    if (value !== undefined) {
      return mapping(value);
    }
  }
  return { next: map };
}

function Sequence(list) {
  const iterable = Array.isArray(list)
    ? { next: getGeneratorFromList(list) }
    : list;

  function map(mapping) {
    return Sequence(createMapIterable(mapping, iterable));
  }

  return {
    map,
  };
}

map 写完后,我们还需要一个函数帮我们把行为转换回数据

function toList(next) {
  const arr = [];
  let value = next();
  while (value !== undefined) {
    arr.push(value);
    value = next();
  }
  return arr;
}

然后我们就有一个完整的惰性求值的库了,虽然现在它只能 map

function Sequence(list) {
  const iterable = Array.isArray(list)
    ? { next: getGeneratorFromList(list) }
    : list;

  function map(mapping) {
    return Sequence(createMapIterable(mapping, iterable));
  }

  return {
    map,
    toList: () => toList(iterable.next);
  };
}

// 例子:
const double = x => x * 2 // 箭头函数这样用是没问题的啊啊啊,破个例吧
Sequence([1, 3, 6])
  .map(double)
  .toList() // [2,6,12]

再给 Sequence 加个 filter 方法就差不多完整了,其它方法再扩展很简单了。

function createFilterIterable(predicate, { next }) {
  function filter() {
    const value = next();
    if (value !== undefined) {
      if (predicate(value)) {
        return value;
      }
      return filter();
    }
  }
  return {next: filter};
}

function Sequence(list) {
  const iterable = Array.isArray(list)
    ? { next: getGeneratorFromList(list) }
    : list;

  function map(mapping) {
    return Sequence(createMapIterable(mapping, iterable));
  }

  function filter(predicate) {
    return Sequence(createFilterIterable(predicate, iterable));
  }

  return {
    map,
    filter,
    toList: () => toList(iterable.next);
  };
}

// 例子:
Sequence([1, 2, 3])
  .map(triple)
  .filter(isEven)
  .toList() // [6]

看样子接着上面的例子继续扩展就没问题了。

问题

我继续写了十几个函数,如 take, takeWhile, concat, zip 等。直到写到我不知道接着写哪些了,然后我去参考了下 lazy.js 的 API,一看倒吸一口凉气。lazy.js 快 200 个 API 吧(没数过,目测),写完代码还要写文档。我实在不想这么折腾了。更严重的问题不在于工作量,而是这么庞大的 API 数量让我意识到我这种写法的问题。

在使用工厂函数实现链式调用的时候,每次调用都返回了一个新的对象,这个新对象包含了所有的 API。假设有 200 个 API,每次调用都是只取了其中一个,剩下 199 个全扔掉了…… 内存再够用也不能这么玩吧。我有强迫症,受不了这种浪费。

结论就是,如果想实现链式调用,还是用原型链实现比较好。

然而链式调用本身就没问题了吗?虽然用原型链实现的链式调用能省去后续调用的对象创建,但是在初始化的时候也无可避免浪费内存。比如,原型链上有 200 个方法,我只调用其中 10 个,剩下的那 190 个都不需要,但它们还是会在初始化时创建。

我想到了 Rx.js 在版本 5 升级到版本 6 的 API 变动。

// rx.js 5 的写法:
Source.startWith(0)
  .filter(predicate)
  .takeWhile(predicate2)
  .subscribe(() => {});

// rx.js 6 的写法:
import { startWith, filter, takeWhile } from 'rxjs/operators';

Source.pipe(
  startWith(0),
  filter(predicate),
  takeWhile(predicate2)
).subscribe(() => {});

RxJS 6 里面采用了管道组合替代了链式调用。这样子改动之后,想用什么操作符就引用什么,没有多余的操作符初始化,也利于 tree shaking。那么我们就模仿 Rxjs 6 的 API 改写上面的 Sequence 库吧。

用管道组合实现惰性求值

操作符的实现和上面没太大区别,主要区别在操作符的组合方式变了:

function getGeneratorFromList(list) {
  let index = 0;
  return function generate() {
    if (index < list.length) {
      const result = list[index];
      index += 1;
      return result;
    }
  };
}

function toList(sequence) {
  const arr = [];
  let value = sequence();
  while (value !== undefined) {
    arr.push(value);
    value = sequence();
  }
  return arr;
}

// Sequence 函数本身非常轻量,操作符按需引入
function Sequence(list) {
  const initSequence = getGeneratorFromList(list);

  function pipe(...args) {
    return args.reduce((prev, current) => current(prev), initSequence);
  }
  return { pipe };
}

function filter(predicate) {
  return function(sequence) {
    return function filteredSequence() {
      const value = sequence();
      if (value !== undefined) {
        if (predicate(value)) {
          return value;
        }
        return filteredSequence();
      }
    };
  };
}

function map(mapping) {
  return function(sequence) {
    return function mappedSequence() {
      const value = sequence();
      if (value !== undefined) {
        return mapping(value);
      }
    };
  };
}

function take(n) {
  return function(sequence) {
    let count = 0;
    return function() {
      if (count < n) {
        count += 1;
        return sequence();
      }
    };
  };
}

function skipWhile(predicate) {
  return function(sequence) {
    let startTaking = false;
    return function skippedSequence() {
      const value = sequence();
      if (value !== undefined) {
        if (startTaking) {
          return value;
        } else if (!predicate(value)) {
          startTaking = true;
          return value;
        }
        return skippedSequence();
      }
    };
  };
}

function takeUntil(predicate) {
  return function(sequence) {
    return function() {
      const value = sequence();
      if (value !== undefined) {
        if (predicate(value)) {
          return value;
        }
      }
    };
  };
}

Sequence([2, 4, 6, 7, 9, 11, 13]).pipe(
  filter(x => x % 2 === 1),
  skipWhile(y => y < 10),
  toList
); // [11,13]

【重点】

蚂蚁金服保险体验与社区技术组招高级前端开发工程师/专家。我所在的团队,队友们个个都是独当一面。学霸很多,我天天跟着他们学习。(坐在我右手边的同学是清华医学博士。可能是因为玩过手术刀,这位大神撸代码行云流水,全 Vim 撸到底)我们开发了很有社会公益价值的相互宝,接下来会有更多激动人心的产品。有兴趣的同学联系我 ray.hl@alipay.com


参考:

https://medium.freecodecamp.org/lets-experiment-with-functional-generators-and-the-pipeline-operator-in-javascript-520364f97448medium.freecodecamp.org

编辑于 2019-03-29

文章被以下专栏收录