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

JavaScript异步请求的Fetch API与Promise结合

2023-12-205.7k 阅读

JavaScript异步请求基础

在JavaScript中,异步操作是非常重要的一部分,尤其是在处理网络请求时。传统的同步请求会阻塞代码的执行,直到请求完成,这在网页应用中会导致用户界面卡顿,影响用户体验。而异步请求允许代码在等待请求响应的同时继续执行其他任务,提升了应用的流畅性和响应性。

JavaScript最初通过回调函数来处理异步操作。例如,使用XMLHttpRequest对象发送异步请求:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com/api/data', true);
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    }
};
xhr.send();

然而,随着异步操作的增多,回调函数嵌套会变得非常复杂,形成所谓的“回调地狱”。例如:

asyncFunction1((result1) => {
    asyncFunction2(result1, (result2) => {
        asyncFunction3(result2, (result3) => {
            // 更多嵌套...
        });
    });
});

为了解决回调地狱的问题,Promise被引入。

Promise详解

Promise是JavaScript中处理异步操作的一种更优雅的方式。一个Promise代表一个异步操作的最终完成(或失败)及其结果值。Promise有三种状态:

  1. Pending(进行中):初始状态,既不是成功,也不是失败状态。
  2. Fulfilled(已成功):意味着操作成功完成,此时Promise有一个 resolved 值。
  3. Rejected(已失败):意味着操作失败,此时Promise有一个 rejection 原因。

创建一个Promise实例:

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

Promise实例有thencatchfinally方法来处理不同状态的结果。

  • then方法:用于处理Promise成功(resolved)的情况。它接受一个回调函数作为参数,该回调函数会在Promise变为resolved时被调用,并且会传入Promise的 resolved 值。
myPromise.then((value) => {
    console.log(value); // 输出:操作成功
});
  • catch方法:用于处理Promise失败(rejected)的情况。它接受一个回调函数作为参数,该回调函数会在Promise变为rejected时被调用,并且会传入Promise的 rejection 原因。
myPromise.catch((error) => {
    console.error(error); // 输出:操作失败
});
  • finally方法:无论Promise是成功还是失败,都会执行finally中的回调函数。
myPromise.finally(() => {
    console.log('操作结束');
});

多个Promise可以通过链式调用的方式进行组合,避免了回调地狱。例如:

function asyncFunction1() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('结果1');
        }, 1000);
    });
}
function asyncFunction2(result1) {
    return new Promise((resolve) => {
        setTimeout(() => {
            const newResult = result1 + ' -> 结果2';
            resolve(newResult);
        }, 1000);
    });
}
function asyncFunction3(result2) {
    return new Promise((resolve) => {
        setTimeout(() => {
            const newResult = result2 + ' -> 结果3';
            resolve(newResult);
        }, 1000);
    });
}
asyncFunction1()
   .then(asyncFunction2)
   .then(asyncFunction3)
   .then((finalResult) => {
        console.log(finalResult); // 输出:结果1 -> 结果2 -> 结果3
    })
   .catch((error) => {
        console.error(error);
    });

Fetch API介绍

Fetch API是JavaScript中用于发起网络请求的现代接口,它提供了一个更强大且灵活的方式来处理HTTP请求和响应。Fetch API基于Promise,这使得它与现有的异步操作处理方式很好地集成。

基本的fetch请求非常简单:

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

在上述代码中:

  1. fetch函数接受一个URL作为参数,发起一个GET请求。它返回一个Promise,该Promise在接收到响应时被 resolved,其 resolved 值是一个Response对象。
  2. 第一个then回调函数处理Response对象。Response对象提供了多种方法来获取响应数据,如json()(用于解析JSON数据)、text()(用于获取文本数据)、blob()(用于获取二进制大对象数据)等。这里调用response.json(),它返回一个新的Promise,用于解析JSON格式的响应数据。
  3. 第二个then回调函数处理解析后的JSON数据。
  4. catch回调函数捕获请求过程中发生的任何错误。

Fetch API的请求方法

fetch函数默认发起GET请求,但可以通过传递一个配置对象来指定其他请求方法,如POST、PUT、DELETE等。

POST请求

const data = {
    key: 'value'
};
fetch('https://example.com/api/data', {
    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);
    });

在上述代码中:

  1. method字段指定请求方法为POST
  2. headers字段设置请求头,这里设置Content-Typeapplication/json,表示请求体是JSON格式的数据。
  3. body字段包含要发送的数据,通过JSON.stringify将JavaScript对象转换为JSON字符串。

PUT请求

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

DELETE请求

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

Fetch API与Promise的结合优势

  1. 链式调用Fetch API基于Promise,可以进行链式调用,使得代码更加清晰和易于维护。例如在处理多个连续的异步请求时,能够清晰地表达请求的先后顺序和依赖关系。
  2. 错误处理:通过catch方法统一处理请求过程中的错误,避免了在每个回调函数中重复处理错误的繁琐。
  3. 更好的异步控制Promise的特性使得可以对异步操作进行更细粒度的控制,比如使用Promise.allPromise.race来处理多个异步请求。

Promise.all

Promise.all用于并行处理多个Promise,当所有Promise都 resolved 时,它返回的Promise才会 resolved。例如,同时发起多个fetch请求:

