前端必刷手写题系列 [24]

1,079 阅读5分钟

这是我参与8月更文挑战的第28天,活动详情查看:8月更文挑战

这个系列也没啥花头,就是来整平时面试的一些手写函数,考这些简单实现的好处是能看出基本编码水平,且占用时间不长,更全面地看出你的代码实力如何。一般不会出有很多边界条件的问题,那样面试时间不够用,考察不全面。

平时被考到的 api 如果不知道或不清楚,直接问面试官就行, api 怎么用这些 Google 下谁都能马上了解的知识也看不出水平。关键是在实现过程,和你的编码状态习惯思路清晰程度等。

注意是简单实现,不是完整实现,重要的是概念清晰实现思路清晰,建议先解释清楚概念 => 写用例 => 写伪代码 => 再实现具体功能,再优化,一步步来。

34. 异步求和

题目

假设有一台本地机器,无法做加减乘除运算,因此无法执行 a + b、a += 1 这样的 js 代码,然后我们提供一个服务器端的 HTTP API,可以传两个数字类型的参数,响应结果是这两个参数的和,这个 HTTP API 的 JS SDK(在本地机器上运行)的使用方法如下:

asyncAdd(3, 5, (err, result) => {
    console.log(result);  // 8
});

其实就是把这个 加运算变成了需要异步调用接口来获得和

我们为了调试,可以简单模拟实现这个异步接口

function asyncAdd(a, b, cb) {
  setTimeout(() => {
    cb(null, a + b);
  }, Math.floor(Math.random() * 100))
}

你可以合并上面代码运行在浏览器试试, 成功得到 8 这个答案

那么问题就是需要你实现一个方法 sum, 支持如下调用方式,并输出正确结果

(async () => {
  const result1 = await sum(1, 4, 6, 9, 1, 4);
  const result2 = await sum(3, 4, 9, 2, 5, 3, 2, 1, 7);
  const result3 = await sum(1, 6, 0, 5);
  console.log([result1, result2, result3]); // [25, 36, 12]
})()

分析

题目叙述完毕,我们了解了背景之后进行思考,sum 接受很多个参数,一个个加过去,但是又需要继续进行家和,而且是调用接口返回异步的结果作为和。想到 Promise 这个东西有 then 和进行链式调用 then().then().then()... 想到之前的 那个并发控制的实现吗,有点类似。

或者我们想到 async/await 同步方式编写异步,虽然底层还是 Promise, 但代码更简单,下面来看代码注释

手写实现

其实核心就是用 promise 包装一层异步函数

function asyncAdd(a, b, cb) {
  setTimeout(() => {
    cb(null, a + b);
  }, Math.floor(Math.random() * 100))
}

function promiseAsyncAdd(a, b) {
  // 如果是 0 直接返回另一个数
  if (a === 0) return b
  // 用 Promise 包一层是要利用它能用 then进行链式调用
  return new Promise((resolve, reject) => {
    asyncAdd(a, b, (err, res) => {
      resolve(res);
    });
  })
}

// 我们尝试着调用下这个函数, 没问题
// promiseAsyncAdd(1, 2).then(res => {
//     console.log(res) // 3
// })

// 下面就是实现sum, 先用 then链式调用
function sum() {
  let args = Array.from(arguments)
  // 初始化一个 Promise, 和是 0
  let p = Promise.resolve(0)
  return args.reduce((acc, cur) => {
    // 利用 promise 可链式调用
    p = p.then(res => {
      return promiseAsyncAdd(res, cur)
    })
    return p.then(res => {
      // res 是每一轮 reduce计算的和 return出去
      return res
    })
  }, 0)
}

(async () => {
  const result1 = await sum(1, 4, 6, 9, 1, 4);
  const result2 = await sum(3, 4, 9, 2, 5, 3, 2, 1, 7);
  const result3 = await sum(1, 6, 0, 5);
  console.log([result1, result2, result3]); // [25, 36, 12]
})()

