从手写Promise到手写Ajax:深入JavaScript异步编程

489 阅读20分钟

引言

在当今快节奏的Web开发领域,异步编程已经成为构建高效、响应迅速的应用程序不可或缺的一部分。JavaScript作为一种主要的前端开发语言,提供了丰富的工具和特性来处理异步操作。然而,对于许多开发者来说,真正掌握这些特性的内部机制并非易事。

PromiseAjax 是JavaScript中处理异步任务的核心概念。Promise 提供了一种优雅的方式来管理异步操作的结果,而 Ajax 则允许网页在不重新加载的情况下与服务器进行数据交换,从而提供更加流畅的用户体验。理解这两个概念的工作原理及其相互之间的关系,不仅有助于编写更高效的代码,还能为解决复杂的异步问题打下坚实的基础。

在这篇文章中,我们将开启一段深入探索JavaScript异步编程的旅程。从零开始实现自己的 Promise,我们将一步步揭开这个强大工具背后的神秘面纱。接着,我们会利用所学的知识,手写一个 Ajax 请求函数,模拟浏览器发送HTTP请求并与服务器通信的过程。通过这种方式,我们不仅可以巩固对JavaScript异步编程的理解,还可以学习如何将理论应用于实际的编码实践中。

无论你是初学者,渴望了解JavaScript异步编程的基本原理;还是经验丰富的开发者,希望加深对底层机制的认识,这篇文章都将为你提供宝贵的见解。让我们一起踏上这段激动人心的学习之旅,探索从手写 Promise 到手写 Ajax 的每一步,共同挖掘JavaScript异步编程的无限潜力。

什么是Promise?

Promise 是JavaScript中的一种对象,用于处理异步操作的结果。它代表了一个可能还未完成但最终会产生一个值的操作——这个值要么是成功的结果(fulfilled),要么是失败的原因(rejected)。Promise 提供了一种更清晰、更具可读性的方法来管理异步代码,避免了回调地狱(callback hell)的问题,并且简化了错误处理。其他大型语言一般是同步的,JavaScript通过es6的promise类来解决了单线程的异步不可控的问题。promise 让异步变成了同步的,提供了完善解决异步任务的机制。

Promise的核心概念

  • 状态:每个 Promise 都有一个内部状态,状态一旦改变,就不会再变并且状态改变不可逆它可以处于以下三种状态之一:

    • Pending(等待中) :这是初始状态,表示异步操作尚未完成。
    • Fulfilled(已成功) :当异步操作成功完成时,Promise 的状态变为 fulfilled,并且会携带一个结果值。
    • Rejected(已失败) :如果异步操作遇到问题或抛出异常,Promise 的状态将变为 rejected,并且会包含一个错误原因。
  • 链式调用:通过 .then().catch() 方法,可以为 Promise 注册回调函数,这些回调会在 Promise 状态改变时被调用。.then() 可以接收两个参数,分别是处理 fulfilled 状态的回调和处理 rejected 状态的回调。而 .catch() 则专门用来捕获任何在 Promise 链中发生的错误。

  • 静态方法:除了构造函数外,Promise 还提供了一些有用的静态方法,如 Promise.resolve()Promise.reject() 来立即创建 resolved 或 rejected 的 PromisePromise.all()Promise.race() 分别用于并行执行多个 Promise 并等待所有完成或直到第一个完成。

使用Promise的好处

  • 更清晰的代码结构:相比于嵌套的回调函数,使用 Promise 可以让代码更加线性和直观。
  • 更好的错误处理Promise 允许你集中处理整个异步操作链中的错误,而不是在每个回调函数中单独处理。
  • 组合异步操作:借助 Promise.all() 和 Promise.race() 等方法,可以轻松地组合多个异步操作,从而实现复杂的业务逻辑。

在JavaScript中,Promise 构造函数接收一个执行器(executor)函数作为参数,这个函数会在 Promise 被创建时立即调用。执行器函数本身接收两个参数:resolvereject。这两个函数用于改变 Promise 的状态,并且是处理异步操作结果的关键。

1. resolve

  • 定义resolve 是一个函数,当异步操作成功完成时被调用。
  • 作用:调用 resolve 函数会将 Promise 的状态从 "pending"(等待中)变为 "fulfilled"(已成功)。同时,它可以传递一个值给 .then() 方法中指定的回调函数,这个值可以是任何有效的JavaScript数据类型,如数字、字符串、对象等。
  • 使用场景:当你希望告知外部代码异步操作已经成功完成,并且可能需要传递一些数据(例如API响应结果)给后续的操作时,你应该使用 resolve

