如果在准备面试,请务必看看这道题,前端编码题中的集大成者,异步sum/add

简介: 如果在准备面试,请务必看看这道题,前端编码题中的集大成者,异步sum/add

最近刷各个大厂面试题,反复出现一道题,让我印象深刻。

废话不多,我们直接上题目

请实现一个 sum 函数,接收一个数组 arr 进行累加,并且只能使用add异步方法

add 函数已实现,模拟异步请求后端返回一个相加后的值

/*
  请实现一个 sum 函数,接收一个数组 arr 进行累加,并且只能使用add异步方法
  add 函数已实现,模拟异步请求后端返回一个相加后的值
*/
function add(a, b) {
  return Promise.resolve(a + b);
}
function sum(arr) {
}

这道题在问什么?

add 函数是返回一个promise, 异步输出a+b 的值,

我们使用add 模拟接口请求,a,b 是给接口的参数,a+b 是接口返回的值。

sum 函数输入一个数组例如[1,2,3,4,5],进行累加,使用add异步方法。

转换成我们业务中的场景就是,

  • 调用接口执行1+2, 接口返回3,
  • 调用接口再执行3+3, 接口返回6
  • 调用接口再执行6+4,接口返回10
  • 调用接口再执行10+5,接口返回15

简单实现

不用多考虑,遍历执行累加就好

  • 借助数组方法 reduce方法实现累加
function add(a, b) {
  return Promise.resolve(a + b);
}
function sum(arr) {
  if (arr.length === 1) return arr[0];
  return arr.reduce((x, y) => Promise.resolve(x).then((x) => add(x, y)));
}

简单实现

不用多考虑,遍历执行累加就好

  • 借助数组方法 reduce方法实现累加
function add(a, b) {
  return Promise.resolve(a + b);
}
function sum(arr) {
  if (arr.length === 1) return arr[0];
  return arr.reduce((x, y) => Promise.resolve(x).then((x) => add(x, y)));
}
  • 借助async/await 实现,原理一样,代码更加清晰
function add(a, b) {
  return Promise.resolve(a + b);
}
async function sum(arr) {
  let s = arr[0]
  for (let i = 1; i < arr.length; i++) {
    s = await add(s, arr[i])
  }
  return s
}

问题

如果add 函数调用接口,假设接口执行完毕返回需要1s,那么一个长度为n 的数组,需要执行n-1次累加,需要的时间为n-1 s

面试官继续问了,有没有什么优化空间呢?

优化实现

既然是异步操作,还是累加操作,也就是说,只要输入的数组是确定的,返回的累加值也就是确定的。

之前从前往后累加,一个一个加。

我现在可以借助Promise.all() 改成并行请求,数组两两一组,进行累加,然后再把和累加。

比如输入[1,2,3,4,5]

第一次请求[1,2] [3,4],拿到接口返回3,7

第二次请求[3,7],拿到接口返回 10

第三次请求[10,5], 拿到接口返回 15

function add(a, b) {
  return Promise.resolve(a + b);
}
// chunk 函数把输入数组两两分组
// 输入[1,2,3,4,5]
// 返回[[1,2],[3,4],5]
function chunk(arr){
  const res = []
  for(let i =0;i<arr.length;i++) {
    const index = Math.floor(i/2)
    res[index] ??= []
    res[index].push(arr[i])
  }
  return res
}
async function sum(arr) {
  if (arr.length === 1) return arr[0];
  const promises = chunk(arr).map(([x, y]) =>
    // 注意此时单数的情况
    y === undefined ? x : add(x, y)
  );
  return Promise.all(promises).then((list) => sum(list));
}

有点类似归并思想,把数组分成两两一组,最后一项如果是单数,直接输出就好,直到数组长度为1.

时间复杂度也降低了logN

问题

这种是代码code的思路,但是实践生产能写这样的代码?

promise.all 中可以写100个,1000个元素,一起发起请求,但是浏览器起能同时发起100个1000个请求吗?

于是面试官继续问了, 比如有1000个数据,那第一次就会发送500个请求,网络拥堵了,我想控制成只能同时发送10个请求怎么办?

再次优化

要控制成只能同时发送10个请求,就要对promise.all进行修改。

function add(a, b) {
  return Promise.resolve(a + b);
}
function chunk(arr){
  const res = []
  for(let i =0;i<arr.length;i++) {
    const index = Math.floor(i/2)
    res[index] ??= []
    res[index].push(arr[i])
  }
  return res
}
async function pMap(list, mapper, concurrency = Infinity) {
  const results = [];
  const batches = chunk(list, Math.min(concurrency, list.length));
  await Promise.all(
    batches.map(async (batch) => {
      const batchResults = await Promise.all(batch.map(mapper));
      results.push(...batchResults);
    })
  );
  return results;
}
async function sum(arr, concurrency) {
  if (arr.length === 1) return arr[0];
  const mapped = await pMap(
    chunk(arr, 2),
    ([x, y]) => (y === undefined ? x : add(x, y)),
    concurrency
  );
  return sum(mapped, concurrency);
}

