选择正确的方法

为了完成这个模块,我们将简要讨论之前章节谈论过编码技术和功能,看看你应该使用哪一个,并提供适当的建议和提醒。随着时间的推移,我们可能会添加到此资源中。

预备条件: 基本的计算机素养,对JavaScript基础知识的合理理解。
目标: 能够在使用不同的异步编程技术时做出合理的选择。

异步回调

通常在旧式API中找到,涉及将函数作为参数传递给另一个函数,然后在异步操作完成时调用该函数,以便回调可以依次对结果执行某些操作。这是promise的前身;它不那么高效或灵活。仅在必要时使用。

Useful for...
Single delayed operation Repeating operation Multiple sequential operations Multiple simultaneous operations
No Yes (recursive callbacks) Yes (nested callbacks) No

代码示例

通过XMLHttpRequest API加载资源的示例(run it live,并查看see the source):

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);

缺陷

  • 嵌套回调可能很麻烦且难以阅读(即“回调地狱”)
  • 每层嵌套都需要故障回调,而使用promises,您只需使用一个.catch()代码块来处理整个链的错误。
  • 异步回调不是很优雅。
  • Promise回调总是按照它们放在事件队列中的严格顺序调用;异步回调不是。
  • 当传入到一个第三方库时,异步回调对函数如何执行失去完全控制。

浏览器兼容性

非常好的一般支持,尽管API中回调的确切支持取决于特定的API。有关更具体的支持信息,请参阅您正在使用的API的参考文档。

更多信息

setTimeout()

setTimeout() 是一种允许您在经过任意时间后运行函数的方法

Useful for...
Single delayed operation Repeating operation Multiple sequential operations Multiple simultaneous operations
Yes Yes (recursive timeouts) Yes (nested timeouts) No

代码示例

这里浏览器将在执行匿名函数之前等待两秒钟,然后将显示警报消息(see it running livesee the source code):

let myGreeting = setTimeout(function() {
  alert('Hello, Mr. Universe!');
}, 2000)

缺陷

您可以使用递归的setTimeout()调用以类似于setInterval()的方式重复运行函数,使用如下代码:

let i = 1;
setTimeout(function run() {
  console.log(i);
  i++;

  setTimeout(run, 100);
}, 100);

递归setTimeout()setInterval()之间存在差异:

  • 递归setTimeout()保证两次执行间经过指定的时间量(在本例中为100ms);代码将运行,然后等待100毫秒再次运行。无论代码运行多长时间,间隔都是相同的。
  • 使用setInterval(),我们选择的时间间隔包含了运行代码所花费的时间。(还是100ms为例)假设代码需要40毫秒才能运行 –– 间隔最终只会有60毫秒。

当你的代码有可能比你分配的时间间隔更长时间运行时,最好使用递归的setTimeout() ––这将使执行之间的时间间隔保持不变,无论代码执行多长时间,你不会得到错误。

浏览器兼容性

BCD tables only load in the browser

更多信息

setInterval()

setInterval()函数允许重复执行一个函数,并设置时间间隔。不如requestAnimationFrame()有效率,但允许您选择运行速率/帧速率。

Useful for...
Single delayed operation Repeating operation Multiple sequential operations Multiple simultaneous operations
No Yes No (unless it is the same one) No

代码示例

以下函数创建一个新的Date()对象,使用toLocaleTimeString()从中提取时间字符串,然后在UI中显示它。然后我们使用setInterval()每秒运行一次,创建每秒更新一次的数字时钟的效果(see this livesee the source):

function displayTime() {
   let date = new Date();
   let time = date.toLocaleTimeString();
   document.getElementById('demo').textContent = time;
}

const createClock = setInterval(displayTime, 1000);

缺陷

  • 帧速率未针对运行动画的系统进行优化,并且可能效率低下。除非您需要选择特定(较慢)的帧速率,否则通常最好使用requestAnimationFrame().

浏览器兼容性

BCD tables only load in the browser

更多信息

requestAnimationFrame()

requestAnimationFrame()是一种允许您以给定当前浏览器/系统的最佳帧速率重复且高效地运行函数的方法。除非您需要特定的速率帧,否则您应该尽可能使用它而不要去使用setInterval()/recursive setTimeout()

Useful for...
Single delayed operation Repeating operation Multiple sequential operations Multiple simultaneous operations
No Yes No (unless it is the same one) No

代码示例

