Open leeword opened 3 years ago
最近看到一个比较有意思的题目,如何实现异步的 reduce。数组原生的reduce方法非常强大,合理使用可以使代码更加简洁明朗,如果实现异步版本 ,能做的事情就多了,这是非常有价值的一件事情。
reduce
reduce 是 ES5 版本新增的数组方法,它挂载在 JavaScript 内置对象 Array 的原型上,只能处理同步的场景。 reducer 的理念也影响了状态管理库 redux 的设计。
JavaScript
Array
reducer
redux
MDN 上是这样描述的:
reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。 语法 arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
reduce()
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的第一位参数传入。
callback
initialValue
accumulator
MDN 介绍它的参数有个点吸引了我的注意:
第二个参数(initialValue)的提供与否,会影响到数组索引0值的处理逻辑:
也就是第一次执行 callback 时,它的第一个参数传入的,要么是初始值,要么是数组第一项。
怎么实现一个异步版本呢?先找一下有没有类似的实现。
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 方法,可谓是非常精巧。
runPromiseInSequence
Promise chain
.then
到这里我们的目的已经达到了,文章。。。完。
开个小玩笑,我们来分析一下:
优点很明显,通过简单的封装,确实实现了异步流程控制,但缺点也很明显:
首先,数组的每一项必须是 function,以前使用 reduce 的一些数组比如 ['a', 'b' ,'c'].reduce(...)无法平滑的切换到该方法,适用场景受到了限制。假如数组中的项不为 function,必须包装一层,因为 .then 方法只接收回调函数,这是第一点;
function
['a', 'b' ,'c'].reduce(...)
第二点,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 的原型上实现,迭代数组直接当作一个参数传入,下面是一个具体实现:
/** * @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; }
到这里,我们已经实现了 reduce 的同步版本,如何将它改造成异步函数呢?
首先要确定异步的方式,回调 or Promise。 回调的问题这里不做过多的赘述,现代工程中的异步,构筑在强大的 Promise 对象之上,Promise 很适合做异步流程管理。我们可以在遍历数组时做一些文章,允许 callback 返回一个 Promise,等待它状态凝固后再进行下一次的遍历。 for 循环不支持基于 Promise 的异步,需要寻找一个替代品,ES9 加入了异步的“for”循环写法,即 for await...of ,用于遍历异步可迭代对象。
Promise
for
for await...of
/** * @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 接口,有兴趣的小伙伴可自行实现,这里仅提供思路。
iterableSource
array
Symbol.asyncIterator
for await...of 语法比较新,能否仅仅使用ES6 的语法实现这个特性呢,答案是肯定的,循环可用递归代替。
ES6
/** * @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 函数内部实现,功能也做到了兼容数组各类值。希望本文可以为你带来一点启发。
for...in
参考链接:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce
循序渐进,让人很好理解
背景
最近看到一个比较有意思的题目,如何实现异步的
reduce
。数组原生的reduce
方法非常强大,合理使用可以使代码更加简洁明朗,如果实现异步版本 ,能做的事情就多了,这是非常有价值的一件事情。原生
reduce
简介reduce
是 ES5 版本新增的数组方法,它挂载在JavaScript
内置对象Array
的原型上,只能处理同步的场景。reducer
的理念也影响了状态管理库redux
的设计。MDN 上是这样描述的:
示例如下:
可以观察到,它接收两个参数, 回调函数(
callback
)是必传参数,第二项初始值(initialValue
)非必传。reduce
执行时,会内部遍历数组每一项,执行callback
函数,并将它的返回值accumulator
当作callback
的第一位参数传入。MDN 介绍它的参数有个点吸引了我的注意:
第二个参数(
initialValue
)的提供与否,会影响到数组索引0值的处理逻辑:也就是第一次执行
callback
时,它的第一个参数传入的,要么是初始值,要么是数组第一项。怎么实现一个异步版本呢?先找一下有没有类似的实现。
MDN上的异步版本
MDN上有一个比较满足需求的实现,代码如下(为了篇幅删减了部分代码):
可以看到,核心函数是
runPromiseInSequence
方法,它对原生reduce
进行了一次封装,利用Promise chain
控制异步执行顺序,数组的每一项都是函数,在遍历执行时当作回调函数传入.then
方法,可谓是非常精巧。到这里我们的目的已经达到了,文章。。。完。
开个小玩笑,我们来分析一下:
优点很明显,通过简单的封装,确实实现了异步流程控制,但缺点也很明显:
首先,数组的每一项必须是
function
,以前使用reduce
的一些数组比如['a', 'b' ,'c'].reduce(...)
无法平滑的切换到该方法,适用场景受到了限制。假如数组中的项不为function
,必须包装一层,因为.then
方法只接收回调函数,这是第一点;第二点,
runPromiseInSequence
本身的问题,直接上代码:第三点,需要自己手动实现
Promise chain
链;是否可以不对数组每一项的类型做限制,并且将链式调用封装到方法内部呢?
实现一个异步的
reduce
吧!同步版本的实现
先来写一个同步
reduce
,理解下它内部的实现原理,再考虑改造。文章开头介绍过原生
reduce
方法的大致情况,了解了这些信息,可以着手构建一个reduce
函数了。笔者打算写一个独立函数,不将方法放到Array
的原型上实现,迭代数组直接当作一个参数传入,下面是一个具体实现:一个同步的
reduce
实现到这里,我们已经实现了
reduce
的同步版本,如何将它改造成异步函数呢?异步版本的实现
首先要确定异步的方式,回调 or
Promise
。 回调的问题这里不做过多的赘述,现代工程中的异步,构筑在强大的Promise
对象之上,Promise
很适合做异步流程管理。我们可以在遍历数组时做一些文章,允许callback
返回一个Promise
,等待它状态凝固后再进行下一次的遍历。for
循环不支持基于Promise
的异步,需要寻找一个替代品,ES9 加入了异步的“for”循环写法,即for await...of
,用于遍历异步可迭代对象。基于
for await...of
的异步reduce
实现上述的代码调用循环的前一行,生成了新的对象
iterableSource
,主要为了保存待迭代对象的索引,因为使用for await...of
进行迭代时,只能拿到当前迭代的值,无法拿到它的索引。 如果array
很大,这么做无疑会有性能问题,最优解应部署Symbol.asyncIterator
接口,有兴趣的小伙伴可自行实现,这里仅提供思路。for await...of
语法比较新,能否仅仅使用ES6
的语法实现这个特性呢,答案是肯定的,循环可用递归代替。基于递归 +
Promise
的异步reduce
实现写到这里,尽管没有尽善尽美,比如:未对稀疏数组做处理(借用
for...in
遍历键名),但异步流程控制的功能已经迁移到reduce
函数内部实现,功能也做到了兼容数组各类值。希望本文可以为你带来一点启发。参考链接:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce