【面试题】聊聊 js 异步解决方案

简介: 【面试题】聊聊 js 异步解决方案

大厂面试题分享 面试题库

前端面试题库 (面试必备)   推荐:★★★★★

地址:前端面试题库

前言

继续聊聊 js 的异步解决方案有哪些,以及各个方案的优缺点是什么......

回调函数(callback)

回调函数 简单理解就是一个函数被作为参数传递给另一个函数。回调是早期最常用的一种异步解决方案。

回调并不一定就是异步,并没有直接关系。

举个简单的例子:

function f1(cb) {
  setTimeout(() => {
    cb && cb();
  }, 2000);
}
f1(() => {
  console.log("1");
});
复制代码

如上,我们使用 setTimeout 在函数 f1 中模拟了一个耗时 2s 的任务,耗时任务结束后会抛出一个回调,那么我们在调用时就可以做到在函数 f1 的耗时任务结束后执行回调函数了。

采用这种方式,我们把同步操作变成了异步操作,f1 不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。

回调优缺点

优点:简单、容易理解

缺点:代码不优雅,可读性差,不易维护,高度耦合,层层嵌套造成回调地狱

事件监听(发布订阅模式)

发布订阅模式 定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将会得到通知。

其实我们都用过发布订阅模式,比如我们在 DOM 节点上绑定一个事件函数:

document.body.addEventListener('click', function () {
  console.log('点击');
})
复制代码

但这只是对发布订阅模式最简单的使用,在很多场景下我们经常会使用一些自定义事件来满足我们的需求。

发布订阅模式有很多种实现方式,下面我们用 class 来简单实现下:

class Emitter {
  constructor() {
    // _listener数组,key为自定义事件名,value为执行回调数组-因为可能有多个
    this._listener = []
  }
  // 订阅 监听事件
  on(type, fn) {
    // 判断_listener数组中是否存在该事件命
    // 存在将回调push到事件名对应的value数组中,不存在直接新增
    this._listener[type] 
      ? this._listener[type].push(fn) 
      : (this._listener[type] = [fn])
  }
  // 发布 触发事件
  trigger(type, ...rest) {
    // 判断该触发事件是否存在
    if (!this._listener[type]) return
    // 遍历执行该事件回调数组并传递参数
    this._listener[type].forEach(callback => callback(...rest))
  }
}
复制代码

如上所示,我们创建了一个 Emitter 类,并且添加了两个原型方法 ontrigger,使用如下:

// 创建一个emitter实例
const emitter = new Emitter()
emitter.on("done", function(arg1, arg2) {
  console.log(arg1, arg2)
})
emitter.on("done", function(arg1, arg2) {
  console.log(arg2, arg1)
})
function fn1() {
  console.log('我是主程序')
  setTimeout(() => {
    emitter.trigger("done", "异步参数一", "异步参数二")
  }, 1000)
}
fn1()
复制代码

我们先创建一个 emitter 实例,接着注册事件,再触发事件,也解决了异步问题。

事件监听优缺点

优点:比较符合模块化思想,我们自写监听器时可以做很多优化从而更好地监控程序运行。

缺点:整个程序变成了事件驱动,流程上或多或少都会有点影响,每次使用还得注册事件监听再进行触发挺麻烦的,代码也不太优雅。

Promise

ES2015(ES6)标准化和引入了 Promise 对象,它是异步编程的一种解决方案。

简单来说就是用同步的方式写异步的代码,可用来解决回调地狱问题。

Promise 对象状态一旦改变,就不会再变,只有两种变化可能:

  1. Pending 变为 Resolved
  2. Pending 变为 Rejected

我们用 setTimeout 模拟异步操作:

function analogAsync(n) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(n + 500), n);
  });
}
function fn1(n) {
  console.log(`step1 with ${n}`);
  return analogAsync(n);
}
function fn2(n) {
  console.log(`step2 with ${n}`);
  return analogAsync(n);
}
function fn3(n) {
  console.log(`step3 with ${n}`);
  return analogAsync(n);
}
复制代码

Promise 来实现:

function fn() {
  let time1 = 0;
  fn1(time1)
    .then((time2) => fn2(time2))
    .then((time3) => fn3(time3))
    .then((res) => {
      console.log(`result is ${res}`);
    });
}
fn();
复制代码

Promise 优缺点

优点:Promise 用同步的方式写异步的代码,避免了层层嵌套的回调函数,可读性更好。链式操作,可以在 then 中继续写 Promise 对象并返回,然后继续调用 then 来进行回调操作。

缺点:Promise 对象一旦新建就会立即执行,无法中途取消。若不设置回调函数,Promise 内部会抛出错误,不会流到外部。

Generator

Generator 其实是一个函数,只不过是一个特殊的函数。普通函数,你执行了这个函数,函数内部不会停,直到这个函数结束。Generator 这个函数特殊之处就是中间可以停。

示例:

function *generatorFn() {
  console.log("a");
  yield '1';
  console.log("b");
  yield '2'; 
  console.log("c");
  return '3';
}
let it = generatorFn();
it.next();
it.next();
it.next();
it.next();
复制代码

上面这个示例就是一个 Generator 函数,它有如下特点:

  • 不同于普通函数,Generator 函数在 function 后面,函数名之前有个 *
  • 函数内部有 yield 字段
  • 调用后其函数返回值使用了 next 方法

Generator 优缺点

优点:优雅的流程控制方式,可以让函数可中断执行

缺点:Generator 函数的执行必须靠执行器,只针对异步处理来说,还是不太方便

