资讯专栏INFORMATION COLUMN

一个有味道的函数

megatron / 2965人阅读

摘要:所以我们分析这个新需求的效果我们在函数执行到一半时,执行了,的返回值为后续函数的执行返回值。也就是说,我们在中处理,直接调用队列中的下一个函数即可然后监听和回调,即可在当前函数中获取到返回值拿到返回值后就可以执行我们后续的代码。

最近想到了一个自认为很有意思的面试题
如何实现一个compose函数。
函数接收数个参数,参数均为Function类型,右侧函数的执行结果将作为左侧函数执行的参数来调用。
compose(arg => `${arg}%`, arg => arg.toFixed(2), arg => arg + 10)(5) // 15.00%
compose(arg => arg.toFixed(2), arg => arg + 10)(5) // 15.00
compose(arg => arg + 10)(5) // 15

执行结果如上述代码,有兴趣的同学可以先自己实现一下再来看后续的。

1.0实现方案

大致的思路为:

获取所有的参数

调用最后一个函数,并接收返回值

如果没有后续的函数,返回数据,如果有,将返回值放入下一个函数中执行

所以这种情况用递归来实现会比较清晰一些

function compose (...funcs) {
  return function exec (arg) {
    let func = funcs.pop()
    let result = func(arg) // 执行函数,获取返回值

    // 如果后续还有函数,将返回值放入下一个函数执行
    // 如果后续没有了,直接返回
    return funcs.length ? exec(result) : result
  }
}

这样,我们就实现了上述的compose函数。
真是可喜可贺,可喜可贺。

本文完。

好了,如果现实生活中开发做需求也是如此爽快不做作就好了,但是,产品总是会来的,需求总是会改的。

2.0需求变更

我们现在有如下要求,函数需要支持Promise对象,而且要兼容普通函数的方式。
示例代码如下:

// 为方便阅读修改的排版
compose(
  arg => new Promise((resolve, reject) =>
    setTimeout(_ =>
      resolve(arg.toFixed(2)),
      1000
    )
  ),
  arg => arg + 10
)(5).then(data => {
  console.log(data) // 15.00
})

我们有如下代码调用,对toFixed函数的调用添加1000ms的延迟。让用户觉得这个函数执行很慢,方便下次优化

所以,我们就需要去修改compose函数了。
我们之前的代码只能支持普通函数的处理,现在因为添加了Promise对象的原因,所以我们要进行如下修改:

首先,异步函数改为同步函数是不存在的readFile/readFileSync这类除外。
所以,最简单的方式就是,我们将普通函数改为异步函数,也就是在普通函数外包一层Promise

function compose (...funcs) {
  return function exec (arg) {
    return new Promise((resolve, reject) => {
      let func = funcs.pop()

      let result = promiseify(func(arg)) // 执行函数,获取返回值,并将返回值转换为`Promise`对象

      // 注册`Promise`的`then`事件,并在里边进行下一次函数执行的准备
      // 判断后续是否还存在函数,如果有,继续执行
      // 如果没有,直接返回结果
      result.then(data => funcs.length ?
        exec(data).then(resolve).catch(reject) :
        resolve(data)
      ).catch(reject)
    })
  }
}

// 判断参数是否为`Promise`
function isPromise (pro) {
  return pro instanceof Promise
}

// 将参数转换为`Promise`
function promiseify (pro) {
  // 如果结果为`Promise`,直接返回
  if (isPromise(pro)) return pro
  // 如果结果为这些基本类型,说明是普通函数
  // 我们给他包一层`Promise.resolve`
  if (["string", "number", "regexp", "object"].includes(typeof pro)) return Promise.resolve(pro)
}

我们针对compose代码的改动主要是集中在这几处:

compose的返回值改为了Promise对象,这个是必然的,因为内部可能会包含Promise参数,所以我们一定要返回一个Promise对象

将各个函数执行的返回值包装为了Promise对象,为了统一返回值。

处理函数返回值,监听thencatch、并将resolvereject传递了过去。

3.0终极版

现在,我们又得到了一个新的需求,我们想要在其中某些函数执行中跳过部分代码,先执行后续的函数,等到后续函数执行完后,再拿到返回值执行剩余的代码:

compose(
  data => new Promise((resolve, reject) => resolve(data + 2.5)),
  data => new Promise((resolve, reject) => resolve(data + 2.5)),
  async function c (data, next) { // async/await为Promise语法糖,不赘述
    data += 10 // 数值 + 10
    let result = await next(data) // 先执行后续的代码

    result -= 5  // 数值 - 5

    return result
  },
  (data, next) => new Promise((resolve, reject) => {
    next(data).then(data => {
      data = data / 100 // 将数值除以100限制百分比
      resolve(`${data}%`)
    }).catch(reject) // 先执行后续的代码
  }),
  function d (data) { return data + 20 }
)(15).then(console.log) // 0.45%