const promise1 = fetch('https://example.com/api/data1');
const promise2 = fetch('https://example.com/api/data2');
Promise.all([promise1, promise2])
   .then((responses) => {
        return Promise.all(responses.map((response) => response.json()));
    })
   .then((dataList) => {
        console.log(dataList);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

在上述代码中:

  1. 首先创建了两个fetch请求的Promise对象promise1promise2
  2. 使用Promise.all传入这两个Promise数组,当这两个请求都成功响应时,Promise.all返回的Promise会 resolved,其 resolved 值是一个包含两个Response对象的数组。
  3. 通过map方法对每个Response对象调用json()方法,并再次使用Promise.all等待所有解析操作完成,最终得到一个包含解析后JSON数据的数组。

Promise.race

Promise.race同样接受一个Promise数组作为参数,但只要数组中的任何一个Promise resolved 或 rejected,它返回的Promise就会 resolved 或 rejected。例如:

const promise1 = fetch('https://example.com/api/data1');
const promise2 = fetch('https://example.com/api/data2');
Promise.race([promise1, promise2])
   .then((response) => {
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

在这个例子中,先完成的那个fetch请求的结果会被处理,另一个请求会继续执行但不会被处理(除非需要取消)。

Fetch API的高级特性

  1. 请求缓存fetch支持请求缓存,通过设置cache选项来控制缓存策略。例如:
fetch('https://example.com/api/data', {
    cache:'reload'
})
   .then((response) => {
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

cache的取值可以是default(默认缓存策略)、no - cache(不使用缓存,总是从服务器获取最新数据)、reload(重新从服务器加载数据,不使用缓存)、force - cache(强制使用缓存,即使缓存过期)等。

  1. 跨域请求fetch遵循同源策略,但可以通过设置mode选项来处理跨域请求。例如:
fetch('https://otherdomain.com/api/data', {
    mode: 'cors'
})
   .then((response) => {
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

mode的取值可以是same - origin(只允许同源请求)、no - cors(允许跨域请求,但只能使用简单请求,不支持PUTDELETE等方法和自定义请求头)、cors(允许跨域请求,需要服务器设置正确的CORS头)、navigate(用于导航请求)。

  1. 处理重定向fetch默认会跟随重定向,但可以通过设置redirect选项来控制。例如:
fetch('https://example.com/api/data', {
    redirect: 'error'
})
   .then((response) => {
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

redirect的取值可以是follow(默认,跟随重定向)、error(如果发生重定向,返回一个 rejected 的Promise)、manual(手动处理重定向,需要在Response对象上调用response.redirected来判断是否发生重定向,并手动处理)。

实际应用场景

  1. 数据获取与渲染:在网页应用中,经常需要从服务器获取数据并渲染到页面上。例如,一个博客网站需要获取文章列表数据并展示:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>博客文章列表</title>
</head>

<body>
    <ul id="articleList"></ul>
    <script>
        fetch('https://blog.example.com/api/articles')
           .then((response) => {
                return response.json();
            })
           .then((articles) => {
                const articleList = document.getElementById('articleList');
                articles.forEach((article) => {
                    const listItem = document.createElement('li');
                    listItem.textContent = article.title;
                    articleList.appendChild(listItem);
                });
            })
           .catch((error) => {
                console.error('获取文章列表出错:', error);
            });
    </script>
</body>

</html>
  1. 表单提交:当用户提交表单时,使用fetch将表单数据发送到服务器进行处理。例如:
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF - 8">
    <title>表单提交</title>
</head>

<body>
    <form id="myForm">
        <label for="name">姓名:</label><input type="text" id="name" name="name"><br>
        <label for="email">邮箱:</label><input type="email" id="email" name="email"><br>
        <input type="submit" value="提交">
    </form>
    <script>
        const form = document.getElementById('myForm');
        form.addEventListener('submit', (event) => {
            event.preventDefault();
            const formData = new FormData(form);
            fetch('https://example.com/api/submitForm', {
                method: 'POST',
                body: formData
            })
               .then((response) => {
                    return response.json();
                })
               .then((result) => {
                    console.log(result);
                })
               .catch((error) => {
                    console.error('提交表单出错:', error);
                });
        });
    </script>
</body>

</html>

在上述代码中:

  1. 给表单添加了submit事件监听器,阻止表单默认的提交行为。
  2. 使用FormData对象来收集表单数据。
  3. 发起POST请求,将FormData对象作为请求体发送到服务器。

注意事项

  1. 旧浏览器兼容性Fetch API在一些旧版本浏览器中不支持,需要使用polyfill来提供兼容性。例如,可以使用whatwg - fetch库来实现兼容。
  2. 网络错误处理:虽然catch可以捕获请求过程中的错误,但对于一些网络相关的底层错误,如网络连接中断等,可能需要更复杂的处理机制,例如使用navigator.onLine属性来检测网络状态。
  3. 响应状态码处理fetch默认情况下,即使响应状态码是404、500等错误码,Promise也不会被 rejected,需要手动检查response.status来处理非200 - 299范围的状态码。例如:
fetch('https://example.com/api/data')
   .then((response) => {
        if (!response.ok) {
            throw new Error('网络响应错误:'+ response.status);
        }
        return response.json();
    })
   .then((data) => {
        console.log(data);
    })
   .catch((error) => {
        console.error('请求出错:', error);
    });

通过Fetch APIPromise的结合,JavaScript开发者能够更高效、更优雅地处理异步网络请求,构建出更强大、更流畅的网络应用。无论是简单的数据获取,还是复杂的多请求并发处理,这种组合都提供了良好的解决方案。在实际开发中,需要根据具体的业务需求和场景,合理运用这些技术,同时注意兼容性和错误处理等方面的问题。