pMap 函数中,先调用 chunk 函数将原数组按照指定大小切割成多个子数组(每个子数组大小不超过并发数),然后使用 Promise.all 将每个子数组中的数据并发地传递给 mapper 函数进行处理,最后将每个子数组的处理结果拼接成一个新数组返回。

sum 函数则使用了 pMap 函数,将原数组切割成两两一组的子数组,对每个子数组调用 add 函数求和,最终递归地将求和后的结果作为新数组再次传入 pMap 函数中继续处理。

这样实现可以提高代码的并发性能,加速数据处理的速度。

以下是一个使用例子,测试用例将一个长度为 10 的数组按照每个元素加 1 的方式进行异步处理,并发数为 3:

const arr = [1, 2, 3, 4, 5];
sum(arr, 2)
  .then(result => console.log(result))
  .catch(error => console.error(error));

总结

上述一个代码题考查了哪些部分

  • promise串行,并行
  • 二分
  • 并发控制

考察技术深度广度都有了,但是大部分人题目都看不懂,其实我也是,我连答案也没明白。 但是这些题目看不懂,给了答案也得理解,告诉你思路,下手实现却写不出代码,往往是最有区分度的地方。 人们精通并擅长为自己的行为找原因,但却非常不善于做我们已经找到原因的事。 要完全弄懂,还需花时间思考啊。

参考链接github.com/shfshanyue/…


相关文章
|
3月前
|
缓存 前端开发 中间件
[go 面试] 前端请求到后端API的中间件流程解析
[go 面试] 前端请求到后端API的中间件流程解析
|
11天前
|
缓存 前端开发 JavaScript
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
42 1
|
22天前
|
机器学习/深度学习 人工智能 自然语言处理
前端大模型入门(三):编码(Tokenizer)和嵌入(Embedding)解析 - llm的输入
本文介绍了大规模语言模型(LLM)中的两个核心概念:Tokenizer和Embedding。Tokenizer将文本转换为模型可处理的数字ID,而Embedding则将这些ID转化为能捕捉语义关系的稠密向量。文章通过具体示例和代码展示了两者的实现方法,帮助读者理解其基本原理和应用场景。
119 1
|
2月前
|
Web App开发 前端开发 Linux
「offer来了」浅谈前端面试中开发环境常考知识点
该文章归纳了前端开发环境中常见的面试知识点,特别是围绕Git的使用进行了详细介绍,包括Git的基本概念、常用命令以及在团队协作中的最佳实践,同时还涉及了Chrome调试工具和Linux命令行的基础操作。
「offer来了」浅谈前端面试中开发环境常考知识点
|
26天前
|
设计模式 前端开发 JavaScript
前端编程的异步解决方案有哪些?
本文首发于微信公众号“前端徐徐”,介绍了异步编程的背景和几种常见方案,包括回调、事件监听、发布订阅、Promise、Generator、async/await和响应式编程。每种方案都有详细的例子和优缺点分析,帮助开发者根据具体需求选择最合适的异步编程方式。
57 1
|
15天前
|
前端开发 JavaScript API
2025年前端框架是该选vue还是react?有了大模型-例如通义灵码辅助编码,就不用纠结了!vue用的多选react,react用的多选vue
本文比较了Vue和React两大前端框架,从状态管理、数据流、依赖注入、组件管理等方面进行了详细对比。当前版本和下载量数据显示React更为流行,但Vue在国内用户量增长迅速。Vue 3通过组合式API提供了更灵活的状态管理和组件逻辑复用,适合中小型项目;React则更适合大型项目和复杂交互逻辑。文章还给出了选型建议,强调了多框架学习的重要性,认为技术问题已不再是选型的关键,熟悉各框架的最佳实践更为重要。
|
3月前
|
存储 XML 移动开发
前端大厂面试真题
前端大厂面试真题
|
22天前
|
Web App开发 JavaScript 前端开发
前端Node.js面试题
前端Node.js面试题
|
3月前
|
JavaScript 前端开发 编译器
TypeScript:一场震撼前端开发的效率风暴!颠覆想象,带你领略前所未有的编码传奇!
【8月更文挑战第22天】TypeScript 凭借其强大的静态类型系统和丰富的工具支持,已成为前端开发的优选语言。它通过类型检查帮助开发者早期发现错误,显著提升了代码质量和维护性。例如,定义函数时明确参数类型,能在编译阶段捕获类型不匹配的问题。TypeScript 还提供自动补全功能,加快编码速度。与 Angular、React 和 Vue 等框架的无缝集成进一步提高了开发效率,使 TypeScript 成为现代前端开发中不可或缺的一部分。
38 1
|
3月前
|
存储 前端开发 JavaScript
44 个 React 前端面试问题
【8月更文挑战第18天】
49 2