拿到需求后,陷入沉思。。。
好好地顺序执行代码,突然就变成了这个鸟样,随时可能会跳到后边的函数去。
所以我们分析这个新需求的效果:

我们在函数执行到一半时,执行了nextnext的返回值为后续函数的执行返回值。
也就是说,我们在next中处理,直接调用队列中的下一个函数即可;
然后监听thencatch回调,即可在当前函数中获取到返回值;
拿到返回值后就可以执行我们后续的代码。

然后他的实现呢,也是非常的简单,我们只需要修改如下代码即可完成操作:

// 在这里会强行调用`exec`并传入参数
// 而`exec`的执行,则意味着`funcs`集合中又一个函数被从队列中取出来
promiseify(func(arg, arg => exec(arg)))

也就是说,我们会提前执行下一个函数,而且下一个函数的then事件注册是在我们当前函数内部的,当我们拿到返回值后,就可以进行后续的处理了。
而我们所有的函数是存放在一个队列里的,在我们提前执行完毕该函数后,后续的执行也就不会再出现了。避免了一个函数被重复执行的问题。

如果看到这里已经很明白了,那么恭喜,你已经了解了实现koajs最核心的代码:
中间件的实现方式洋葱模型

想必现在整个函数周遭散发着洋葱的味道。

参考资料

koa-compose

相关示例代码仓库

1.0,普通函数
2.0,Promise函数
3.0,支持洋葱模型

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93790.html

相关文章

  • 重构改善既代码设计(代码味道

    摘要:坏的味道指的是应该被修改,被重构的代码,不具有可读性,复用性,判断逻辑复杂,冗余代码。它们通常能指出代码用途和实现手法之间的语义距离。把所有和这个变量相关的代码新建一个类放入。但这往往不够,请反复运用将某些行为移入类,直到者的协议一致为止。 坏的味道:指的是应该被修改,被重构的代码,不具有可读性,复用性,判断逻辑复杂,冗余代码。应该使用各种重构的手法去改变它! Duplicated...

    Code4App 评论0 收藏0
  • 重构-改善既代码设计(三)--代码味道

    摘要:坏味道的代码重复代码会自动标注重复的代码。一般都是遇到真实情况后才考虑得到霰弹式修改添加或修改一个功能引发多个类相应修改遇到这种情况可以移动代码,将需要修改的代码都放在同一个类下。被拒绝的遗赠子类应该继承超类的函数和数据。 坏味道的代码 重复代码 idea会自动标注重复的代码。一般重复代码就是可以重构的点。 同一个类的两个函数还有相同的表达式,这时需要提炼出重复代码。 两个互为兄弟的...

    Mr_houzi 评论0 收藏0
  • 重构:一项常常被忽略基本功

    摘要:无论如何,单元测试一直是一中非常重要却常常被忽视的技能。在实践中,重构的要求是很高的它需要有足够详尽的单元测试,需要有持续集成的环境,需要随时随地在小步伐地永远让代码处于可工作状态下去进行改善。 showImg(https://segmentfault.com/img/bVbttWF?w=1000&h=528); 五月初的时候朋友和我说《重构》出第 2 版了,我才兴冲冲地下单,花了一个...

    idealcn 评论0 收藏0
  • js 零碎知识整理

    摘要:,看了这样的解释,或许让你更摸不着头脑了。弹出弹出在这个例子中,只要会使用浏览器的朋友,都能看得出来完全继承了的属性和方法,否则是无法解释的,因为在中并没有定义属性和,那么按常理推断在的实例对象中,并不会出现这两个属性。 快速删除尾部数组 var arr=[1,2,3,4,5]; arr.length=3; console.log(arr)//[1,2,3] 直接改变数组的length...

    liaorio 评论0 收藏0
  • 前端基本功-示例代码 (二)

    摘要:前端基本功示例代码一点这里前端基本功示例代码二点这里一像素伪类实现对于老项目,有没有什么办法能兼容的尴尬问题了,个人认为伪类是比较完美的方法了。 前端基本功-示例代码 (一) 点这里前端基本功-示例代码 (二) 点这里 1.一像素 伪类 + transform 实现对于老项目,有没有什么办法能兼容1px的尴尬问题了,个人认为伪类+transform是比较完美的方法了。 原理是把原先元素...

    Java3y 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<