MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript中的异步请求与Fetch API

2023-11-232.6k 阅读

JavaScript 中的异步编程基础

为什么需要异步编程

在 JavaScript 的运行环境中,尤其是在浏览器端,很多操作是耗时的,例如网络请求、读取文件等。如果这些操作是同步执行的,那么在操作进行期间,主线程会被阻塞,导致页面失去响应,用户体验极差。而异步编程允许 JavaScript 在执行这些耗时操作时,不会阻塞主线程,使得主线程可以继续处理其他任务,比如响应用户的交互等。

回调函数(Callback)

回调函数是 JavaScript 中实现异步操作最基础的方式。当一个异步操作完成时,会调用预先传入的回调函数来处理结果。

例如,在 Node.js 中读取文件是一个异步操作,fs.readFile 函数接受一个回调函数作为参数:

const fs = require('fs');
fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(data);
});

在这个例子中,fs.readFile 开始读取文件,并不会阻塞主线程。当文件读取完成后,会调用回调函数,将可能出现的错误 err 和读取到的数据 data 作为参数传入回调函数进行处理。

回调地狱(Callback Hell)

当有多个异步操作需要顺序执行时,回调函数的嵌套会变得非常复杂,形成所谓的“回调地狱”。

例如,假设我们需要依次读取三个文件:

const fs = require('fs');
fs.readFile('file1.txt', 'utf8', (err1, data1) => {
    if (err1) {
        console.error(err1);
        return;
    }
    fs.readFile('file2.txt', 'utf8', (err2, data2) => {
        if (err2) {
            console.error(err2);
            return;
        }
        fs.readFile('file3.txt', 'utf8', (err3, data3) => {
            if (err3) {
                console.error(err3);
                return;
            }
            console.log(data1, data2, data3);
        });
    });
});

这样的代码嵌套层次过多,可读性和维护性都很差。

Promise

为了解决回调地狱的问题,ES6 引入了 Promise。Promise 表示一个异步操作的最终完成(或失败)及其结果值。

一个 Promise 有三种状态:

  1. Pending(进行中):初始状态,既没有被兑现,也没有被拒绝。
  2. Fulfilled(已兑现):意味着操作成功完成,Promise 有一个值。
  3. Rejected(已拒绝):意味着操作失败,Promise 有一个原因(通常是错误对象)。

创建一个 Promise 示例:

const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const success = true;
        if (success) {
            resolve('操作成功');
        } else {
            reject(new Error('操作失败'));
        }
    }, 1000);
});

promise.then((value) => {
    console.log(value);
}).catch((error) => {
    console.error(error);
});

在这个例子中,new Promise 接受一个执行器函数,该函数有两个参数 resolvereject。当异步操作成功时,调用 resolve 并传入结果值;当异步操作失败时,调用 reject 并传入错误对象。then 方法用于处理 Promise 被兑现的情况,catch 方法用于处理 Promise 被拒绝的情况。

Promise 链式调用

Promise 的一个强大之处在于可以进行链式调用,解决了回调地狱的问题。例如,上面依次读取三个文件的操作可以用 Promise 改写为:

const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);

readFile('file1.txt', 'utf8')
   .then((data1) => {
        console.log(data1);
        return readFile('file2.txt', 'utf8');
    })
   .then((data2) => {
        console.log(data2);
        return readFile('file3.txt', 'utf8');
    })
   .then((data3) => {
        console.log(data3);
    })
   .catch((error) => {
        console.error(error);
    });

这里使用了 util.promisify 方法将 fs.readFile 这个基于回调的函数转换为返回 Promise 的函数。通过链式调用 then 方法,可以清晰地看到异步操作的顺序执行,代码的可读性大大提高。

async/await

async/await 是 ES2017 引入的异步函数语法,它是基于 Promise 构建的更简洁的异步编程方式。async 函数总是返回一个 Promise。如果 async 函数的返回值不是 Promise,JavaScript 会自动将其包装成一个已兑现状态的 Promise。

await 只能在 async 函数内部使用,它用于暂停 async 函数的执行,等待一个 Promise 被兑现或被拒绝。当 Promise 被兑现时,await 表达式的值就是 Promise 的 resolved 值;当 Promise 被拒绝时,await 会抛出错误。

例如,用 async/await 改写上面读取三个文件的代码:

const fs = require('fs');
const util = require('util');

const readFile = util.promisify(fs.readFile);

async function readFiles() {
    try {
        const data1 = await readFile('file1.txt', 'utf8');
        console.log(data1);
        const data2 = await readFile('file2.txt', 'utf8');
        console.log(data2);
        const data3 = await readFile('file3.txt', 'utf8');
        console.log(data3);
    } catch (error) {
        console.error(error);
    }
}

readFiles();

在这个例子中,readFiles 是一个 async 函数,通过 await 依次等待每个文件读取操作完成,代码看起来更像是同步代码,极大地提高了代码的可读性和可维护性。

Fetch API 简介

什么是 Fetch API

