Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

实现异步的reduce #2

Open
leeword opened this issue Mar 15, 2021 · 1 comment
Open

实现异步的reduce #2

leeword opened this issue Mar 15, 2021 · 1 comment

Comments

@leeword
Copy link
Owner

leeword commented Mar 15, 2021

背景

最近看到一个比较有意思的题目,如何实现异步的 reduce。数组原生的reduce方法非常强大,合理使用可以使代码更加简洁明朗,如果实现异步版本 ,能做的事情就多了,这是非常有价值的一件事情。

原生 reduce 简介

reduceES5 版本新增的数组方法,它挂载在 JavaScript 内置对象 Array 的原型上,只能处理同步的场景。 reducer 的理念也影响了状态管理库 redux 的设计。

MDN 上是这样描述的:

reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

语法

arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

示例如下:

const array1 = [1, 2, 3, 4];
const reducer = (accumulator, currentValue) => accumulator + currentValue;

// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10

// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15

可以观察到,它接收两个参数, 回调函数(callback)是必传参数,第二项初始值(initialValue)非必传。
reduce 执行时,会内部遍历数组每一项,执行 callback 函数,并将它的返回值 accumulator 当作callback的第一位参数传入。

MDN 介绍它的参数有个点吸引了我的注意:
image

第二个参数(initialValue)的提供与否,会影响到数组索引0值的处理逻辑:

  • 如果提供了第二个参数初始值,那数组将正常进行遍历操作;
  • 假如没有提供初始值,数组将从索引1开始遍历,索引0的值,将会当作初始值传入回调函数;

也就是第一次执行 callback 时,它的第一个参数传入的,要么是初始值,要么是数组第一项。

怎么实现一个异步版本呢?先找一下有没有类似的实现。

MDN上的异步版本

MDN上有一个比较满足需求的实现,代码如下(为了篇幅删减了部分代码):

/**
 * Runs promises from array of functions that can return promises
 * in chained manner
 *
 * @param {array} arr - promise arr
 * @return {Object} promise object
 */
function runPromiseInSequence(arr, input) {
  return arr.reduce(
    (promiseChain, currentFunction) => promiseChain.then(currentFunction),
    Promise.resolve(input)
  );
}

// promise function 1
function p1(a) {
  return new Promise((resolve, reject) => {
    resolve(a * 5);
  });
}

// function 3  - will be wrapped in a resolved promise by .then()
function f3(a) {
 return a * 3;
}

// promise function 4
function p4(a) {
  return new Promise((resolve, reject) => {
    resolve(a * 4);
  });
}

const promiseArr = [p1, f3, p4];

runPromiseInSequence(promiseArr, 10)
  .then(console.log);   // 600

可以看到,核心函数是 runPromiseInSequence 方法,它对原生 reduce 进行了一次封装,利用 Promise chain 控制异步执行顺序,数组的每一项都是函数,在遍历执行时当作回调函数传入.then 方法,可谓是非常精巧。

到这里我们的目的已经达到了,文章。。。完。

开个小玩笑,我们来分析一下:

优点很明显,通过简单的封装,确实实现了异步流程控制,但缺点也很明显:

  • 首先,数组的每一项必须是 function,以前使用 reduce 的一些数组比如 ['a', 'b' ,'c'].reduce(...)无法平滑的切换到该方法,适用场景受到了限制。假如数组中的项不为 function,必须包装一层,因为 .then 方法只接收回调函数,这是第一点;

  • 第二点,runPromiseInSequence 本身的问题,直接上代码:

    function runPromiseInSequence(arr, input) {
      return arr.reduce(
        // 当参数 input 为函数类型时,必须判断传入 currentFunction 的值是否为函数,否则会出问题
        (promiseChain, currentFunction) => promiseChain.then(currentFunction),
        // 必须传入初始值
        Promise.resolve(input)
      );
    }
  • 第三点,需要自己手动实现 Promise chain 链;

是否可以不对数组每一项的类型做限制,并且将链式调用封装到方法内部呢?

实现一个异步的 reduce 吧!

同步版本的实现

先来写一个同步 reduce ,理解下它内部的实现原理,再考虑改造。

文章开头介绍过原生 reduce方法的大致情况,了解了这些信息,可以着手构建一个 reduce 函数了。笔者打算写一个独立函数,不将方法放到 Array 的原型上实现,迭代数组直接当作一个参数传入,下面是一个具体实现:

一个同步reduce 实现

/**
 * @description 同步的迭代器
 * @param { Array|ArrayLike|string } array 数组/类数组/字符串
 * @param { Function } callback 回调函数
 * @param { any } [initialValue] 初始值
 * @returns { any }
 */