示例

const myPromise = new Promise((resolve, reject) => {
    // 模拟一个成功的异步操作
    setTimeout(() => {
        const data = { message: 'Operation succeeded!' };
        resolve(data); // 异步操作成功后调用resolve
    }, 1000);
});

myPromise.then(data => console.log(data.message)); // 输出: Operation succeeded!

image.png

2. reject

  • 定义reject 同样是一个函数,但在异步操作失败或发生错误时被调用。
  • 作用:调用 reject 函数会将 Promise 的状态从 "pending" 变为 "rejected"(已失败),并且可以传递一个原因(通常是一个 Error 对象),这将触发 .catch() 方法中定义的错误处理逻辑。
  • 使用场景:如果你遇到异常情况或者异步操作未能按预期完成,应该使用 reject 来通知外部代码出现了问题,并提供有关错误的信息。

示例

const myPromise = new Promise((resolve, reject) => {
    // 模拟一个失败的异步操作
    setTimeout(() => {
        const errorMessage = 'Something went wrong.';
        reject(new Error(errorMessage)); // 异步操作失败后调用reject
    }, 1000);
});

myPromise.catch(error => console.error('Error:', error.message)); // 输出: Error: Something went wrong.

image.png

综合示例

下面的例子展示了如何结合使用 resolvereject 来根据条件决定 Promise 的最终状态:

const fetchData = (success) => new Promise((resolve, reject) => {
    // 模拟网络请求或其他异步任务
    setTimeout(() => {
        if (success) {
            const data = { id: 1, name: 'Example' };
            resolve(data); // 如果操作成功,则调用resolve
        } else {
            const error = new Error('Failed to fetch data.');
            reject(error); // 如果操作失败,则调用reject
        }
    }, 2000);
});

fetchData(true)
    .then(data => console.log('Data received:', data))
    .catch(error => console.error('Error occurred:', error.message));

fetchData(false)
    .then(data => console.log('This will not be called'))
    .catch(error => console.error('Error occurred:', error.message));

image.png

在这个例子中,fetchData 函数接受一个布尔参数来模拟不同的操作结果。如果 success 参数为 true,则调用 resolve 并传递模拟的数据;反之,如果 successfalse,则调用 reject 并传递一个错误对象。通过这种方式,我们可以灵活地控制 Promise 的行为,以适应各种实际应用中的需求。

手写Promise

在JavaScript中,Promise 是处理异步操作的核心工具之一。它提供了一种更加清晰和可控的方式来管理异步任务的结果。为了更好地理解 Promise 的工作原理,我们将从头开始手写一个简单的 Promise 实现。这不仅有助于加深对 Promise 内部机制的理解,还可以帮助我们构建更高效的异步代码。

1. 基础构造函数

首先,我们需要定义一个构造函数 MyPromise,它接收一个执行器(executor)函数作为参数。这个执行器函数会在 Promise 被创建时立即调用,并且会传递两个参数:resolvereject 函数,用于改变 Promise 的状态。

function MyPromise(executor) {
    let self = this;
    self.status = 'pending'; // 初始状态为等待中
    self.value = undefined;  // 成功时保存的值
    self.reason = undefined; // 失败时保存的原因
    self.onFulfilledCallbacks = []; // 存储成功的回调
    self.onRejectedCallbacks = [];  // 存储失败的回调

    function resolve(value) {
        if (self.status === 'pending') {
            self.status = 'fulfilled';
            self.value = value;
            self.onFulfilledCallbacks.forEach(cb => cb());
        }
    }

    function reject(reason) {
        if (self.status === 'pending') {
            self.status = 'rejected';
            self.reason = reason;
            self.onRejectedCallbacks.forEach(cb => cb());
        }
    }

    try {
        executor(resolve, reject);
    } catch (err) {
        reject(err);
    }
}

2. 添加 .then() 方法

接下来,我们为 MyPromise 添加 .then() 方法,允许注册当 Promise 状态变化时应该执行的回调函数。.then() 应该返回一个新的 MyPromise,以便可以链式调用多个 .then()

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    const self = this;
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };

    return new MyPromise((resolve, reject) => {
        function handleCallback(callback, isFulfilled) {
            try {
                let x = callback(self[isFulfilled ? 'value' : 'reason']);
                resolvePromise(self, x, resolve, reject);
            } catch (error) {
                reject(error);
            }
        }

        if (self.status === 'fulfilled') {
            setTimeout(() => handleCallback(onFulfilled, true));
        } else if (self.status === 'rejected') {
            setTimeout(() => handleCallback(onRejected, false));
        } else {
            // 如果是pending状态,则将回调函数存入队列
            self.onFulfilledCallbacks.push(() => handleCallback(onFulfilled, true));
            self.onRejectedCallbacks.push(() => handleCallback(onRejected, false));
        }
    });
};

