前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • HTTP

    • 基础篇

      • HTTP的前世今生
      • HTTP是什么
      • HTTP世界全览
      • HTTP分层
      • 键入网址到回车发生什么
      • HTTP报文是什么样子的
      • 理解请求方法
      • URI
      • 响应状态码
      • HTTP有哪些特点
      • HTTP优缺点
      • HTTP的实体数据
      • HTTP传输大文件
      • HTTP的连接管理
      • HTTP的重定向
      • HTTP的Cookie机制
      • HTTP的缓存控制
      • HTTP的代理服务
      • HTTP的缓存代理
    • 高级篇

    • 扩展篇

  • 浏览器相关

  • 计算机基础

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

使用Promise告别回调函数|浏览器篇

# 使用Promise告别回调函数

30 秒速记

  • Promise主要改善异步代码的组织方式,不会把异步任务变成同步任务,也不会消除任务本身的耗时。
  • 它把异步操作抽象为一个可传递的结果对象,并用成功与失败两种路径描述最终状态。
  • 依赖关系可以通过链式调用展开,业务步骤不必持续嵌套在前一步的回调内部。
  • 链路中的失败可以沿调用链传递并集中处理,从而减少每一步重复声明错误分支。
  • Promise仍依赖事件循环执行后续处理;它解决的是可读性与控制流问题,不是线程调度或性能问题。

Promise解决的是异步代码难组织的问题,让连续操作和错误处理不必层层嵌套。 它把异步结果包装成可传递的对象,通过链式调用表达前后依赖,失败也能沿调用链交给统一位置处理。比如多个异步步骤存在先后关系时,代码会比连续回调更容易阅读和维护。需要注意,Promise仍由事件循环调度,不会让任务同步执行,也不会缩短任务本身的耗时。

在上一篇文章中我们聊到了微任务是如何工作的,并介绍了 MutationObserver 是如何利用微任务来权衡性能和效率的。今天我们就接着来聊聊微任务的另外一个应用Promise,DOM/BOM API 中新加入的 API 大多数都是建立在 Promise 上的,而且新的前端框架也使用了大量的 Promise。可以这么说,Promise 已经成为现代前端的“水”和“电”,很是关键,所以深入学习 Promise 势在必行。

不过,Promise 的知识点有那么多,而我们只有一篇文章来介绍,那应该怎么讲解呢?具体讲解思路是怎样的呢?

如果你想要学习一门新技术,最好的方式是先了解这门技术是如何诞生的,以及它所解决的问题是什么。了解了这些后,你才能抓住这门技术的本质。所以本文我们就来重点聊聊 JavaScript 引入 Promise 的动机,以及解决问题的几个核心关键点。

要谈动机,我们一般都是先从问题切入,那么 Promise 到底解决了什么问题呢?在正式开始介绍之前,我想有必要明确下,Promise 解决的是异步编码风格的问题,而不是一些其他的问题,所以接下来我们聊的话题都是围绕编码风格展开的

面试官追问

追问 1接口负责人说把商品请求从回调函数改成 Promise,首屏网络耗时就会从 800ms 降下来;你会接受这个优化结论吗?
参考回答

不会,Promise 解决的是异步结果的编码和组织方式,不会直接缩短网络或服务端处理时间。改造后应以可读性、组合关系和错误路径是否更清晰来验收,不能把请求耗时下降归因于语法形式。

追问 2结算页有请求、库存校验和结果渲染三段异步流程,回调层层嵌套且失败分支散落;你会要求怎样评估 Promise 改造?
参考回答

这类存在依赖链和多处分支的流程适合评估 Promise,重点是把结果传递与后续步骤组织成清晰链路。改造仍需保留每一步的成功、失败和业务状态处理;Promise 只改善编码结构,不会自动修复流程逻辑。

追问 3一个旧页面只有一次请求和一次回调,团队为统一技术栈要求全部包装成 Promise;你会无条件支持吗?
参考回答

不会无条件支持,单层回调若已足够清晰,包装后的结构收益可能有限。只有后续需要组合异步步骤、传递依赖结果或集中管理错误路径时,Promise 的编码优势才更明显;统一形式也会带来改造和回归成本。