一个简单的动画旋转器;你可以查看example live on GitHub(参见 source code ):

const spinner = document.querySelector('div');
let rotateCount = 0;
let startTime = null;
let rAF;

function draw(timestamp) {
  if(!startTime) {
    startTime = timestamp;
  }

  let rotateCount = (timestamp - startTime) / 3;

  spinner.style.transform = 'rotate(' + rotateCount + 'deg)';

  if(rotateCount > 359) {
    rotateCount = 0;
  }

  rAF = requestAnimationFrame(draw);
}

draw();

缺陷

  • 您无法使用requestAnimationFrame()选择特定的帧速率。如果需要以较慢的帧速率运行动画,则需要使用setInterval()或递归的setTimeout()

浏览器兼容性

BCD tables only load in the browser

更多信息

Promises

Promises 是一种JavaScript功能,允许您运行异步操作并等到它完全完成后再根据其结果运行另一个操作。 Promise是现代异步JavaScript的支柱。

Useful for...
Single delayed operation Repeating operation Multiple sequential operations Multiple simultaneous operations
No No Yes See Promise.all(), below

代码示例

以下代码从服务器获取图像并将其显示在 <img> 元素中;(see it live alsothe source code):

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);
});

缺陷

Promise链可能很复杂,难以解析。如果你嵌套了许多promises,你最终可能会遇到类似的麻烦来回调地狱。例如:

remotedb.allDocs({
  include_docs: true,
  attachments: true
}).then(function (result) {
  var docs = result.rows;
  docs.forEach(function(element) {
    localdb.put(element.doc).then(function(response) {
      alert("Pulled doc with id " + element.doc._id + " and added to local db.");
    }).catch(function (err) {
      if (err.name == 'conflict') {
        localdb.get(element.doc._id).then(function (resp) {
          localdb.remove(resp._id, resp._rev).then(function (resp) {
// et cetera...

最好使用promises的链功能,这样使用更平顺,更易于解析的结构:

remotedb.allDocs(...).then(function (resultOfAllDocs) {
  return localdb.put(...);
}).then(function (resultOfPut) {
  return localdb.get(...);
}).then(function (resultOfGet) {
  return localdb.put(...);
}).catch(function (err) {
  console.log(err);
});

乃至:

remotedb.allDocs(...)
.then(resultOfAllDocs => {
  return localdb.put(...);
})
.then(resultOfPut => {
  return localdb.get(...);
})
.then(resultOfGet => {
  return localdb.put(...);
})
.catch(err => console.log(err));

这涵盖了很多基础知识。对于更完整的论述,请参阅诺兰劳森的We have a problem with promises

浏览器兼容性

BCD tables only load in the browser

更多信息

Promise.all()

一种JavaScript功能,允许您等待多个promises完成,然后根据所有其他promises的结果运行进一步的操作。

Useful for...
Single delayed operation Repeating operation Multiple sequential operations Multiple simultaneous operations
No No No Yes

代码示例

以下示例从服务器获取多个资源,并使用Promise.all()等待所有资源可用,然后显示所有这些资源––  see it live,并查看source code

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);
  });
}

// 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
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]);
  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);
});

缺陷

  • 如果Promise.all()拒绝,那么你在其数组参数中输入的一个或多个promise(s)就会被拒绝,或者可能根本不返回promises。你需要检查每一个,看看他们返回了什么。

浏览器兼容性

BCD tables only load in the browser

更多信息

Async/await

构造在promises之上的语法糖,允许您使用更像编写同步回调代码的语法来运行异步操作。

Useful for...
Single delayed operation Repeating operation Multiple sequential operations Multiple simultaneous operations
No No Yes Yes (in combination with Promise.all())

代码示例

以下示例是我们之前看到的简单承诺示例的重构,该示例获取并显示图像,使用async / await编写(see it live,并查看source code):

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);
}

myFetch();

缺陷

  • 您不能在非async函数内或代码的顶级上下文环境中使用await运算符。这有时会导致需要创建额外的函数封包,这在某些情况下会略微令人沮丧。但大部分时间都值得。
  • 浏览器对async / await的支持不如promises那样好。如果你想使用async / await但是担心旧的浏览器支持,你可以考虑使用BabelJS 库 - 这允许你使用最新的JavaScript编写应用程序,让Babel找出用户浏览器需要的更改。

浏览器兼容性

BCD tables only load in the browser

更多信息

本章内容