3. 解决Promise A+ 规范中的x问题

根据 Promise A+ 规范.then() 返回的新 Promise 必须考虑几种特殊的情况,特别是当回调函数返回另一个 Promise 或者循环引用自身时。为此,我们实现一个辅助函数 resolvePromise 来处理这些情况。

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected for promise'));
    }

    let called;

    if ((x !== null && typeof x === 'object') || typeof x === 'function') {
        try {
            let then = x.then;
            if (typeof then === 'function') {
                then.call(x, y => {
                    if (called) return;
                    called = true;
                    resolvePromise(promise2, y, resolve, reject);
                }, err => {
                    if (called) return;
                    called = true;
                    reject(err);
                });
            } else {
                resolve(x);
            }
        } catch (e) {
            if (called) return;
            called = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

4. 添加静态方法

为了使我们的 MyPromise 更加完整,我们可以添加一些常用的静态方法,如 resolverejectallrace。这里以 resolvereject 为例:

MyPromise.resolve = function(value) {
    return new MyPromise((resolve) => resolve(value));
};

MyPromise.reject = function(reason) {
    return new MyPromise((_, reject) => reject(reason));
};

5. 测试手写的 MyPromise

现在我们有了一个基本的手写 Promise 实现,可以对其进行测试:

const myPromise = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        const success = true; // 模拟成功或失败
        if (success) {
            resolve('Operation succeeded!');
        } else {
            reject(new Error('An error occurred.'));
        }
    }, 1000);
});

myPromise
    .then(data => console.log(data))
    .catch(error => console.error('Error:', error.message));

// 测试静态方法
MyPromise.resolve('Static method test')
    .then(data => console.log(data));

MyPromise.reject(new Error('Static rejection'))
    .catch(error => console.error('Error:', error.message));

通过上述步骤,我们已经完成了一个基础但功能完整的 Promise 实现。当然,实际应用中的 Promise 可能会包含更多的特性和优化,但这个简易版本足以帮助我们掌握其核心概念和工作原理。


什么是Ajax?

Ajax(Asynchronous JavaScript and XML)是一种用于创建快速动态网页的技术,它允许网页在不重新加载整个页面的情况下与服务器进行通信并更新部分网页内容。这一技术使得Web应用程序能够提供更流畅、响应更快的用户体验,因为用户不需要等待整个页面刷新来获取或提交数据。

Ajax的核心概念

  • 异步请求:Ajax最显著的特点是它的“异步”性质,意味着当一个Ajax请求被发送到服务器时,用户可以继续使用网页上的其他功能,而无需等待服务器响应。这大大提高了用户的交互体验。
  • JavaScript和XMLHttpRequest对象:Ajax依赖于JavaScript编程语言以及浏览器内置的XMLHttpRequest对象(或者现代浏览器中的fetch API)。通过这些工具,前端代码可以直接向服务器发起HTTP请求,并处理返回的数据。
  • 局部更新:Ajax使开发者能够在接收到服务器响应后仅更新页面的一部分,而不是像传统方式那样重新加载整个页面。这不仅加快了页面的响应速度,还减少了网络流量。
  • 多种数据格式支持:尽管名称中包含“XML”,但Ajax并不局限于使用XML格式的数据。实际上,JSON(JavaScript Object Notation)已经成为Ajax应用中最常用的数据交换格式,因为它更轻量且易于解析。

什么是 XMLHttpRequest 对象?

XMLHttpRequest 是浏览器内置的一个 JavaScript 对象,它允许开发者从网页中发起 HTTP 请求并与服务器进行交互。通过 XMLHttpRequest,可以在不重新加载整个页面的情况下更新网页内容,从而提供更流畅的用户体验。尽管名字中有 "XML",但这个对象不仅可以处理 XML 数据,还能与多种数据格式(如 JSON、纯文本等)一起工作。