追问 4线上把回调改成 Promise 后仍出现输出顺序与开发预期不一致,有人据此判断 Promise 没有生效;你会先查什么?
参考回答

应先还原同步代码、异步结果产生点以及后续处理的实际调度顺序,而不是仅按源码行号推断。Promise 是微任务机制的重要应用,后续处理仍受事件循环时机约束;改写编码风格不等于同步执行。

追问 5架构评审中一方主张 Promise 能解决所有异步故障,另一方坚持继续使用多层回调;你会怎样划定方案边界?
参考回答

两种说法都过度了,Promise 的核心价值是改善异步编码风格,使依赖关系和结果交付更易组织。它不会让任务更快,也不会自动消除网络失败或业务错误;简单流程可保留清晰回调,复杂链路则更值得结构化改造。

# 异步编程的问题:代码逻辑不连续

30 秒速记

  • 页面主线程发起耗时操作后,由主线程之外的执行单元处理,主线程可以继续消费其他任务。
  • 异步操作完成后,相关任务进入消息队列,等待事件循环取出,再执行注册的回调。
  • 单次XMLHttpRequest需要分别监听成功、错误和超时等结果,配置代码与业务处理容易交错。
  • 回调会把发起操作与处理结果拆到不同位置;分支越多,控制流越难按业务顺序阅读。
  • 异步回调避免耗时操作长期占用主线程,但代价是代码连续性下降,需要进一步封装控制流。

异步编程的问题在于,任务的发起和结果处理被回调拆开,代码很难按业务顺序连续阅读。 耗时操作会交给主线程之外的执行单元,完成后再进入消息队列,由事件循环取出并触发回调。比如一次 XMLHttpRequest 既要处理成功,也要监听错误和超时,配置与业务逻辑很容易交错。这样能避免耗时任务长期占用主线程,但分支和回调越多,控制流越难维护,通常需要进一步封装。

首先我们来回顾下 JavaScript 的异步编程模型,你应该已经非常熟悉页面的事件循环系统了,也知道页面中任务都是执行在主线程之上的,相对于页面来说,主线程就是它整个的世界,所以在执行一项耗时的任务时,比如下载网络文件任务、获取摄像头等设备信息任务,这些任务都会放到页面主线程之外的进程或者线程中去执行,这样就避免了耗时任务“霸占”页面主线程的情况。你可以结合下图来看看这个处理过程:

上图展示的是一个标准的异步编程模型,页面主线程发起了一个耗时的任务,并将任务交给另外一个进程去处理,这时页面主线程会继续执行消息队列中的任务。等该进程处理完这个任务后,会将该任务添加到渲染进程的消息队列中,并排队等待循环系统的处理。排队结束之后,循环系统会取出消息队列中的任务进行处理,并触发相关的回调操作。

这就是页面编程的一大特点:异步回调。

Web 页面的单线程架构决定了异步回调,而异步回调影响到了我们的编码方式,到底是如何影响的呢?

假设有一个下载的需求,使用 XMLHttpRequest 来实现,具体的实现方式你可以参考下面这段代码:

// 执行状态
function onResolve(response){console.log(response) }
function onReject(error){console.log(error) }
 
let xhr = new XMLHttpRequest()
xhr.ontimeout = function(e) { onReject(e)}
xhr.onerror = function(e) { onReject(e) }
xhr.onreadystatechange = function () { onResolve(xhr.response) }
 
// 设置请求类型,请求 URL,是否同步信息
let URL = 'https://time.geekbang.com'
xhr.open('Get', URL, true);
 
// 设置参数
xhr.timeout = 3000 // 设置 xhr 请求的超时时间
xhr.responseType = "text" // 设置响应返回的数据格式
xhr.setRequestHeader("X_TEST","time.geekbang")
 
// 发出请求
xhr.send();
@前端进阶之旅: 代码已经复制到剪贴板

我们执行上面这段代码,可以正常输出结果的。但是,这短短的一段代码里面竟然出现了五次回调,这么多的回调会导致代码的逻辑不连贯、不线性,非常不符合人的直觉,这就是异步回调影响到我们的编码方式。

那有什么方法可以解决这个问题吗?当然有,我们可以封装这堆凌乱的代码,降低处理异步回调的次数。