// 成功输出 [25, 36, 12]

我们另外,写一下 async/await 写法

async function sum() {
  const args = [...arguments];
  let sum = 0
  // 这里 i++ 也不给用 可以用 i--
  for (let i = args.length - 1; i >= 0; i--) {
    sum = await promiseAsyncAdd(sum, args[i]);
  }
  return sum;
}

那么8月最后一天更文,再加点料,提升效率

我们是希望这个执行时间更短,用

  • console.time
  • console.timeEnd

这两个 api 来测量一个 javascript 脚本程序执行消耗的时间。

// 启动计时器
console.time('testCodeTag');

// 需要计时的代码 标识 'testCodeTag' 之间的代码 比如下面的 for 循环执行时间
let res = []
for(let i = 0; i < 10000; i++) {
    res.push(i * 5)
}

// 停止计时,输出时间
console.timeEnd('testCodeTag');
// 打印结果: testCodeTag: 0.792ms

接下来我们测量上面的代码效率, 先测 then

function asyncAdd(a, b, cb) {
  setTimeout(() => {
    cb(null, a + b);
  }, Math.floor(50)) // 注意因为这个我们要计时就把它固定
}

(async () => {
  console.time('testCodeTag');
  const result1 = await sum(1, 4, 6, 9, 1, 4);
  const result2 = await sum(3, 4, 9, 2, 5, 3, 2, 1, 7);
  const result3 = await sum(1, 6, 0, 5);
  console.log([result1, result2, result3]); // [25, 36, 12]
  console.timeEnd('testCodeTag');
  // testCodeTag: 895.751ms
})()

再测 async/await 的, 大概 896ms 两种差不多

我们思考有什么方式去更加缩短这个时间呢, 想到 Promise.all并发的,会不会增加速度

这个具体写法我先来个思路,两两一组,合并成一次异步调用,我们现在有很多个异步调用,用Promise.all 来一波,之后进行下一轮再分组,all 一波直到最后剩一个和。

function asyncAdd(a, b, cb) {
  setTimeout(() => {
    cb(null, a + b);
  }, Math.floor(50)) // 注意因为这个我们要计时就把它固定
}

function promiseAsyncAdd(a, b) {
  // 如果是 0 直接返回另一个数
  if (a === 0) return b
  // 用 Promise 包一层是要利用它能用 then进行链式调用
  return new Promise((resolve, reject) => {
    asyncAdd(a, b, (err, res) => {
      resolve(res);
    });
  })
}

// 拆分出来的并发方法
async function concurrentPromise(promises) {
  return await Promise.all(promises).then(res => {
    return res
  })
} 

async function sum() {
  let res = 0
  const args = [...arguments];
  let promiseArr = []
  // 两两分组
  args.forEach(async (item, i) => {
    if ((i+1) % 2 === 1) {
      promiseArr.push(promiseAsyncAdd(item, args[i+1] || 0))
    }
  })
  // 然后并发执行分组后的所有 promise
  let endProRes = await concurrentPromise(promiseArr)
  // 如果结果长度递归到只剩最后的一个和
  if (endProRes.length > 1) {
    res = sum(...endProRes)
  } else {
    res = endProRes[0]
  }
  return res;
}

(async () => {
  console.time('testCodeTag');
  const result1 = await sum(1, 4, 6, 9, 1, 4);
  const result2 = await sum(3, 4, 9, 2, 5, 3, 2, 1, 7);
  const result3 = await sum(1, 6, 0, 5);
  console.log([result1, result2, result3]); // [25, 36, 12]
  console.timeEnd('testCodeTag');
  // testCodeTag: 487.948ms
})()

果然执行时间果然变短了,效率提升不少。

另外向大家着重推荐下另一个系列的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列 记得点赞哈

今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 点击此处交个朋友 Or 搜索我的微信号infinity_9368,可以聊天说地 加我暗号 "天王盖地虎" 下一句的英文,验证消息请发给我 presious tower shock the rever monster,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考