主要特点

  • 异步通信XMLHttpRequest 支持异步请求,这意味着在发送请求的同时,用户仍然可以继续与网页互动,无需等待服务器响应。
  • 同步通信(不推荐) :虽然也支持同步请求,但这会导致浏览器在等待服务器响应期间冻结,影响用户体验,因此通常不建议使用。
  • 跨域限制:出于安全考虑,默认情况下 XMLHttpRequest 受同源策略(Same-origin policy)约束,即只能向同一域名、协议和端口下的资源发起请求。不过,现代浏览器支持 CORS(跨域资源共享)机制来放宽这一限制。
  • 事件驱动XMLHttpRequest 提供了一系列事件,用于监听请求的不同阶段(例如开始、进度、完成),以及处理成功或失败的结果。

基本属性和方法

属性
  • readyState:表示请求的状态。取值范围为0到4:

    • 0 (UNSENT):已经创建了对象,但尚未调用 .open() 方法。
    • 1 (OPENED):.open() 已经被调用。
    • 2 (HEADERS_RECEIVED):.send() 已经被调用,并且头部信息已经被接收。
    • 3 (LOADING):正在下载响应体;对于某些类型的请求(如GET),此时可以访问部分响应数据。
    • 4 (DONE):操作已完成。
  • statusstatusText:HTTP 状态码及其描述文本,仅当 readyState 为 3 或 4 时可用。

  • responseText:作为字符串返回的响应主体,适用于非二进制数据。

  • responseXML:如果响应的内容类型是 text/xml 或兼容类型,则返回一个解析后的 XML 文档对象;否则返回 null

  • response:根据 responseType 的设置,以适当的形式返回响应主体。

  • responseType:定义如何解释服务器响应的数据类型,可能的值包括 ""(默认,表示文本)、"arraybuffer""blob""document""json""text"

方法
  • open(method, url[, async[, user[, password]]) :初始化一个新的请求。参数包括请求的方法(如 GET 或 POST)、目标 URL、是否异步(布尔值,默认为 true),以及可选的身份验证凭据。
  • send([data]) :发送请求。对于 GET 请求,通常不需要传递 data 参数;而对于 POST 请求,则可以在该参数中包含要发送的数据。
  • setRequestHeader(header, value) :设置 HTTP 请求头字段。
  • abort() :中断请求。
  • getAllResponseHeaders()getResponseHeader(name) :获取所有响应头或指定名称的响应头。
  • overrideMimeType(mime) :重写 MIME 类型,用于控制如何解析响应数据。

Ajax的工作流程

  1. 创建 XMLHttpRequest 对象:首先,在客户端使用JavaScript创建一个新的XMLHttpRequest实例。
  2. 配置请求:设置请求的方法(GET、POST等)、URL及是否异步执行。
  3. 发送请求:调用send()方法将请求发送到服务器。对于GET请求,通常不需要传递额外的数据;而对于POST请求,则可以在send()方法中附带要发送的数据。
  4. 接收响应:服务器处理完请求后会返回响应。客户端可以通过监听XMLHttpRequest对象的状态变化(通过onreadystatechange事件)来捕获这个响应。
  5. 处理响应:一旦收到完整的响应(即readyState为4),就可以检查HTTP状态码以确定请求是否成功,并根据需要解析返回的数据(如JSON字符串转换成JavaScript对象)。
  6. 更新DOM:最后,使用JavaScript操作DOM来更新页面上的元素,显示新获取的数据或通知用户操作的结果。

实际应用示例

下面是一个简单的Ajax请求示例,演示如何从GitHub API获取组织成员列表,并将其显示在网页上:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ajax Example</title>
</head>
<body>
    <ul id="members"></ul>
    <script>
        function getMembers(url) {
            const xhr = new XMLHttpRequest();
            xhr.open("GET", url, true);
            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4) { // 请求已完成
                    if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                        const members = JSON.parse(xhr.responseText);
                        displayMembers(members);
                    } else {
                        console.error('Error fetching data:', xhr.statusText);
                    }
                }
            };
            xhr.onerror = function () {
                console.error('Network error occurred.');
            };
            xhr.send();
        }

        function displayMembers(members) {
            const ul = document.getElementById('members');
            ul.innerHTML = ''; // 清空现有列表项
            members.forEach(member => {
                const li = document.createElement('li');
                li.textContent = member.login;
                ul.appendChild(li);
            });
        }

        // 调用函数获取GitHub组织成员
        getMembers('https://api.github.com/orgs/lemoncode/members');
    </script>
</body>
</html>

在这个例子中,我们定义了一个名为getMembers的函数,它接受一个URL作为参数,并使用XMLHttpRequest对象向该URL发送GET请求。一旦收到响应,我们就解析返回的JSON数据,并调用displayMembers函数来更新页面上的无序列表,显示组织成员的名字。