面试官追问

追问 1相册页正在上传一个大文件,进度区还没显示完成,但按钮和滚动仍能响应;产品经理据此认定上传已经结束,你会怎么纠正这个判断?
参考回答

页面仍可交互只说明网络任务没有持续占用主线程,不能证明上传已经完成。耗时任务会在主线程之外处理,完成后再把对应任务加入消息队列;若主线程仍在执行其他任务,完成回调和提示都会继续等待。

追问 2支付页的 XMLHttpRequest 同时注册了 ontimeout、onerror 和 onreadystatechange,评审者想只留状态变化回调来减少代码,你会接受吗?
参考回答

不能仅为减少回调就删除超时和网络错误路径,它们表达的结束原因并不相同。封装层可以对业务统一暴露成功与失败,但底层仍要识别各类结果并避免失败路径误触发成功处理,否则诊断信息会丢失。

追问 3下载页需要先更新“开始下载”,再等待文件返回并渲染结果,业务方要求代码视觉上严格从上到下执行,于是提议把 xhr.open 的异步参数改为同步,你怎么取舍?
参考回答

不应为了代码外观线性而改成同步请求,因为耗时操作会占住页面主线程,破坏异步模型保障交互响应的目标。更合适的做法是保留异步执行并封装回调细节;代价是控制流仍需通过回调或更高层抽象表达。

追问 4上传完成事件已经由网络侧返回,但线上提示偶发延迟数秒,接口耗时记录却正常;你会先沿哪条执行链排查?
参考回答

应先检查主线程当时是否被长任务占用,以及完成任务进入消息队列后是否迟迟未被事件循环取出。网络任务结束不等于回调立即执行;若主线程繁忙,提示延后属于调度等待,继续优化接口本身未必有效。

追问 5一个页面只有一次请求,代码却有成功、超时、网络错误和状态变化等多处回调;架构师认为“回调数量多就是异步模型设计错误”,你会怎么回应?
参考回答

多条回调首先反映异步任务存在多种完成路径,并不说明异步模型本身错误。真正影响维护的是请求细节散落、业务逻辑被回调切断;可以把底层事件集中封装,但不能因此抹平必要的状态与失败语义。

# 封装异步代码,让处理流程变得线性

30 秒速记

  • 把地址、方法、响应类型等输入收敛为request对象,可以将请求参数与执行细节分开。
  • XFetch负责创建和配置XMLHttpRequest,并把底层事件转换成resolve与reject两个出口。
  • 调用方只需提交请求信息并处理成功或失败,业务代码不再重复请求初始化和事件绑定。
  • 这种封装降低了单次异步操作的噪声,但回调参数依然存在,连续依赖任务仍可能形成嵌套。
  • 示例仅表达封装思路,不能直接视为生产实现:其中xhr.status = 200是赋值而非比较,URL也未从request.url读取。

可以把请求信息收敛到 request 对象,再由 XFetch 统一封装 XMLHttpRequest 的创建、配置和事件处理。 这样输入参数与执行细节分开,调用方只关注请求内容,以及 resolve 和 reject 对应的成功、失败结果,业务代码会更干净。不过它仍然依赖回调,多个前后依赖的异步任务依旧可能出现嵌套。示例只用于说明封装思路,xhr.status = 200 是赋值而非比较,xhr.open 中的地址也应从 request.url 获取。

← 宏任务和微任务:不是所有的任务都是一个待遇async await使用同步方式写异步代码 →

fe
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
  • HTTP

    • 基础篇

      • HTTP的前世今生
      • HTTP是什么
      • HTTP世界全览
      • HTTP分层
      • 键入网址到回车发生什么
      • HTTP报文是什么样子的
      • 理解请求方法
      • URI
      • 响应状态码
      • HTTP有哪些特点
      • HTTP优缺点
      • HTTP的实体数据
      • HTTP传输大文件
      • HTTP的连接管理
      • HTTP的重定向
      • HTTP的Cookie机制
      • HTTP的缓存控制
      • HTTP的代理服务
      • HTTP的缓存代理
    • 高级篇

    • 扩展篇

  • 浏览器相关

  • 计算机基础