JavaScript:异步操作与回调简析

    xiaoxiao2025-08-03  22

    了解异步操作与回调,是学习网络请求必不可少的


    回调(Callback)

    当执行一个耗时操作或者等待某些触发性事件时,我们得保证耗时操作完成或者事件触发后才能进行下一步动作,这就是回调的应用场景(MDN文档居然说回调过时了QAQ)

    截图为证 一个经典的例子便是监听器

    var action = function(){} btn.addEventListener("click", action);

    以上代码即是为btn注册了一个监听器,当btn被点击后,执行action函数 action函数即是一个回调函数,它既没有被coder直接调用,也不会在被传参为btn的回调函数时立即执行

    XMLHttpRequest方式的网络请求中异步回调的使用 function loadAsset(url, type, callback) { let xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = type; xhr.onload = function() { callback(xhr.response); }; xhr.send(); } function displayImage(blob) { let objectURL = URL.createObjectURL(blob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } loadAsset('coffee.jpg', 'blob', displayImage);displayImage作为回调函数传入

    并非所有回调都是异步的 比如 Array.prototype.forEach(function) 遍历数组执行function操作 就是立即执行的


    Promise

    Promise是您在现代Web API中使用的新的异步代码样式 Promise允许推迟进一步的操作,直到上一个操作完成或响应其失败,Promise就是表示上一步操作结果,来调用下一步操作的对象,起承上启下的过渡作用 一个经典的应用是fetch()API,它是XMLHTTPRequest的现代版本

    以下是一个从服务器获取数据的简单示例

    fetch('products.json').then(function(response) { return response.json(); }).then(function(json) { products = json; initialize(); }).catch(function(err) { console.log('Fetch problem: ' + err.message); }); fetch()返回一个Promise对象,这个对象是表示异步操作完成或失败的对象;.then()函数中定义了一个回调函数,可以接收上一步成功操作的结果,进行下一个异步操作;每一个.then()也返回该操作的Promise对象,因此我们可以根据需要链接多个异步操作;(听起来就很爽)如果任何.then()块失败,可以由.catch()捕捉异常

    像promises这样的异步操作被放入一个事件队列中,该事件队列不会阻止后续的JavaScript代码运行。排队的操作将尽快完成,然后将结果返回到JavaScript环境

    Promise与回调

    Promise本质上是将异步操作的结果以对象的形式返回到主过程,在主过程中将回调函数附加到这个对象上,再去异步执行,再返回操作结果;而回调则是将回调函数交给异步过程,在异步过程中进行调用

    Promise与回调相比有一些优点

    可以使用多个.then()块链接多个异步操作,这种方式远比多个嵌套的回调直观、可读Promise总是严格按照它们放置在事件队列中的顺序调用只需一个.catch块处理异常,比在嵌套回调的每一层中处理错误方便

    Promise与监听器有相似之处,但又有些许不同

    Promise只能成功(fulfilled)或失败(rejected)一次。它不能成功或失败两次。一旦操作完成,就不能从成功转为失败,反之亦然当获取到一个Promise对象后,不做任何处理,在以后的时间为它添加.then()回调,也会调用正确的回调方法(就是说,在得到一个Promise后,可以在适当的时候增加.then()执行下一步动作)

    异步操作像是多车道行车,不会阻塞主车道。同步操作则都是行驶在主车道上


    async/await 关键字

    async 和 await是基于Promise的 使用async关键字修饰函数声明,使该函数变成一个异步函数,返回一个Promise 使用await关键字,仅可在async函数中使用,表示等待一个Promise的返回,如果修饰的表达式的返回值并不是Promise对象,那么就返回该值本身

    async

    以下几种方式都是定义了一个异步函数hello,返回函数执行完后的Promise

    async function hello() { return "Hello" }; let hello = async function() { return "Hello" }; let hello = async () => { return "Hello" }; await

    使用await在async函数中等待任何返回Promise对象的表达式

    fetch('coffee.jpg') .then(response => response.blob()) .then(myBlob => { let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch(e => { console.log('There has been a problem with your fetch operation: ' + e.message); });

    用async/await改写

    async function myFetch() { let response = await fetch('coffee.jpg'); let myBlob = await response.blob(); let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } 或是 async function myFetch() { let response = await fetch('coffee.jpg'); return await response.blob(); } myFetch().then((blob) => { let objectURL = URL.createObjectURL(blob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); });

    await 会暂停myFetch函数(当然允许其他代码执行),等到异步操作返回结果后继续向下运行


    为 async/await 添加错误处理

    使用同步形式的 try…catch… async function myFetch() { try { let response = await fetch('coffee.jpg'); let myBlob = await response.blob(); let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); } catch(e) { console.log(e); } }

    使用.catch()块 async function myFetch() { let response = await fetch('coffee.jpg'); return await response.blob(); } myFetch().then((blob) => { let objectURL = URL.createObjectURL(blob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image); }) .catch((e) => console.log(e) );

    在以上的代码中,不能使用 try…catch…来包裹myFetch()和.then()块捕获异常 因为 try…catch…没法捕获异步函数中抛出的异常 而.catch()块可以捕获异步函数调用中抛出的异常,也能捕获promise链中的异常


    为什么使用 async/await

    .then()块的链接远比多层Callback清晰可读,而 async 中的 await 使得异步程序完全可以以同步代码的形式编写,这是其他异步操作不可比的


    等待多个Promise

    如果一个操作需要等待多个异步操作完成 可以使用Promise.all()方法

    function fetchAndDecode(url, type) { // Returning the top level promise, so the result of the entire chain is returned out of the function return fetch(url).then(response => { // Depending on what type of file is being fetched, use the relevant function to decode its contents if(type === 'blob') { return response.blob(); } else if(type === 'text') { return response.text(); } }) .catch(e => { console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message); }); } async function displayContent() { // Call the fetchAndDecode() method to fetch the images and the text, and store their promises in variables let coffee = fetchAndDecode('coffee.jpg', 'blob'); let tea = fetchAndDecode('tea.jpg', 'blob'); let description = fetchAndDecode('description.txt', 'text'); // Use Promise.all() to run code only when all three function calls have resolved let values = await Promise.all([coffee, tea, description]); console.log(values); // Store each value returned from the promises in separate variables; create object URLs from the blobs let objectURL1 = URL.createObjectURL(values[0]); let objectURL2 = URL.createObjectURL(values[1]); let descText = values[2]; // Display the images in <img> elements let image1 = document.createElement('img'); let image2 = document.createElement('img'); image1.src = objectURL1; image2.src = objectURL2; document.body.appendChild(image1); document.body.appendChild(image2); // Display the text in a paragraph let para = document.createElement('p'); para.textContent = descText; document.body.appendChild(para); } displayContent() .catch((e) => console.log(e) );

    用 async/await 改写 fetchAndDecode

    async function fetchAndDecode(url, type) { try { // Returning the top level promise, so the result of the entire chain is returned out of the function let response = await fetch(url); let content; // Depending on what type of file is being fetched, use the relevant function to decode its contents if(type === 'blob') { content = await response.blob(); } else if(type === 'text') { content = await response.text(); } return content; } finally { console.log(`fetch attempt for "${url}" finished.`); }; }

    Promise.all()后也可以跟.then()块处理

    Promise.all([coffee, tea, description]).then(values => { console.log(values); // Store each value returned from the promises in separate variables; create object URLs from the blobs let objectURL1 = URL.createObjectURL(values[0]); ...... ...... });

    async/await 使用中的问题

    如果有一系列操作接连进行 await,那么你的异步函数就频频阻塞,等待Promise返回,真的变成了"同步代码"

    function timeoutPromise(interval) { return new Promise((resolve, reject) => { setTimeout(function(){ resolve("done"); }, interval); }); }; async function timeTest() { await timeoutPromise(3000); await timeoutPromise(3000); await timeoutPromise(3000); } let startTime = Date.now(); timeTest().then(() => { let finishTime = Date.now(); let timeTaken = finishTime - startTime; alert("Time taken in milliseconds: " + timeTaken); })

    以上代码运行结果 可见每个timeTest()都必须等待上一个timeTest()执行完成 这显然不是我们想要的 我们可是异步操作!

    那么 修改 timeTest() 函数如下

    async function timeTest() { let a = timeoutPromise(3000); let b = timeoutPromise(3000); let c = timeoutPromise(3000); await a; await b; await c; }

    结果如下 如何理解这个问题呢 await 三连好像是开一个线程去执行任务1,任务1执行完后,再开一个线程执行任务2,任务2执行完后,开线程执行任务3 而将返回的Promise先用变量保存,再一一 await,像是连开了三个线程去分别做三个任务,最后等待三个任务都完成

    所以要考虑一系列任务间的同步关系,选择合适的 await 方式

    另外需要注意的一点是:await 只能在 async 函数中使用


    async/await 在OO中的使用

    class Person { constructor(first, last, age, gender, interests) { this.name = { first, last }; this.age = age; this.gender = gender; this.interests = interests; } async greeting() { return await Promise.resolve(`Hi! I'm ${this.name.first}`); }; farewell() { console.log(`${this.name.first} has left the building. Bye for now!`); }; } let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);

    就可以写出这样的代码

    han.greeting().then(console.log);

    2019/5/27

    最后编辑 2019/6/2

    最新回复(0)