leeword / blog

总结 沉淀 分享
3 stars 0 forks source link

实现异步的reduce #2

Open leeword opened 3 years ago

leeword commented 3 years ago

背景

最近看到一个比较有意思的题目,如何实现异步的 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值的处理逻辑:

也就是第一次执行 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 方法,可谓是非常精巧。

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

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

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

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

实现一个异步的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 commented 3 years ago

循序渐进,让人很好理解