AJAX vs Fetch API:Promise 与异步 JavaScript 怎么用?
今天在学习promise的时候,看到一些比较早的教程,其中提到有一个重要的概念就是AJAX。
尽管也许现代的做法更常见的是用Fetch API ,但是我也可以了解一下旧版实现里的做法,也能够帮助理解早期的异步 API,理解老项目的代码是如何做的。
关于异步JS(Promise)的前置知识,有关细节补充可阅读文档:异步 JavaScript 简介
我理解为promise的出现是异步编程中防止传统回调嵌套函数写法(回调地狱)。promise是现代 JavaScript 异步编程的基础。
常常见到的await async等其实是一种语法糖,使得写法简洁易读,并且有关try catch 错误异常的捕获和管理会比较方便(对比于原先采用catch统一管理错误的办法…)。这样的写法看起来是同步代码的长相,其实底层是异步编程。
早期异步Web API: XMLHttpRequest(AJAX)
AJAX全称为Asynchronous JavaScript and XML(异步JavaScript和XML),是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。
它通过在后台与服务器进行少量数据交换,使得网页可以实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
示例:
const log = document.querySelector(".event-log");
document.querySelector("#xhr").addEventListener("click", () => {
log.textContent = "";
const xhr = new XMLHttpRequest();
xhr.addEventListener("loadend", () => {
log.textContent = `${log.textContent}完成!状态码:${xhr.status}`;
});
xhr.open(
"GET",
"https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json",
);
xhr.send();
log.textContent = `${log.textContent}请求已发起\n`;
});
document.querySelector("#reload").addEventListener("click", () => {
log.textContent = "";
document.location.reload();
});
<button id="xhr">点击发起请求</button>
<button id="reload">重载</button>
<pre readonly class="event-log"></pre>
点击“点击发起请求”按钮来发送一个请求。我们将创建一个新的
XMLHttpRequest并监听它的loadend事件。loadend事件在请求完成时总会触发,无论成功还是失败。如果需要区分成功和失败,可以分别监听load(成功)和error(失败)事件。而我们的事件处理程序则会在控制台中输出一个“完成!”的消息和请求的状态代码。
AJAX的工作原理基于一系列现有的互联网标准,主要包括以下几个方面:
- XMLHttpRequest对象:这是AJAX的核心,它提供了在网页加载后从服务器请求数据的能力。
- JavaScript/DOM:用于动态显示和交互的信息。
- CSS:用于定义数据的样式。
- XML:作为数据传输的格式,尽管现在JSON格式更为常用。
XMLHttpRequest
XMLHttpRequest API 使 web 应用能够通过 JavaScript 向 web 服务器发起 HTTP 请求并接收响应。这使得网站能够仅更新页面中的部分内容(使用服务器返回的数据),而无需跳转至全新页面。这种做法有时也被称为 AJAX。
Fetch API 是取代 XMLHttpRequest API 的更灵活、更强大的方案。
Fetch API 使用 promise 替代事件机制处理异步响应,对 service worker 支持良好,并支持 HTTP 的高级特性,如跨源资源共享控制。
基于这些优势,现代 web 应用通常采用 Fetch API 替代 XMLHttpRequest。
XMLHttpRequest 用于在后台与服务器交换数据。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。
AJAX能允许网页在不影响用户操作的情况下,与服务器进行数据交换和更新。例如Google地图、新浪微博等,依托核心还是XMLHttpRequest。
实现AJAX
通常需要以下几个步骤:
-
创建XMLHttpRequest对象:这是所有AJAX请求的起点。
-
发送请求到服务器:使用*open()和send()*方法,可以指定请求的类型(如GET或POST),URL以及是否异步。
-
处理服务器响应:通过监听onreadystatechange事件,可以在请求的不同阶段执行不同的操作。当readyState属性变为4,且status属性表示请求成功时,可以处理响应数据。
-
更新网页内容:使用JavaScript操作DOM,可以根据服务器的响应更新网页的特定部分。
跨域问题和解决方法
在使用AJAX时,可能会遇到跨域问题,即浏览器出于安全考虑,限制了来自不同源的HTTP请求。解决跨域问题的方法包括:
CORS(Cross-Origin Resource Sharing):通过服务器设置适当的HTTP响应头,可以允许特定的外部域访问资源。
JSONP(JSON with Padding):通过动态创建*
AJAX的优势和注意事项
AJAX的主要优势在于提高了用户体验,通过异步更新可以减少等待时间,使得Web应用程序更加快速和响应。然而,也需要注意一些问题,例如:
浏览器兼容性:不同浏览器对AJAX的支持程度可能不同,需要进行充分的测试。
用户体验:需要合理设计用户界面,以便在数据加载过程中给予用户适当的反馈。
网络延迟:应考虑到网络延迟对用户体验的影响,并采取相应的优化措施。
总的来说,AJAX技术使得Web开发进入了一个新的阶段,它允许开发者创建出更加动态和交互性强的网页应用。
使用Fetch API与Promise
MDN的教程已经讲解的非常好了,我们一起来跟着学一学,现代使用Fetch API 的做法。
在基于 Promise 的 API 中,异步函数会启动操作并返回一个 Promise 对象。
首先,Promise 有三种状态:
- 待定(pending):初始状态,既没有被兑现,也没有被拒绝。这是调用
fetch()返回 Promise 时的状态,此时请求还在进行中。 - 已兑现(fulfilled):意味着操作成功完成。当 Promise 完成时,它的
then()处理函数被调用。 - 已拒绝(rejected):意味着操作失败。当一个 Promise 失败时,它的
catch()处理函数被调用。
注意,这里的“成功”或“失败”的含义取决于所使用的 API:例如,fetch() 认为服务器返回一个错误(如 404 Not Found)时请求成功,但如果网络错误阻止请求被发送,则认为请求失败。
有时我们用已敲定(settled)这个词来同时表示已兑现(fulfilled)和已拒绝(rejected)两种情况。
如果一个 Promise 已敲定,或者如果它被“锁定”以跟随另一个 Promise 的状态,那么它就是已解决(resolved)的。
(关于术语:Let’s talk about how to talk about promises)
然后,你可以将处理函数附加到 Promise 对象上,当操作完成时(成功或失败),这些处理函数将被执行。
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
console.log(fetchPromise);
fetchPromise.then((response) => {
console.log(`已收到响应:${response.status}`);
});
console.log("已发送请求……");
- 调用
fetch()API,并将返回值赋给fetchPromise变量。 - 紧接着,输出
fetchPromise变量,输出结果应该像这样:Promise { <state>: "pending" }。这告诉我们有一个Promise对象,它有一个state属性,值是"pending"。"pending"状态意味着操作仍在进行中。 - 将一个处理函数传递给 Promise 的
then()方法。当(如果)获取操作成功时,Promise 将调用我们的处理函数,传入一个包含服务器的响应的Response对象。 - 输出一条信息,说明我们已经发送了这个请求。
Promise { <state>: "pending" }
已发送请求……
已收到响应:200
与之前的 XMLHttpRequest 不同的是,事件处理程序并不是添加在 XMLHttpRequest 的对象中,我们这一次将处理程序传递到返回的promise对象的then方法里面。
Promise链
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
fetchPromise.then((response) => {
const jsonPromise = response.json();
jsonPromise.then((json) => {
console.log(json[0].name);
});
});
等等!还记得上一篇文章吗?我们好像说过,**在回调中调用另一个回调会出现多层嵌套的情况?我们是不是还说过,这种“回调地狱”使我们的代码难以理解?**这不是也一样吗,只不过变成了用
then()调用而已?当然如此。但 Promise 的优雅之处在于
then()本身也会返回一个 Promise,这个 Promise 将指示then()中调用的异步函数的完成状态。
官方教程划重点:Promise 的优雅之处在于 then() 本身也会返回一个 Promise,这个 Promise 将指示 then() 中调用的异步函数的完成状态。
所以以上代码等价于:
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
fetchPromise
.then((response) => response.json())
.then((data) => {
console.log(data[0].name);
});
我们需要在尝试读取请求之前检查服务器是否接受并处理了该请求。我们将通过检查响应中的状态码来做到这一点,如果状态码不是“OK”,就抛出一个错误:
const fetchPromise = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
fetchPromise
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP 请求错误:${response.status}`);
}
return response.json();
})
.then((json) => {
console.log(json[0].name);
});
错误捕获
const fetchPromise = fetch(
"bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
fetchPromise
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP 请求错误:${response.status}`);
}
return response.json();
})
.then((json) => {
console.log(json[0].name);
})
.catch((error) => {
console.error(`无法获取产品列表:${error}`);
});
catch处理函数的输出错误。
- 注意:
fetch()只有在网络层面失败时才会进入catch。服务器返回 404 或 500 状态码时,Promise 依然是 fulfilled 状态,需要通过response.ok手动判断。
合并使用多个promise
有时你需要所有的 Promise 都得到实现,但它们并不相互依赖。在这种情况下,将它们一起启动然后在它们全部被兑现后得到通知会更有效率。这里需要 Promise.all() 方法。它接收一个 Promise 数组,并返回一个单一的 Promise。
Promise.all()
由Promise.all()返回的 Promise:
- 当且仅当数组中所有的 Promise 都被兑现时,才会通知
then()处理函数并提供一个包含所有响应的数组,数组中响应的顺序与被传入all()的 Promise 的顺序相同。 - 会被拒绝——如果数组中有任何一个 Promise 被拒绝。此时,
catch()处理函数被调用,并提供被拒绝的 Promise 所抛出的错误。
const fetchPromise1 = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
const fetchPromise2 = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found",
);
const fetchPromise3 = fetch(
"https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json",
);
Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
.then((responses) => {
for (const response of responses) {
console.log(`${response.url}:${response.status}`);
}
})
.catch((error) => {
console.error(`获取失败:${error}`);
});
promise.all用于批量处理不是相互依赖的promise,这样提高了效率,但是弊端是只有全部成功才会成功,如果有一个失败(rejected)则所有all包含在内的promise都不能被兑现。此时错误会用catch抛出。
Promise.any()
有时,你可能需要一组 Promise 中的某一个 Promise 的兑现,而不关心是哪一个。在这种情况下,你需要 Promise.any()。
这就像 Promise.all(),不过在 Promise 数组中的任何一个被兑现时它就会被兑现,如果所有的 Promise 都被拒绝,它也会被拒绝。
在这种情况下,我们无法预测哪个获取请求会先被兑现。
const fetchPromise1 = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
const fetchPromise2 = fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found",
);
const fetchPromise3 = fetch(
"https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json",
);
Promise.any([fetchPromise1, fetchPromise2, fetchPromise3])
.then((response) => {
console.log(`${response.url}:${response.status}`);
})
.catch((error) => {
console.error(`获取失败:${error}`);
});
async 和 await
async function fetchProducts() {
try {
const response = await fetch(
"https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json",
);
if (!response.ok) {
throw new Error(`HTTP 请求错误:${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error(`无法获取产品列表:${error}`);
}
}
const promise = fetchProducts();
promise.then((data) => console.log(data[0].name));
这里我们调用 await fetch(),我们的调用者得到的并不是 Promise,而是一个完整的 Response 对象,就好像 fetch() 是一个同步函数一样。
我们甚至可以使用 try...catch 块来处理错误,就像我们在写同步代码时一样。
但请注意,这个写法只在异步函数中起作用。异步函数总是返回一个 Promise。也就意味着async 函数总是返回一个 Promise。即使你返回一个普通值,它也会被自动包装成 Promise。
小结与更多Promise
Promise 是现代 JavaScript 异步编程的基础。它避免了深度嵌套回调,使表达和理解异步操作序列变得更加容易,并且它们还支持一种类似于同步编程中 try...catch 语句的错误处理方式。
async 和 await 关键字使得从一系列连续的异步函数调用中建立一个操作变得更加容易,避免了创建显式 Promise 链,并允许你像编写同步代码那样编写异步代码。
Promise 在所有现代浏览器的最新版本中都可以使用;唯一会出现支持问题的地方是 Opera Mini 和 IE11 及更早的版本。
在这篇文章中,我们没有涉及到所有的 Promise 功能,只是介绍了最有趣和最有用的那一部分。随着你开始学习更多关于 Promise 的知识,你会遇到更多有趣的特性。
许多现代 Web API 是基于 Promise 的,包括 WebRTC、Web Audio API、媒体捕捉与媒体流等等。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)