手写Ajax

让我们先看一个简单的示例

const getJSON = function (url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
        xhr.open('GET', url, true);
        xhr.onreadystatechange = function () {
            if (xhr.readyState !== 4) return;
            if (xhr.status === 200 || xhr.status === 304) {
                resolve(xhr.responseText);
            } else {
                reject(new Error(xhr.statusText));
            }
        };
        xhr.send();
    });
};

getJSON('https://api.github.com/orgs/lemoncode/members')
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.error('Error:', error.message);
    });

这段代码定义了一个名为 getJSON 的函数,它使用 XMLHttpRequest 对象来发起一个异步HTTP GET请求,并将结果解析为JSON格式。让我们逐步解释这段代码的工作原理及其执行流程。

image.png

1. 定义 getJSON 函数

const getJSON = function (url) {
    return new Promise((resolve, reject) => {
        // ...
    });
}
  • 返回一个新的 PromisegetJSON 函数接收一个URL作为参数,并返回一个新的 Promise。这意味着调用者可以通过 .then() 和 .catch() 方法来处理请求成功或失败的情况。

2. 创建 XMLHttpRequest 对象

const xhr = new XMLHttpRequest 
    ? new XMLHttpRequest()
    : new ActiveXObject("Microsoft.XMLHTTP");
  • 浏览器兼容性检查:这里使用了条件运算符(三元运算符)来确保在所有主流浏览器中都能正确创建 XMLHttpRequest 实例。对于不支持现代 XMLHttpRequest 的旧版IE浏览器,它会回退到使用 ActiveXObject("Microsoft.XMLHTTP")

3. 配置和发送请求

xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
    if (xhr.readyState !== 4) return;
    
    if (xhr.status === 200 || xhr.status === 304) {
        resolve(xhr.responseText);
    } else {
        reject(new Error(xhr.statusText));
    }
};
xhr.send();
  • 配置请求xhr.open('GET', url, true) 方法用于初始化一个异步的GET请求。第三个参数设置为 true 表示该请求是异步的。

  • 监听状态变化:通过 xhr.onreadystatechange 属性设置一个回调函数,每当 readyState 属性发生变化时都会触发这个函数。根据 MDN文档readyState 的值为 4 表示请求已完成并且响应已经准备好。

  • 处理响应

    • 如果状态码为 200 或 304,则调用 resolve() 并传递 xhr.responseText,即服务器返回的数据。
    • 状态码 200 意味着请求成功。
    • 状态码 304 表示资源没有被修改,因此可以从缓存中读取数据而不是再次从服务器获取。这有助于减少网络流量并加快页面加载速度。
    • 如果状态码既不是 200 也不是 304,则调用 reject() 并传递一个新的错误对象,其中包含来自服务器的状态文本信息。
  • 发送请求:最后,调用 xhr.send() 来实际发送请求。

4. 使用 getJSON 函数

getJSON('https://api.github.com/orgs/lemoncode/members')
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.error('Error:', error.message);
    });
  • 发起请求:调用 getJSON 函数并向其传递目标URL,以向GitHub API发出请求获取组织成员列表。
  • 处理结果:使用 .then() 方法注册一个成功的回调函数,当 Promise 被解决时(即请求成功),这个回调函数会被调用,并且可以访问到服务器返回的数据。
  • 错误处理:使用 .catch() 方法捕获任何可能发生的错误,并打印出错误信息。

执行流程概述

  1. 发起请求:当 getJSON 函数被调用时,它会立即创建并配置一个 XMLHttpRequest 对象,然后发送一个异步的GET请求到指定的URL。
  2. 等待响应:浏览器继续执行其他任务,不会阻塞主线程,直到收到服务器的响应。
  3. 处理响应:一旦请求完成(readyState 变为 4),浏览器会检查HTTP状态码。如果状态码为 200304,则将响应文本解析为JSON格式,并通过 resolve() 将其传递给 .then() 中的回调函数;否则,通过 reject() 触发错误处理逻辑。
  4. 更新DOM或其他操作:最终,.then() 中的回调函数会接收解析后的JSON数据,并可以根据需要进一步处理这些数据,例如更新页面上的DOM元素或进行其他业务逻辑操作。
  5. 错误处理:如果有任何问题(如网络错误或非预期的HTTP状态码),.catch() 中的回调函数将会被执行,以便通知用户或采取适当的措施。

下面我们开始手写Ajax

