JavaScript:异步操作与回调简析

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


回调(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 关键字

asyncawait是基于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  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);
      })

以上代码运行结果
JavaScript:异步操作与回调简析_第1张图片
可见每个timeTest()都必须等待上一个timeTest()执行完成
这显然不是我们想要的
我们可是异步操作!

那么
修改 timeTest() 函数如下

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

结果如下
JavaScript:异步操作与回调简析_第2张图片
如何理解这个问题呢
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

你可能感兴趣的:(JavaScript)