Fetch API 是 JavaScript 中用于发起网络请求的现代接口,它提供了一个更强大、更灵活且基于 Promise 的方式来处理异步网络请求,相比于传统的 XMLHttpRequest 更加简洁易用。Fetch API 不仅可以用于浏览器端,在 Node.js 中也可以通过安装 node - fetch 库来使用。

Fetch API 的基本用法

使用 fetch 函数发起一个简单的 GET 请求:

fetch('https://example.com/api/data')
   .then((response) => {
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

在这个例子中,fetch 函数接受一个 URL 作为参数,返回一个 Promise。当请求成功时,then 方法会接收到一个 Response 对象。Response 对象包含了服务器返回的所有信息,例如状态码、响应头以及响应体。通常我们会根据响应体的格式,使用 response.json()(如果响应体是 JSON 格式)、response.text()(如果响应体是文本格式)或 response.blob()(如果响应体是二进制大对象)等方法来解析响应体。

POST 请求

发起一个 POST 请求并发送数据:

const data = {
    username: 'JohnDoe',
    password: 'password123'
};

fetch('https://example.com/api/login', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
})
   .then((response) => {
        return response.json();
    })
   .then((result) => {
        console.log(result);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

在这个例子中,fetch 函数的第二个参数是一个配置对象。method 属性指定请求方法为 POSTheaders 属性设置请求头,告诉服务器发送的数据是 JSON 格式,body 属性则是要发送的数据,需要使用 JSON.stringify 方法将 JavaScript 对象转换为 JSON 字符串。

处理响应状态码

在实际应用中,我们需要根据服务器返回的状态码来进行不同的处理。例如,当状态码为 404 时,表示资源未找到,我们可以提示用户相应的信息。

fetch('https://example.com/api/nonexistentdata')
   .then((response) => {
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

在这个例子中,通过 response.ok 属性来判断响应状态码是否在 200 - 299 之间,如果不在这个范围,则抛出一个错误,在 catch 块中进行统一的错误处理。

Fetch API 深入探究

自定义请求头

除了常见的 Content - Type 等请求头,我们还可以根据服务器的需求自定义请求头。例如,有些服务器可能需要在请求头中传递身份验证令牌。

const token = 'your - authentication - token';

fetch('https://example.com/api/protecteddata', {
    method: 'GET',
    headers: {
        'Authorization': `Bearer ${token}`
    }
})
   .then((response) => {
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

在这个例子中,Authorization 请求头用于传递身份验证令牌,Bearer 是一种常见的令牌类型前缀。

处理响应头

Response 对象不仅包含响应体,还包含响应头信息。我们可以通过 response.headers 属性来获取响应头。

fetch('https://example.com/api/data')
   .then((response) => {
        console.log('响应头:', response.headers);
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

response.headers 是一个 Headers 对象,它提供了一系列方法来操作和查询响应头。例如,response.headers.get('Content - Type') 可以获取 Content - Type 响应头的值。

处理重定向

默认情况下,Fetch API 会自动跟随重定向(状态码 301、302 等)。但是,我们可以通过设置 fetch 配置对象的 redirect 属性来改变这种行为。

fetch('https://example.com/api/redirecteddata', {
    redirect: 'manual'
})
   .then((response) => {
        if (response.redirected) {
            console.log('重定向到:', response.url);
        }
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

在这个例子中,redirect 属性设置为 manual,表示手动处理重定向。当响应是重定向时,response.redirectedtrue,我们可以通过 response.url 获取最终的重定向地址。

取消 Fetch 请求

在某些情况下,我们可能需要取消正在进行的 Fetch 请求,例如用户在请求过程中关闭了相关页面或取消了操作。在 JavaScript 中,可以使用 AbortControllerAbortSignal 来实现取消 Fetch 请求。

const controller = new AbortController();
const signal = controller.signal;

fetch('https://example.com/api/slowdata', { signal })
   .then((response) => {
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        if (error.name === 'AbortError') {
            console.log('请求已取消');
        } else {
            console.error('请求出错:', error);
        }
    });

// 模拟一段时间后取消请求
setTimeout(() => {
    controller.abort();
}, 5000);

在这个例子中,首先创建了一个 AbortController 对象和对应的 AbortSignal 对象,并将 signal 传递给 fetch 函数。然后,通过 setTimeout 模拟在 5 秒后调用 controller.abort() 方法来取消请求。当请求被取消时,catch 块中会捕获到一个 AbortError 错误,我们可以根据这个错误类型来判断请求是被取消了。

Fetch API 与其他异步编程方式结合

Fetch API 与 Promise 链式调用

由于 Fetch API 本身返回的是 Promise,所以它可以很方便地与 Promise 的链式调用结合。例如,我们可能需要先获取一个资源的 URL,然后再根据这个 URL 发起另一个 Fetch 请求。

function getResourceUrl() {
    return new Promise((resolve, reject) => {
        // 模拟异步操作获取 URL
        setTimeout(() => {
            resolve('https://example.com/api/data - resource');
        }, 1000);
    });
}

getResourceUrl()
   .then((url) => {
        return fetch(url);
    })
   .then((response) => {
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

在这个例子中,getResourceUrl 函数返回一个 Promise,在 then 方法中,当获取到 URL 后,立即使用这个 URL 发起 Fetch 请求,形成了一个 Promise 链式调用。

Fetch API 与 async/await

Fetch API 与 async/await 结合使用可以使代码更加简洁易读。例如,假设我们需要多次发起 Fetch 请求并顺序处理响应。

async function fetchDataSequentially() {
    try {
        const response1 = await fetch('https://example.com/api/data1');
        const data1 = await response1.json();
        console.log(data1);

        const response2 = await fetch('https://example.com/api/data2');
        const data2 = await response2.json();
        console.log(data2);

        const response3 = await fetch('https://example.com/api/data3');
        const data3 = await response3.json();
        console.log(data3);
    } catch (error) {
        console.error('请求出错:', error);
    }
}

fetchDataSequentially();

在这个例子中,fetchDataSequentially 是一个 async 函数,通过 await 依次等待每个 Fetch 请求完成并处理响应,代码结构清晰,如同同步代码一样易于理解。

错误处理与最佳实践

Fetch API 中的错误类型

  1. 网络错误:当无法建立网络连接时,fetch 返回的 Promise 会被拒绝,并抛出一个 TypeError。例如,网络断开或者 URL 格式错误等情况。
fetch('invalid - url')
   .catch((error) => {
        if (error instanceof TypeError) {
            console.log('网络错误或 URL 格式错误:', error);
        }
    });
  1. HTTP 错误:如前文所述,当服务器返回的状态码不在 200 - 299 之间时,fetch 返回的 Promise 不会被拒绝(默认行为),需要我们手动检查 response.ok 并抛出错误。
fetch('https://example.com/api/nonexistentdata')
   .then((response) => {
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
    })
   .catch((error) => {
        if (error.message.includes('HTTP error')) {
            console.log('HTTP 错误:', error);
        }
    });
  1. 解析错误:当使用 response.json()response.text() 等方法解析响应体失败时,会抛出错误。例如,响应体不是预期的 JSON 格式。
fetch('https://example.com/api/invalid - json - data')
   .then((response) => {
        return response.json();
    })
   .catch((error) => {
        if (error instanceof SyntaxError) {
            console.log('解析错误:', error);
        }
    });

最佳实践

  1. 统一错误处理:在应用中,可以创建一个全局的错误处理函数来处理所有 Fetch 请求可能出现的错误,这样可以保持代码的一致性。
function handleFetchError(error) {
    if (error instanceof TypeError) {
        console.log('网络错误或 URL 格式错误:', error);
    } else if (error.message.includes('HTTP error')) {
        console.log('HTTP 错误:', error);
    } else if (error instanceof SyntaxError) {
        console.log('解析错误:', error);
    } else {
        console.log('其他错误:', error);
    }
}

async function fetchData() {
    try {
        const response = await fetch('https://example.com/api/data');
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        console.log(data);
    } catch (error) {
        handleFetchError(error);
    }
}

fetchData();
  1. 设置合理的超时时间:为了防止 Fetch 请求长时间等待,可以设置一个合理的超时时间。结合前文提到的 AbortControllerAbortSignal 来实现。
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
    const controller = new AbortController();
    const signal = controller.signal;

    const id = setTimeout(() => controller.abort(), timeout);

    try {
        const response = await fetch(url, { ...options, signal });
        clearTimeout(id);
        return response;
    } catch (error) {
        if (error.name === 'AbortError') {
            throw new Error('请求超时');
        }
        throw error;
    }
}

fetchWithTimeout('https://example.com/api/slowdata', {}, 3000)
   .then((response) => {
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

在这个例子中,fetchWithTimeout 函数接受一个 URL、一个可选的配置对象和一个超时时间(默认为 5000 毫秒)。通过 setTimeoutAbortController 实现了请求超时功能。

  1. 缓存策略:对于一些不经常变化的数据,可以考虑使用缓存策略来减少不必要的网络请求,提高应用性能。可以结合 localStorage 或者浏览器的缓存机制来实现。
async function fetchCachedData(url) {
    const cachedData = localStorage.getItem(url);
    if (cachedData) {
        return JSON.parse(cachedData);
    }

    const response = await fetch(url);
    const data = await response.json();
    localStorage.setItem(url, JSON.stringify(data));
    return data;
}

fetchCachedData('https://example.com/api/staticdata')
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

在这个例子中,fetchCachedData 函数首先检查 localStorage 中是否有缓存的数据,如果有则直接返回;如果没有,则发起 Fetch 请求,获取数据后将其缓存到 localStorage 中。

通过以上对 JavaScript 中异步请求基础以及 Fetch API 的详细介绍,包括其基本用法、深入探究、与其他异步编程方式的结合以及错误处理和最佳实践,希望能帮助开发者在实际项目中更高效、准确地使用异步请求和 Fetch API 来构建强大的网络应用。