async/await

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。async 是异步的意思,而 awaitasync wait 的简写,即异步等待,async/await 的出现,被很多人认为是 js 异步操作的最终且最优雅的解决方案。

async 在做什么

async 函数返回的是一个 Promise 对象,如果在 async 函数中直接 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象返回。

await 在等待什么

await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定,啥都行)。

  • 如果 await 后面不是 Promise 对象,直接执行
  • 如果 await 后面是 Promise 对象会阻塞后面的代码,Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果
  • await 只能在 async 函数中使用

上述用 setTimeout 模拟异步操作,我们用 async/await 来实现:

async function fn() {
  let time1 = 0;
  let time2 = await fn1(time1);
  let time3 = await fn2(time2);
  let res = await fn3(time3);
  console.log(`result is ${res}`);
}
fn();
复制代码

输出结果和上面用 Promise 实现是一样的,但这个 async/await 代码结构看起来清晰得多,几乎跟同步写法一样,十分优雅。

async/await 优缺点

优点:内置执行器,更好的语义,更广的适用性

缺点:滥用 await 可能会导致性能问题,因为 await 会阻塞代码

总结

以上就是笔者对于 js 异步解决方案的概述,以及对它们的优缺点的分析,如有不足欢迎大家指出,如果大家觉得还不错的话,不要忘了点赞呦~

 

大厂面试题分享 面试题库

前端面试题库 (面试必备)   推荐:★★★★★

地址:前端面试题库

相关文章
|
12月前
|
JavaScript 前端开发
JS浮点数精度问题及高精度小数运算:BigNumber解决方案
JS浮点数精度问题及高精度小数运算:BigNumber解决方案
1159 0
|
6月前
|
存储 NoSQL Redis
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 + 无锁架构 + EDA架构 + 异步日志 + 集群架构
阿里面试:Redis 为啥那么快?怎么实现的100W并发?说出了6大架构,面试官跪地: 纯内存 + 尖端结构 +  无锁架构 +  EDA架构  + 异步日志 + 集群架构
|
8月前
|
Web App开发 JavaScript 前端开发
如何在JavaScript中确定异步操作之间的依赖关系?
如何在JavaScript中确定异步操作之间的依赖关系?
181 58
|
8月前
|
前端开发 JavaScript
有没有方法可以保证在JavaScript中多个异步操作的执行顺序?
有没有方法可以保证在JavaScript中多个异步操作的执行顺序?
300 58
|
12月前
|
小程序 JavaScript 前端开发
uni-app开发微信小程序:四大解决方案,轻松应对主包与vendor.js过大打包难题
uni-app开发微信小程序:四大解决方案,轻松应对主包与vendor.js过大打包难题
2043 1
|
8月前
|
自然语言处理 JavaScript 前端开发
当面试官再问我JS闭包时,我能答出来的都在这里了。
闭包(Closure)是前端面试中的高频考点,广泛应用于函数式编程中。它不仅指函数内部定义的函数,还涉及内存管理、作用域链和垃圾回收机制。闭包可以让函数访问其外部作用域的变量,但也可能引发内存泄漏等问题。通过合理使用闭包,可以实现模块化、高阶函数和回调函数等应用场景。然而,滥用闭包可能导致代码复杂度增加、调试困难以及潜在的性能问题。为了避免这些问题,开发时应谨慎处理闭包,避免不必要的嵌套,并及时清理不再使用的变量和监听器。
307 16
当面试官再问我JS闭包时,我能答出来的都在这里了。
|
7月前
|
JavaScript 前端开发 API
JavaScript中通过array.map()实现数据转换、创建派生数组、异步数据流处理、复杂API请求、DOM操作、搜索和过滤等,array.map()的使用详解(附实际应用代码)
array.map()可以用来数据转换、创建派生数组、应用函数、链式调用、异步数据流处理、复杂API请求梳理、提供DOM操作、用来搜索和过滤等,比for好用太多了,主要是写法简单,并且非常直观,并且能提升代码的可读性,也就提升了Long Term代码的可维护性。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
|
8月前
|
缓存 安全 Java
面试中的难题:线程异步执行后如何共享数据?
本文通过一个面试故事,详细讲解了Java中线程内部开启异步操作后如何安全地共享数据。介绍了异步操作的基本概念及常见实现方式(如CompletableFuture、ExecutorService),并重点探讨了volatile关键字、CountDownLatch和CompletableFuture等工具在线程间数据共享中的应用,帮助读者理解线程安全和内存可见性问题。通过这些方法,可以有效解决多线程环境下的数据共享挑战,提升编程效率和代码健壮性。
244 6
|
7月前
|
消息中间件 JavaScript 前端开发
最细最有条理解析:事件循环(消息循环)是什么?为什么JS需要异步
度一教育的袁进老师谈到他的理解:单线程是异步产生的原因,事件循环是异步的实现方式。 本质是因为渲染进程因为计算机图形学的限制,只能是单线程。所以需要“异步”这个技术思想来解决页面阻塞的问题,而“事件循环”是实现“异步”这个技术思想的最主要的技术手段。 但事件循环并不是全部的技术手段,比如Promise,虽然受事件循环管理,但是如果没有事件循环,单一Promise依然能实现异步不是吗? 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您
|
11月前
|
JSON 前端开发 JavaScript
在 JavaScript 中,如何使用 Promise 处理异步操作?
通过以上方式,可以使用Promise来有效地处理各种异步操作,使异步代码更加清晰、易读和易于维护,避免了回调地狱的问题,提高了代码的质量和可维护性。