1. 创建 XMLHttpRequest 对象

首先,我们需要创建一个函数来初始化XMLHttpRequest对象。考虑到不同浏览器的支持情况,我们还需要兼容IE早期版本:

function createXHR() {
    if (window.XMLHttpRequest) {
        return new XMLHttpRequest(); // 现代浏览器
    } else if (window.ActiveXObject) {
        try {
            return new ActiveXObject("Msxml2.XMLHTTP"); // IE6+
        } catch (e) {
            try {
                return new ActiveXObject("Microsoft.XMLHTTP"); // IE5, IE5.5
            } catch (e) {}
        }
    }
    throw new Error("Your browser does not support XMLHttpRequest.");
}

2. 封装发送请求的方法

为了简化代码,我们可以封装一个通用的函数来发送GET或POST请求。这个函数将接受URL、请求方法、回调函数以及可选的数据参数。

function ajax(url, method = 'GET', callback, data = null) {
    const xhr = createXHR();
    
    xhr.open(method, url, true); // 设置为异步请求
    
    if (method.toUpperCase() === 'POST') {
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    }

    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) { // 请求完成
            if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
                callback(null, xhr.responseText); // 成功时调用回调函数
            } else {
                callback(new Error(`Request failed with status ${xhr.status}`), null);
            }
        }
    };

    xhr.onerror = function () {
        callback(new Error('Network error occurred.'), null);
    };

    xhr.send(data); // 发送请求,POST请求需要传递data
}

3. 使用手写的Ajax函数

现在我们可以使用刚刚定义的ajax函数来发起请求了。这里以获取GitHub用户信息为例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Handwritten Ajax Example</title>
</head>
<body>
    <ul id="user-data"></ul>
    <script>
        // 定义一个显示用户数据的辅助函数
        function displayUserData(error, response) {
            const ul = document.getElementById('user-data');
            ul.innerHTML = ''; // 清空现有列表项
            
            if (error) {
                ul.innerHTML = `<li>Error: ${error.message}</li>`;
                return;
            }

            try {
                const userData = JSON.parse(response);
                ul.innerHTML = `<li>${userData.name} (@${userData.login})</li>`;
            } catch (e) {
                ul.innerHTML = `<li>Error parsing response: ${e.message}</li>`;
            }
        }

        // 调用ajax函数获取GitHub用户信息
        const githubUserUrl = 'https://api.github.com/users/octocat';
        ajax(githubUserUrl, 'GET', displayUserData);
    </script>
</body>
</html>

在这个例子中,我们定义了一个名为displayUserData的回调函数,它负责处理来自服务器的响应,并更新页面上的无序列表来显示用户的名字和用户名。然后,我们调用了ajax函数来向GitHub API发起GET请求,并传入了我们的回调函数。

4. 处理POST请求

如果我们想要发送POST请求并提交数据给服务器,可以稍微调整一下ajax函数的调用方式。例如,假设我们要通过POST方法提交表单数据:

// 准备要发送的数据
const formData = 'name=JohnDoe&email=johndoe@example.com';

// 调用ajax函数发送POST请求
ajax('https://example.com/api/submit', 'POST', function(error, response) {
    if (error) {
        console.error('Error:', error.message);
    } else {
        console.log('Response:', response);
    }
}, formData);

请注意,在实际应用中,你应该根据服务器端点的具体要求正确地编码和格式化提交的数据。

总结

通过上述代码示例和解析,我们不仅实现了从零开始构建一个简单的 getJSON 函数,还深入了解了如何利用 XMLHttpRequestPromise 来处理异步HTTP请求。这种实践不仅增强了我们对JavaScript异步编程的理解,还为解决实际开发中的复杂问题提供了坚实的基础。

在现代Web开发中,掌握这些基础知识至关重要。尽管如今有诸如 fetch API 和各种第三方库(如 Axios)来简化HTTP请求的处理,但了解底层的工作原理能够帮助我们在遇到挑战时找到更有效的解决方案,并且更好地优化我们的应用性能。

此外,通过手写实现像 getJSON 这样的功能,我们可以更加灵活地定制行为,满足特定项目的需求。例如,在处理跨域请求、管理并发请求或集成缓存机制等方面,深入的知识可以为我们提供更多的可能性。

最后,希望这篇文章能为你提供有价值的见解,并激发你进一步探索JavaScript异步编程的兴趣。无论是继续深入研究 Promise 的高级用法,还是尝试使用更新的API和技术,保持好奇心和学习的热情将使你在Web开发的道路上不断进步。