normalize
在写 redux 的时候,我们有时可能会需要将数组进行 Normalization,比如
[{id: 1, value:1}, {id: 2, value: 2}] => { 1: {id: 1, value: 1} 2: {id: 2, value: 2} } 复制代码
使用 reduce
可以先给个初始值 {}
来存放,然后每次只需要将 id => object
就可以了:
export type TUser = { id: number; name: string; age: number; } type TUserEntities = {[key: string]: TUser} function normalize(array: TUser[]) { return array.reduce((prev: TUserEntities, curt) => { prev[curt.id] = curt return prev }, {}) } export default normalize 复制代码
用例
describe('normalize', () => { it('可以 normalize user list', () => { const users: TUser[] = [ {id: 1, name: 'Jack', age: 11}, {id: 2, name: 'Mary', age: 12}, {id: 3, name: 'Nancy', age: 13} ] const result = normalize(users) expect(result).toEqual({ 1: users[0], 2: users[1], 3: users[2] }) }) }) 复制代码
assign
上面例子也只是对 number 做相加,那对象“相加”呢?那就是 Object.assign
嘛,所以用 reduce
去做对象相加也很容易:
function assign<T>(origin: T, ...partial: Partial<T>[]): T { const combinedPartial = partial.reduce((prev, curt) => { return { ...prev, ...curt } }) return { ...origin, ...combinedPartial } } export default assign 复制代码
用例
describe('assign', () => { it('可以合并多个对象', () => { const origin: TItem = { id: 1, name: 'Jack', age: 12 } const changeId = { id: 2 } const changeName = { name: 'Nancy' } const changeAge = { age: 13 } const result = assign<TItem>(origin, changeId, changeName, changeAge) expect(result).toEqual({ id: 2, name: 'Nancy', age: 13 }) }) }) 复制代码
虽然这有点脱裤子放屁,但是如果将数组的里的每个对象都看成 [middleState, middleState, middleState, ...]
,初始值看成 prevState
,最终生成结果看成成 nextState
,是不是很眼熟?那不就是 redux 里的 reducer 嘛,其实也是 reducer 名字的由来。
concat
说了对象“相加”,数组“相加”也简单:
function concat<T> (arrayList: T[][]): T[] { return arrayList.reduce((prev, curt) => { return prev.concat(curt) }, []) } export default concat 复制代码
用例
describe('concat', () => { it('可以连接多个数组', () => { const arrayList = [ [1, 2], [3, 4], [5, 6] ] const result = concat<number>(arrayList) expect(result).toEqual([1, 2, 3, 4, 5, 6]) }) }) 复制代码
functionChain
函数的相加不知道大家会想到什么,我是会想到链式操作,如 a().b().c()....
,使用 reduce
实现,就需要传入这样的数组:[a, b, c]
。
function functionChain (fnList: Function[]) { return fnList.reduce((prev, curt) => { return curt(prev) }, null) } export default functionChain 复制代码
用例
describe('functionChain', () => { it('可以链式调用数组里的函数', () => { const fnList = [ () => 1, (prevResult) => prevResult + 2, (prevResult) => prevResult + 3 ] const result = functionChain(fnList) expect(result).toEqual(6) }) }) 复制代码
promiseChain
既然说到了链式调用,就不得不说 Promise 了,数组元素都是 promise 也是可以进行链式操作的:
function promiseChain (asyncFnArray) { return asyncFnArray.reduce((prev, curt) => { return prev.then((result) => curt(result)) }, Promise.resolve()) } export default promiseChain 复制代码
这里要注意初始值应该为一个 resolved 的 Promise 对象。
用例
describe('functionChain', () => { it('可以链式调用数组里的函数', () => { const fnList = [ () => 1, (prevResult) => prevResult + 2, (prevResult) => prevResult + 3 ] const result = functionChain(fnList) expect(result).toEqual(6) }) }) 复制代码
compose
最后再来说说 compose
函数,这是实现 middeware /洋葱圈模型最重要的组成部分.
为了造这种模型,我们不得不让函数疯狂套娃:
mid1(mid2(mid3())) 复制代码
要构造上面的套娃,这样的函数一般叫做 compose
,使用 reduce
实现如下:
function compose(...funcs) { if (funcs.length === 0) { return arg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((aFunc, bFunc) => (...args) => aFunc(bFunc(...args))) } export default compose 复制代码
用例
describe('compose', () => { beforeEach(() => { jest.spyOn(console, 'log') }) afterEach(() => { clearAllMocks() }) it('可以 compose 多个函数', () => { const aFunc = () => console.log('aFunc') const bFunc = () => console.log('bFunc') const cFunc = () => console.log('cFunc') compose(aFunc, bFunc, cFunc)() expect(console.log).toHaveBeenNthCalledWith(1, 'cFunc') expect(console.log).toHaveBeenNthCalledWith(2, 'bFunc') expect(console.log).toHaveBeenNthCalledWith(3, 'aFunc') }) it('可以使用 next', () => { const aFunc = (next) => () => { console.log('before aFunc') next() console.log('after aFunc') return next } const bFunc = (next) => () => { console.log('before bFunc') next() console.log('after bFunc') return next } const cFunc = (next) => () => { console.log('before cFunc') next() console.log('after cFunc') return next } const next = () => console.log('next') const composedFunc = compose(aFunc, bFunc, cFunc)(next) composedFunc() expect(console.log).toHaveBeenNthCalledWith(1, 'before aFunc') expect(console.log).toHaveBeenNthCalledWith(2, 'before bFunc') expect(console.log).toHaveBeenNthCalledWith(3, 'before cFunc') expect(console.log).toHaveBeenNthCalledWith(4, 'next') expect(console.log).toHaveBeenNthCalledWith(5, 'after cFunc') expect(console.log).toHaveBeenNthCalledWith(6, 'after bFunc') expect(console.log).toHaveBeenNthCalledWith(7, 'after aFunc') }) }) 复制代码
总结
上面的例子仅仅给出了 reduce
实现的最常见的一些工具函数。
就像第2点说的不同类型和不同操作有非常多的组合方式,因此使用 reduce
可以玩出很多种玩法。希望上面给出的这些可以给读者带来一些思考和灵感,在自己项目里多使用 reduce
来减轻对数组的复杂操作。