function syncReduce(array, callback, initialValue) {
    if (array === null) {
        throw new TypeError('in function syncReduce, the parameter `array` called on null');
    }
    if (typeof callback !== 'function') {
        throw new TypeError('parameter `callback` should be a function');
    }

    const hasInitialValue = typeof initialValue !== 'undefined';
    // 根据是否有初始值,处理数组遍历的开始下标
    const startIndex = hasInitialValue ? 0 : 1;
    let accumulator = hasInitialValue ? initialValue : array[0];

    for (let index = startIndex; index < array.length; index++) {
      	const currentEl = array[index];
        accumulator = callback(accumulator, currentEl, index, array);
    }

    return accumulator;
}
  1. 对参数类型进行了简单的判断,如果类型不正确则直接抛一个异常,错误信息可以帮助使用该方法的人,更好的理解函数的操作;
  2. 然后根据是否传入初始值进行一些处理;
  3. 紧接着则是遍历数组的每一项,应用传入的回调函数,并将返回值当作第一个参数传入回调;
  4. 最后将汇总的值返回;

到这里,我们已经实现了 reduce 的同步版本,如何将它改造成异步函数呢?

异步版本的实现

首先要确定异步的方式,回调 or Promise。 回调的问题这里不做过多的赘述,现代工程中的异步,构筑在强大的 Promise 对象之上,Promise 很适合做异步流程管理。我们可以在遍历数组时做一些文章,允许 callback 返回一个 Promise,等待它状态凝固后再进行下一次的遍历。
for 循环不支持基于 Promise 的异步,需要寻找一个替代品,ES9 加入了异步的“for”循环写法,即 for await...of ,用于遍历异步可迭代对象。

基于 for await...of异步 reduce 实现

/**
 * @description 异步的迭代器
 * @param { Array|ArrayLike|string } array 数组/类数组/字符串
 * @param { Function } callback 若为异步函数则返回一个 promise
 * @param { any } [initialValue] 初始值
 * @returns { Promise }
 */
async function asyncReduce(array, callback, initialValue) {
    if (array === null) {
        throw new TypeError('in function asyncReduce, the parameter `array` called on null');
    }
    if (typeof callback !== 'function') {
        throw new TypeError('parameter `callback` should be a function');
    }

    const hasInitialValue = typeof initialValue !== 'undefined';
    // 根据是否有初始值,处理数组遍历的开始下标
    const startIndex = hasInitialValue ? 0 : 1;
    let accumulator = hasInitialValue ? initialValue : array[0];
    // 传参可能为类数组或字符串,使用 call 调用 slice 方法复制源数据
    const iterableSource = Array.prototype.slice.call(array).map((value, index) => ({ value, index })).slice(startIndex);

    for await (let { value, index } of iterableSource) {
        accumulator = await callback(accumulator, value, index, array);
    }

    return accumulator;
}

上述的代码调用循环的前一行,生成了新的对象iterableSource,主要为了保存待迭代对象的索引,因为使用 for await...of 进行迭代时,只能拿到当前迭代的值,无法拿到它的索引。
如果array 很大,这么做无疑会有性能问题,最优解应部署 Symbol.asyncIterator 接口,有兴趣的小伙伴可自行实现,这里仅提供思路。

for await...of 语法比较新,能否仅仅使用ES6 的语法实现这个特性呢,答案是肯定的,循环可用递归代替。

基于递归 + Promise异步 reduce 实现

/**
 * @description 异步的迭代器
 * @param { Array|ArrayLike|string } array 数组/类数组/字符串
 * @param { Function } callback 若为异步函数则返回一个 promise
 * @param { any } [initialValue] 初始值
 * @returns { Promise }
 */
function asyncReduce(array, callback, initialValue) {
    if (array === null) {
        throw new TypeError('in function asyncReduce, the parameter `array` called on null');
    }
    if (typeof callback !== 'function') {
        throw new TypeError('parameter `callback` should be a function');
    }

    function helper(accumulator, el, index, array) {
      	if (index < array.length) {
            return Promise.resolve(
                   callback(accumulator, el, index, array)
               )
               .then(data => {
                   let current = array[++index];

                   return helper(data, current, index, array);
               });
        }

      	return accumulator;
    }

    const hasInitialValue = typeof initialValue !== 'undefined';
    const startIndex = hasInitialValue ? 0 : 1;
    const accumulator = hasInitialValue ? initialValue : array[0];
    // 不会进行遍历操作, 直接将值包装返回
    if (!array.length || (array.length === 1 && !hasInitialValue)) {
        return Promise.resolve(accumulator);
    }

    return helper(accumulator, array[startIndex], startIndex, array);
}

写到这里,尽管没有尽善尽美,比如:未对稀疏数组做处理(借用for...in遍历键名),但异步流程控制的功能已经迁移到 reduce 函数内部实现,功能也做到了兼容数组各类值。希望本文可以为你带来一点启发。

参考链接:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce

@Lucky-girl12
Copy link

循序渐进,让人很好理解

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants