JavaScript进阶之(九) 异步JavaScript

学习整理地址 https://developer.mozilla.org/zh-CN/docs/learn/JavaScript/异步JavaScript

一、一般异步编程概念

通常来说,程序都是顺序执行,同一时刻只会发生一件事。如果一个函数依赖于另一个函数的结果,它只能等待那个函数结束才能继续执行,从用户的角度来说,整个程序才算运行完毕.

Mac 用户有时会经历过这种旋转的彩虹光标(常称为沙滩球),操作系统通过这个光标告诉用户:“现在运行的程序正在等待其他的某一件事情完成,才能继续运行,都这么长的时间了,你一定在担心到底发生了什么事情”。

这是令人沮丧的体验,没有充分利用计算机的计算能力 — 尤其是在计算机普遍都有多核CPU的时代,坐在那里等待毫无意义,你完全可以在另一个处理器内核上干其他的工作,同时计算机完成耗时任务的时候通知你。这样你可以同时完成其他工作,这就是异步编程的出发点。你正在使用的编程环境(就web开发而言,编程环境就是web浏览器)负责为你提供异步运行此类任务的API。

1.1 产生阻塞的代码

异步技术非常有用,特别是在web编程。当浏览器里面的一个web应用进行密集运算还没有把控制权返回给浏览器的时候,整个浏览器就像冻僵了一样,这叫做阻塞;这时候浏览器无法继续处理用户的输入并执行其他任务,直到web应用交回处理器的控制。

我们来看一些阻塞的例子。

例子:在按钮上添加了一个事件监听器,当按钮被点击,它就开始运行一个非常耗时的任务(计算1千万个日期,并在console里显示最后一个日期),然后在DOM里面添加一个段落:

const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  let myDate;
  for(let i = 0; i < 10000000; i++) {
    let date = new Date();
    myDate = date
  }
  console.log(myDate);
  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

运行这个例子的时候,打开JavaScript console,然后点击按钮 — 你会注意到,直到日期的运算结束,最后一个日期在console上显示出来,段落才会出现在网页上。代码按照源代码的顺序执行,只有前面的代码结束运行,后面的代码才会执行。

第二个例子, 我们模拟一个在现实的网页可能遇到的情况:因为渲染UI而阻塞用户的互动,这个例子有2个按钮:

  • “Fill canvas” : 点击的时候用1百万个蓝色的圆填满整个 .
  • “Click me for alert” :点击显示alert 消息.
function expensiveOperation() {
  for(let i = 0; i < 1000000; i++) {
    ctx.fillStyle = 'rgba(0,0,255, 0.2)';
    ctx.beginPath();
    ctx.arc(random(0, canvas.width), random(0, canvas.height), 10, degToRad(0), degToRad(360), false);
    ctx.fill()
  }
}

fillBtn.addEventListener('click', expensiveOperation);

alertBtn.addEventListener('click', () =>
  alert('You clicked me!')
);

如果你点击第一个按钮,然后快速点击第二个,会注意到alert消息并没有出现,只有等到圆圈都画完以后,才会出现:因为第一个操作没有完成之前阻塞了第二个操作的运行.
为什么是这样? 答案是:JavaScript一般来说是单线程的(single threaded)。接着我们来介绍线程的概念。

1.2 线程

一个线程是一个基本的处理过程,程序用它来完成任务。每个线程一次只能执行一个任务:

Task A --> Task B --> Task C

每个任务顺序执行,只有前面的结束了,后面的才能开始。

正如我们之前所说,现在的计算机大都有多个内核(core),因此可以同时执行多个任务。支持多线程的编程语言可以使用计算机的多个内核,同时完成多个任务:

Thread 1: Task A --> Task B
Thread 2: Task C --> Task D

1.2.1 JavaScript 是单线程的

JavaScript 传统上是单线程的。即使有多个内核,也只能在单一线程上运行多个任务,此线程称为主线程(main thread)。我们上面的例子运行如下:

Main thread: Render circles to canvas --> Display alert()

经过一段时间,JavaScript获得了一些工具来帮助解决这种问题。通过 Web workers 可以把一些任务交给一个名为worker的单独的线程,这样就可以同时运行多个JavaScript代码块。一般来说,用一个worker来运行一个耗时的任务,主线程就可以处理用户的交互(避免了阻塞)

Main thread: Task A --> Task C
Worker thread: Expensive task B

记住这些,这个例子重写了前例:在一个单独的worker线程中计算一千万次日期,你再点击按钮,现在浏览器可以在日期计算完成之前显示段落,阻塞消失了。

      const btn = document.querySelector('button');
      const worker = new Worker('worker.js');

      btn.addEventListener('click', () => {
        worker.postMessage('Go!');

        let pElem = document.createElement('p');
        pElem.textContent = 'This is a newly-added paragraph.';
        document.body.appendChild(pElem);
      });

      worker.onmessage = function(e) {
        console.log(e.data);
      }

worker.js

onmessage = function() {
  let myDate;
  for(let i = 0; i < 10000000; i++) {
    let date = new Date();
    myDate = date
  }

  postMessage(myDate);
}

1.3 异步代码

web workers相当有用,但是他们确实也有局限。主要的一个问题是他们不能访问 DOM — 不能让一个worker直接更新UI。我们不能在worker里面渲染1百万个蓝色圆圈,它基本上只能做算数的苦活

其次,虽然在worker里面运行的代码不会产生阻塞,但是基本上还是同步的。当一个函数依赖于几个在它之前运行的过程的结果,这就会成为问题。考虑下面的情况:

Main thread: Task A --> Task B

在这种情况下,比如说Task A 正在从服务器上获取一个图片之类的资源,Task B 准备在图片上加一个滤镜。如果开始运行Task A 后立即尝试运行Task B,你将会得到一个错误,因为图像还没有获取到。

Main thread: Task A --> Task B --> |Task D|
Worker thread: Task C -----------> |      |

在这种情况下,假设Task D 要同时使用 Task B 和Task C的结果,如果我们能保证这两个结果同时提供,程序可能正常运行,但是这不太可能。如果Task D 尝试在其中一个结果尚未可用的情况下就运行,程序就会抛出一个错误。

为了解决这些问题,浏览器允许我们异步运行某些操作。像Promises 这样的功能就允许让一些操作运行 (比如:从服务器上获取图片),然后等待直到结果返回,再运行其他的操作:

Main thread: Task A                   Task B
    Promise:      |__async operation__|

由于操作发生在其他地方,因此在处理异步操作的时候,主线程不会被阻塞。

我们将在下面文章中开始研究如何编写异步代码。 非常令人兴奋,对吧? 继续阅读!

二、介绍异步JS

2.1 同步JavaScript

要理解什么是异步 JavaScript ,我们应该从确切理解同步 JavaScript 开始。本节回顾我们在上面文中看到的一些信息。

先看一个简单的例子 (运行它, 这是源码):

const btn = document.querySelector('button');
btn.addEventListener('click', () => {
  alert('You clicked me!');

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

这段代码, 一行一行的顺序执行:

  1. 先取得一个在DOM里面的 引用。
  2. 点击按钮的时候,添加一个 click 事件监听器:
  3. alert() 消息出现。
  4. 一旦alert 结束,创建一个<p> 元素。
  5. 给它的文本内容赋值。
  6. 最后,把这个段落放进网页。

每一个操作在执行的时候,其他任何事情都没有发生 — 网页的渲染暂停. 任何时候只能做一件事情, 只有一个主线程,其他的事情都阻塞了,直到前面的操作完成。

所以上面的例子,点击了按钮以后,段落不会创建,直到在alert消息框中点击ok,段落才会出现。

2.2 异步JavaScript

就前面提到的种种原因(比如,和阻塞相关)很多网页API特性使用异步代码,特别是从外部的设备上获取资源,譬如,从网络获取文件,访问数据库,从网络摄像头获得视频流,或者向VR头罩广播图像。

为什么使用异步代码这么难?看一个例子,当你从服务器获取一个图像,通常你不可能立马就得到,这需要时间,虽然现在的网络很快。这意味着下面的伪代码可能不能正常工作:

var response = fetch('myImage.png');
var blob = response.blob();
// display your image blob in the UI somehow

因为你不知道下载图片会多久,所以第二行代码执行的时候可能报错(可能间歇的,也可能每次)因为图像还没有就绪。取代的方法就是,代码必须等到 response 返回才能继续往下执行。

在JavaScript代码中,你经常会遇到两种异步编程风格:老派callbacks,新派promise。下面就来分别介绍。

2.3 异步callbacks

异步callbacks 其实就是函数,只不过是作为参数传递给那些在后台执行的其他函数. 当那些后台运行的代码结束,就调用callbacks函数,通知你工作已经完成,或者其他有趣的事情发生了。使用callbacks 有一点老套,在一些老派但经常使用的API里面,你会经常看到这种风格。

举个例子,异步callback 就是addEventListener()第二个参数(前面的例子):

btn.addEventListener('click', () => {
  alert('You clicked me!');

  let pElem = document.createElement('p');
  pElem.textContent = 'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});

第一个参数是侦听的事件类型,第二个就是事件发生时调用的回调函数。.

当我们把回调函数作为一个参数传递给另一个函数时,仅仅是把回调函数定义作为参数传递过去 — 回调函数并没有立刻执行,回调函数会在包含它的函数的某个地方异步执行,包含函数负责在合适的时候执行回调函数。

你可以自己写一个容易的,包含回调函数的函数。来看另外一个例子,用 XMLHttpRequest API 加载资源:

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() 函数,简单的把blob传递给它,生成objectURL,然后再生成一个image元素,把objectURL作为image的源地址,最后显示这张图片。 然后,我们创建 loadAsset() 函数,把URL,type,和回调函数同时都作为参数。函数用 XMLHttpRequest (通常缩写 “XHR”) 获取给定URL的资源,在获得资源响应后再把响应作为参数传递给回调函数去处理。 (使用 onload 事件处理) ,有点烧脑,是不是?!

回调函数用途广泛 — 他们不仅仅可以用来控制函数的执行顺序和函数之间的数据传递,还可以根据环境的不同,将数据传递给不同的函数,所以对下载好的资源,你可以采用不同的操作来处理,譬如 processJSON(), displayText(), 等等。

请注意,不是所有的回调函数都是异步的 — 有一些是同步的。一个例子就是使用 Array.prototype.forEach() 来遍历数组 :

const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus'];

gods.forEach(function (eachName, index){
  console.log(index + '. ' + eachName);
});

在这个例子中,我们遍历一个希腊神的数组,并在控制台中打印索引和值。forEach() 需要的参数是一个回调函数,回调函数本身带有两个参数,数组元素和索引值。它无需等待任何事情,立即运行。

2.4 Promises

Promises 是新派的异步代码,现代的web APIs经常用到。 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() 只需要一个参数— 资源的网络 URL — 返回一个 promise. promise 是表示异步操作完成或失败的对象。可以说,它代表了一种中间状态。 本质上,这是浏览器说“我保证尽快给您答复”的方式,因此得名“promise”。

这个概念需要练习来适应;它感觉有点像运行中的薛定谔猫。这两种可能的结果都还没有发生,因此fetch操作目前正在等待浏览器试图在将来某个时候完成该操作的结果。然后我们有三个代码块链接到fetch()的末尾:

  • 两个 then()块。两者都包含一个回调函数,如果前一个操作成功,该函数将运行,并且每个回调都接收前一个成功操作的结果作为输入,因此您可以继续对它执行其他操作。每个 .then()块返回另一个promise,这意味着可以将多个.then()块链接到另一个块上,这样就可以依次执行多个异步操作。
  • 如果其中任何一个then()块失败,则在末尾运行catch()块——与同步try…catch类似,catch()提供了一个错误对象,可用来报告发生的错误类型。但是请注意,同步try…catch不能与promise一起工作,尽管它可以与async/await一起工作,稍后您将了解到这一点。

2.4.1 事件队列

像promise这样的异步操作被放入事件队列中,事件队列在主线程完成处理后运行,这样它们就不会阻止后续JavaScript代码的运行。排队操作将尽快完成,然后将结果返回到JavaScript环境。

2.4.2 Promises 对比 callbacks

promises与旧式callbacks有一些相似之处。它们本质上是一个返回的对象,您可以将回调函数附加到该对象上,而不必将回调作为参数传递给另一个函数。

然而,Promise是专门为异步操作而设计的,与旧式回调相比具有许多优点:

  • 您可以使用多个then()操作将多个异步操作链接在一起,并将其中一个操作的结果作为输入传递给下一个操作。这种链接方式对回调来说要难得多,会使回调以混乱的“末日金字塔”告终。 (也称为回调地狱)。
  • Promise总是严格按照它们放置在事件队列中的顺序调用。
  • 错误处理要好得多——所有的错误都由块末尾的一个.catch()块处理,而不是在“金字塔”的每一层单独处理。

2.5 异步代码的本质

让我们研究一个示例,它进一步说明了异步代码的本质,展示了当我们不完全了解代码执行顺序以及将异步代码视为同步代码时可能发生的问题。下面的示例与我们之前看到的非常相似。一个不同之处在于,我们包含了许多console.log()语句,以展示代码将在其中执行的顺序。

console.log ('Starting');  //1 
let image;

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

console.log ('All done!'); //2  

浏览器将会执行代码,看见第一个console.log() 输出(Starting) ,然后创建image 变量。

然后,它将移动到下一行并开始执行fetch()块,但是,因为fetch()是异步执行的,没有阻塞,所以在promise相关代码之后程序继续执行,从而到达最后的console.log()语句(All done!)并将其输出到控制台。

只有当fetch() 块完成运行返回结果给.then() ,我们才最后看到第二个console.log() 消息 (It worked 😉) . 所以 这些消息 可能以 和你预期不同的顺序出现:

Starting
All done!
It worked :)

三、合作异步JS:Timeouts and intervals

3.1 setTimeout()

setTimeout() 在指定的时间后执行一段特定代码. 它需要如下参数:

  • 要运行的函数,或者函数引用。
  • 表示在执行代码之前等待的时间间隔(以毫秒为单位,所以1000等于1秒)的数字。如果指定值为0(或完全省略该值),函数将尽快运行(参阅下面的注释,了解为什么它“尽快”而不是“立即”运行)。
  • 更多的参数:在指定函数运行时,希望传递给函数的值.

在下面的示例中,浏览器将在执行匿名函数之前等待两秒钟,然后显示alert消息

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

我们指定的函数不必是匿名的。我们可以给函数一个名称,甚至可以在其他地方定义它,并将函数引用传递给 setTimeout() 。以下两个版本的代码片段相当于第一个版本:

// With a named function
let myGreeting = setTimeout(function sayHi() {
  alert('Hello, Mr. Universe!');
}, 2000)

// With a function defined separately
function sayHi() {
  alert('Hello Mr. Universe!');
}
let myGreeting = setTimeout(sayHi, 2000);

例如,如果我们有一个函数既需要从超时调用,也需要响应某个事件,那么这将非常有用。此外它也可以帮助保持代码整洁,特别是当超时回调超过几行代码时。

setTimeout() 返回一个标志符变量用来引用这个间隔,可以稍后用来取消这个超时任务,下面就会学到 Clearing timeouts 。

3.1.1 传递参数给setTimeout()

我们希望传递给setTimeout()中运行的函数的任何参数,都必须作为列表末尾的附加参数传递给它。

例如,我们可以重构之前的函数,这样无论传递给它的人的名字是什么,它都会向它打招呼:

function sayHi(who) {
  alert('Hello ' + who + '!');
}

人名可以通过第三个参数传进 setTimeout() :

let myGreeting = setTimeout(sayHi, 2000, 'Mr. Universe');

3.1.2 清除超时

最后,如果创建了 timeout,您可以通过调用clearTimeout(),将setTimeout()调用的标识符作为参数传递给它,从而在超时运行之前取消。要取消上面的超时,你需要这样做:

clearTimeout(myGreeting);

3.2 setInterval()

当我们需要在一段时间之后运行一次代码时,setTimeout()可以很好地工作。但是当我们需要反复运行代码时会发生什么,例如在动画的情况下?

这就是setInterval()的作用所在。这与setTimeout()的工作方式非常相似,只是作为第一个参数传递给它的函数,重复执行的时间不少于第二个参数给出的毫秒数,而不是一次执行。您还可以将正在执行的函数所需的任何参数作为 setInterval() 调用的后续参数传递。

让我们看一个例子。下面的函数创建一个新的Date()对象,使用toLocaleTimeString()从中提取一个时间字符串,然后在UI中显示它。然后,我们使用setInterval()每秒运行该函数一次,创建一个每秒更新一次的数字时钟的效果。

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

const createClock = setInterval(displayTime, 1000);

像setTimeout()一样, setInterval() 返回一个确定的值,稍后你可以用它来取消间隔任务。

setInterval()永远保持运行任务,除非我们做点什么——我们可能会想阻止这样的任务,否则当浏览器无法完成任何进一步的任务时我们可能得到错误, 或者动画处理已经完成了。我们可以用与停止超时相同的方法来实现这一点——通过将setInterval()调用返回的标识符传递给clearInterval()函数:

const myInterval = setInterval(myFunction, 2000);

clearInterval(myInterval);

3.3 关于 setTimeout() 和 setInterval() 需要注意的几点

当使用 setTimeout() 和 setInterval()的时候,有几点需要额外注意。 现在让我们回顾一下:

3.3.1 递归的timeouts

还有另一种方法可以使用setTimeout():我们可以递归调用它来重复运行相同的代码,而不是使用setInterval()。

下面的示例使用递归setTimeout()每100毫秒运行传递来的函数:

let i = 1;

setTimeout(function run() {
  console.log(i);
  i++;
  setTimeout(run, 100);
}, 100);

将上面的示例与下面的示例进行比较 ––这使用 setInterval() 来实现相同的效果:

let i = 1;

setInterval(function run() {
  console.log(i);
  i++
}, 100);

递归setTimeout()和setInterval()有何不同?
上述代码的两个版本之间的差异是微妙的。

  • 递归 setTimeout() 保证执行之间的延迟相同,例如在上述情况下为100ms。代码将运行,然后在它再次运行之前等待100ms,因此无论代码运行多长时间,间隔都是相同的。
  • 使用 setInterval() 的示例有些不同。 我们选择的间隔包括执行我们想要运行的代码所花费的时间。假设代码需要40毫秒才能运行 -然后间隔最终只有60毫秒。
  • 当递归使用 setTimeout() 时,每次迭代都可以在运行下一次迭代之前计算不同的延迟。
    换句话说,第二个参数的值可以指定在再次运行代码之前等待的不同时间(以毫秒为单位)。

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

3.3.2 立即超时

使用0用作setTimeout()的回调函数会立刻执行,但是在主线程代码运行之后执行。

举个例子,下面的代码(see it live) 输出一个包含警报的"Hello",然后在您点击第一个警报的OK之后立即弹出“world”。

setTimeout(function() {
  alert('World');
}, 0);

alert('Hello');

如果您希望设置一个代码块以便在所有主线程完成运行后立即运行,这将很有用。将其放在异步事件循环中,这样它将随后直接运行。

3.3.3 使用 clearTimeout() or clearInterval()清除

clearTimeout() 和clearInterval() 都使用相同的条目列表进行清除。有趣的是,这意味着你可以使用任一一种方法来清除 setTimeout() 和 setInterval()。

但为了保持一致性,你应该使用 clearTimeout() 来清除 setTimeout() 条目,使用 clearInterval() 来清除 setInterval() 条目。 这样有助于避免混乱。

3.4 requestAnimationFrame()

requestAnimationFrame() 是一个专门的循环函数,旨在浏览器中高效运行动画。它基本上是现代版本的setInterval() —— 它在浏览器重新加载显示内容之前执行指定的代码块,从而允许动画以适当的帧速率运行,不管其运行的环境如何。

它是针对setInterval() 遇到的问题创建的,比如 setInterval()并不是针对设备优化的帧率运行,有时会丢帧。还有即使该选项卡不是活动的选项卡或动画滚出页面等问题 。

该方法将重新加载页面之前要调用的回调函数作为参数。这是您将看到的常见表达:

function draw() {
   // Drawing code goes here
   requestAnimationFrame(draw);
}

draw();

requestAnimationFrame需要定义一个函数,在函数其中更新动画 (例如,移动精灵,更新乐谱,刷新数据等),然后调用它来开始这个过程。在函数的末尾,以 requestAnimationFrame() 传递的函数作为参数进行调用,这指示浏览器在下一次显示重新绘制时再次调用该函数。然后这个操作连续运行, 因为requestAnimationFrame() 是递归调用的。

3.4.1 你的动画跑得有多快?

动画的平滑度直接取决于动画的帧速率,并以每秒帧数(fps)为单位进行测量。这个数字越高,动画看起来就越平滑。

由于大多数屏幕的刷新率为60Hz,因此在使用web浏览器时,可以达到的最快帧速率是每秒60帧(FPS)。然而,更多的帧意味着更多的处理,这通常会导致卡顿和跳跃-也称为丢帧或跳帧。

如果您有一个刷新率为60Hz的显示器,并且希望达到60fps,则大约有16.7毫秒(1000/60)来执行动画代码来渲染每个帧。这提醒我们,我们需要注意每次通过动画循环时要运行的代码量。

requestAnimationFrame() 总是试图尽可能接近60帧/秒的值,当然有时这是不可能的如果你有一个非常复杂的动画,你是在一个缓慢的计算机上运行它,你的帧速率将更少。requestAnimationFrame() 会尽其所能利用现有资源提升帧速率。

3.4.2 requestAnimationFrame() 与 setInterval() 和 setTimeout()有什么不同?

让我们进一步讨论一下 requestAnimationFrame() 方法与前面介绍的其他方法的区别. 下面让我们看一下代码:

function draw() {
   // Drawing code goes here
   requestAnimationFrame(draw);
}

draw();

现在让我们看看如何使用setInterval():

function draw() {
   // Drawing code goes here
}

setInterval(draw, 17);

如前所述,我们没有为requestAnimationFrame();指定时间间隔;它只是在当前条件下尽可能快速平稳地运行它。如果动画由于某些原因而处于屏幕外浏览器也不会浪费时间运行它。

另一方面setInterval()需要指定间隔。我们通过公式1000毫秒/60Hz得出17的最终值,然后将其四舍五入。四舍五入是一个好主意,浏览器可能会尝试运行动画的速度超过60fps,它不会对动画的平滑度有任何影响。如前所述,60Hz是标准刷新率。

3.4.3 包括时间戳

传递给 requestAnimationFrame() 函数的实际回调也可以被赋予一个参数(一个时间戳值),表示自 requestAnimationFrame() 开始运行以来的时间。这是很有用的,因为它允许您在特定的时间以恒定的速度运行,而不管您的设备有多快或多慢。您将使用的一般模式如下所示:

let startTime = null;

function draw(timestamp) {
    if(!startTime) {
      startTime = timestamp;
    }
   currentTime = timestamp - startTime;
   // Do something based on current time
   requestAnimationFrame(draw);
}

draw();

3.4.4 浏览器支持

与setInterval()或setTimeout() 相比最近的浏览器支持requestAnimationFrame()

— requestAnimationFrame().在Internet Explorer 10及更高版本中可用。因此,除非您的代码需要支持旧版本的IE,否则没有什么理由不使用requestAnimationFrame() 。

3.4.5 撤销requestAnimationFrame()

requestAnimationFrame()可用与之对应的cancelAnimationFrame()方法“撤销”(不同于“set…”类方法的“清除”,此处更接近“撤销”之意)。

该方法以requestAnimationFrame()的返回值为参数,此处我们将该返回值存在变量 rAF 中:

cancelAnimationFrame(rAF);

3.4.6 一个简单的例子

学习上述理论已经足够了,下面让我们仔细研究并构建自己的requestAnimationFrame() 示例。我们将创建一个简单的“旋转器动画”(spinner animation),即当应用程序忙于连接到服务器时可能会显示的那种动画。

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>requestAnimationFrame spinner example</title>
    <style>
      html {
        background-color: white;
        height: 100%;
      }

      body {
        height: inherit;
        background-color: red;
        margin: 0;
        display: flex;
        justify-content: center;
        align-items: center;
      }

      div {
        display: inline-block;
        font-size: 10rem;
      }
    </style>
  </head>
  <body>

    <div></div>
    <script>

      // Store reference to the div element, create a rotate counter and null startTime
      // and create an uninitialized variable to store the requestAnimationFrame() call in
      const spinner = document.querySelector('div');
      let rotateCount = 0;
      let startTime = null;
      let rAF;

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

        rotateCount = (timestamp - startTime) / 3;

        // If rotateCount gets over 359, set it to 'remainder of dividing by 360'
        if(rotateCount > 359) {
          rotateCount %= 360;
        }

        // Set the rotation of the div to be equal to rotateCount degrees
        spinner.style.transform = 'rotate(' + rotateCount + 'deg)';

        // Call the next frame in the animation
        rAF = requestAnimationFrame(draw);
      }

      draw();

    </script>
  </body>
</html>

四、优雅的处理异步操作:Promises

4.1 回调函数的麻烦

要完全理解为什么promise是一件好事,它有助于回想旧式回调,并了解为什么它们有问题。

我们来谈谈订购披萨作为类比。为了使你的订单成功,你必须采取某些步骤,尝试按顺序执行或按顺序但在每个上一步完成之前没有意义:

  • 你选择你想要的配料。如果你是优柔寡断,这可能需要一段时间,如果你无法下定决心或者决定换咖喱,可能会失败。
  • 然后你下订单。返回比萨饼可能需要一段时间,如果餐厅没有烹饪所需的配料,可能会失败。
  • 然后你收集你的披萨吃。如果你忘记了自己的钱包,那么这可能会失败,所以无法支付比萨饼的费用!
    对于旧式callbacks,上述功能的伪代码表示可能如下所示:
chooseToppings(function(toppings) {
  placeOrder(toppings, function(order) {
    collectOrder(order, function(pizza) {
      eatPizza(pizza);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

这很麻烦且难以阅读(通常称为“回调地狱”),需要多次调用failureCallback()(每个嵌套函数一次),还有其他问题。

使用promise改良

Promises使得上面的情况更容易编写,解析和运行。如果我们使用异步promises代表上面的伪代码,我们最终会得到这样的结果:

chooseToppings()
.then(function(toppings) {
  return placeOrder(toppings);
})
.then(function(order) {
  return collectOrder(order);
})
.then(function(pizza) {
  eatPizza(pizza);
})
.catch(failureCallback);

这要好得多 - 更容易看到发生了什么,我们只需要一个.catch()块来处理所有错误,它不会阻塞主线程(所以我们可以在等待时继续玩视频游戏为了准备好收集披萨),并保证每个操作在运行之前等待先前的操作完成。我们能够以这种方式一个接一个地链接多个异步操作,因为每个.then()块返回一个新的promise,当.then()块运行完毕时它会解析。聪明,对吗?

使用箭头函数,你可以进一步简化代码:

chooseToppings()
.then(toppings =>
  placeOrder(toppings)
)
.then(order =>
  collectOrder(order)
)
.then(pizza =>
  eatPizza(pizza)
)
.catch(failureCallback);

甚至这样:

chooseToppings()
.then(toppings => placeOrder(toppings))
.then(order => collectOrder(order))
.then(pizza => eatPizza(pizza))
.catch(failureCallback);

4.2 解释promise的基本语法:一个真实的例子

Promises 很重要,因为大多数现代Web API都将它们用于执行潜在冗长任务的函数。要使用现代Web技术,你需要使用promises。现在我们将看一些你将在Web API中遇到的简单示例。

在第一个示例中,我们将使用fetch()方法从Web获取图像,blob()方法来转换获取响应的原始内容到Blob对象,然后在元素内显示该blob。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Simple fetch()</title>
  </head>
  <body>
    <script>
      // Call the fetch() method to fetch the image, and store it in a variable
      fetch('coffee.jpg')
      // Use a then() block to respond to the promise's successful completion
      // by taking the returned response and running blob() on it to transform it into a blob
      // blob() also returns a promise; when it successfully completes it returns
      // The blob object in the callback
      .then(response => {
        // The promise fetch() does NOT reject on HTTP errors,
        // so we need to check the boolean Response.ok and throw manually a new Error()
        // for the promise2 to be rejected (for example when a 404 occurs).
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        } else {
          return response.blob();
        }
      })
      .then(myBlob => {
        // Create an object URL that points to the blob object
        let objectURL = URL.createObjectURL(myBlob);
        // Create an <img> element to display the blob (it's an image)
        let image = document.createElement('img');
        // Set the src of the <img> to the object URL so the image displays it
        image.src = objectURL;
        // Append the <img> element to the DOM
        document.body.appendChild(image);
      })
      // If there is a problem, log a useful error message to the console
      .catch(e => {
        console.log('There has been a problem with your fetch operation: ' + e.message);
      });
    </script>
  </body>
</html>

4.3 运行代码以响应多个Promises的实现

上面的例子向我们展示了使用promises的一些真正的基础知识。现在让我们看一些更高级的功能。首先,链接进程一个接一个地发生都很好,但是如果你想在一大堆Promises全部完成之后运行一些代码呢?

你可以使用巧妙命名的Promise.all()静态方法完成此操作。这将一个promises数组作为输入参数,并返回一个新的Promise对象,只有当数组中的所有promise都满足时才会满足。它看起来像这样:

Promise.all([a, b, c]).then(values => {
  ...
});

如果它们都实现,那么一个包含所有这些结果的数组将作为.all()的参数传给其链接的.then()块的执行器函数。如果传递给Promise.all()的任何promise都拒绝,整个块将拒绝。

这非常有用。想象一下,我们正在获取信息以在内容上动态填充页面上的UI功能。在许多情况下,接收所有数据然后才显示完整内容,而不是显示部分信息是有意义的。

代码在4.4小结。

4.4 在promise fullfill/reject后运行一些最终代码

在promise完成后,你可能希望运行最后一段代码,无论它是否已实现(fullfilled)或被拒绝(rejected)。此前,你必须在.then()和.catch()回调中包含相同的代码,例如:

myPromise
.then(response => {
  doSomething(response);
  runFinalCode();
})
.catch(e => {
  returnError(e);
  runFinalCode();
});

在最近的现代浏览器中,.finally() 方法可用,它可以链接到常规promise链的末尾,允许你减少代码重复并更优雅地执行操作。上面的代码现在可以写成如下:

myPromise
.then(response => {
  doSomething(response);
})
.catch(e => {
  returnError(e);
})
.finally(() => {
  runFinalCode();
});

有关一个真实示例,这与我们在上面部分中看到的Promise.all()演示完全相同,除了在fetchAndDecode()函数中我们将finally()调用链接到链的末尾:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>fetch() promise.finally() example</title>
  </head>
  <body>
    <script>
      // Define function to fetch a file and return it in a usable form
      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(!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
          } else {
            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);
        })
        .finally(() => {
          console.log(`fetch attempt for "${url}" finished.`);
        });
      }

      // 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);
      });
    </script>
  </body>
</html>

五、让异步编程简单: async and await

5.1 async/await 基础

在代码中使用 async / await 有两个部分。

5.1.1 async 关键字

首先,我们使用 async 关键字,把它放在函数声明之前,使其成为 async function。异步函数是一个知道怎样使用 await 关键字调用异步代码的函数。

尝试在浏览器的JS控制台中键入以下行:

function hello() { return "Hello" };
hello();

该函数返回“Hello” —— 没什么特别的,对吧?

如果我们将其变成异步函数呢?请尝试以下方法:

async function hello() { return "Hello" };
hello();

现在调用该函数会返回一个 promise。这是异步函数的特征之一 —— 它保证函数的返回值为 promise。

你也可以创建一个异步函数表达式(参见 async function expression ),如下所示:

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

你可以使用箭头函数:

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

这些都基本上是一样的。

要实际使用promise完成时返回的值,我们可以使用.then()块,因为它返回的是 promise:

hello().then((value) => console.log(value))

甚至只是简写如

hello().then(console.log)

将 async 关键字加到函数申明中,可以告诉它们返回的是 promise,而不是直接返回值。此外,它避免了同步函数为支持使用 await 带来的任何潜在开销。在函数声明为 async 时,JavaScript引擎会添加必要的处理,以优化你的程序。

5.1.2 await 关键字

当 await 关键字与异步函数一起使用时,它的真正优势就变得明显了 —— 事实上, await 只在异步函数里面才起作用。它可以放在任何异步的,基于 promise 的函数之前。它会暂停代码在该行上,直到 promise 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了。

您可以在调用任何返回Promise的函数时使用 await,包括Web API函数。

这是一个简单的示例:

async function hello() {
  return greeting = await Promise.resolve("Hello");
};

hello().then(alert);

当然,上面的示例不是很有用,但它确实展示了语法。让我们继续,看一个真实示例。

5.2 使用 async/await 重写 promise 代码

5.2.1 示例

让我们回顾一下我们在上文中简单的 fetch 示例:

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

到现在为止,你应该对 promises 及其工作方式有一个较好的理解。让我们将其转换为使用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);
}

myFetch()
.catch(e => {
  console.log('There has been a problem with your fetch operation: ' + e.message);
});

它使代码简单多了,更容易理解 —— 去除了到处都是 .then() 代码块!

由于 async 关键字将函数转换为 promise,您可以重构以上代码 —— 使用 promise 和 await 的混合方式,将函数的后半部分抽取到新代码块中。这样做可以更灵活:

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

5.2.2 它到底是如何工作的?

您会注意到我们已经将代码封装在函数中,并且我们在 function 关键字之前包含了 async 关键字。这是必要的 –– 您必须创建一个异步函数来定义一个代码块,在其中运行异步代码; await 只能在异步函数内部工作。

在myFetch()函数定义中,您可以看到代码与先前的 promise 版本非常相似,但存在一些差异。不需要附加 .then() 代码块到每个promise-based方法的结尾,你只需要在方法调用前添加 await 关键字,然后把结果赋给变量。await 关键字使JavaScript运行时暂停于此行,允许其他代码在此期间执行,直到异步函数调用返回其结果。一旦完成,您的代码将继续从下一行开始执行。例如:

let response = await fetch('coffee.jpg');

解析器会在此行上暂停,直到当服务器返回的响应变得可用时。此时 fetch() 返回的 promise 将会完成(fullfilled),返回的 response 会被赋值给 response 变量。一旦服务器返回的响应可用,解析器就会移动到下一行,从而创建一个Blob。Blob这行也调用基于异步promise的方法,因此我们也在此处使用await。当操作结果返回时,我们将它从myFetch()函数中返回。

这意味着当我们调用myFetch()函数时,它会返回一个promise,因此我们可以将.then()链接到它的末尾,在其中我们处理显示在屏幕上的blob。

你可能已经觉得“这真的很酷!”,你是对的 —— 用更少的.then()块来封装代码,同时它看起来很像同步代码,所以它非常直观。

5.2.3 添加错误处理

如果你想添加错误处理,你有几个选择。
您可以将同步的 try…catch 结构和 async/await 一起使用 。此示例扩展了我们上面展示的第一个版本代码:

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

myFetch();

catch() {} 代码块会接收一个错误对象 e ; 我们现在可以将其记录到控制台,它将向我们提供详细的错误消息,显示错误被抛出的代码中的位置。

如果你想使用我们上面展示的第二个(重构)代码版本,你最好继续混合方式并将 .catch() 块链接到 .then() 调用的末尾,就像这样:

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

这是因为 .catch() 块将捕获来自异步函数调用和promise链中的错误。如果您在此处使用了try/catch 代码块,则在调用 myFetch() 函数时,您仍可能会收到未处理的错误。

5.3 等待Promise.all()

async / await 建立在 promises 之上,因此它与promises提供的所有功能兼容。这包括Promise.all() –– 你完全可以通过调用 await Promise.all() 将所有结果返回到变量中,就像同步代码一样。让我们再次回到上一篇中看到的例子。在单独的选项卡中打开它,以便您可以与下面显示的新版本进行比较和对比。

将其转换为 async / await,现在看起来像这样:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>promise.all() example with async/await</title>
  </head>
  <body>
    <script>
      // Define function to fetch a file and return it in a usable form
      async function fetchAndDecode(url, type) {
        // 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 (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        } else {
          if(type === 'blob') {
            content = await response.blob();
          } else if(type === 'text') {
            content = await response.text();
          }

          return content;
        }
      }

      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)
      );
    </script>
  </body>
</html>

可以看到 fetchAndDecode() 函数只进行了一丁点的修改就转换成了异步函数。请看Promise.all() 行:

let values = await Promise.all([coffee, tea, description]);

在这里,通过使用await,我们能够在三个promise的结果都可用的时候,放入values数组中。这看起来非常像同步代码。我们需要将所有代码封装在一个新的异步函数displayContent() 中,尽管没有减少很多代码,但能够将大部分代码从 .then() 代码块移出,使代码得到了简化,更易读。

为了错误处理,我们在 displayContent() 调用中包含了一个 .catch() 代码块;这将处理两个函数中出现的错误。

5.4 async/awai

了解Async/await是非常有用的,但还有一些缺点需要考虑。

Async/await 让你的代码看起来是同步的,在某种程度上,也使得它的行为更加地同步。 await 关键字会阻塞其后的代码,直到promise完成,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但您自己的代码被阻塞。

这意味着您的代码可能会因为大量await的promises相继发生而变慢。每个await都会等待前一个完成,而你实际想要的是所有的这些promises同时开始处理(就像我们没有使用async/await时那样)。

有一种模式可以缓解这个问题——通过将 Promise 对象存储在变量中来同时开始它们,然后等待它们全部执行完毕。让我们看一些证明这个概念的例子。

慢的例子

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demonstration of slow async/await</title>
  </head>
  <body>
    <script>
      // Define custom promise function

      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);
      })
    </script>
  </body>
</html>

快的例子

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Demonstration of fast async/await</title>
  </head>
  <body>
    <script>
      // Define custom promise function

      function timeoutPromise(interval) {
        return new Promise((resolve, reject) => {
          setTimeout(function(){
            resolve("done");
          }, interval);
        });
      };

      async function timeTest() {
        const timeoutPromise1 = timeoutPromise(3000);
        const timeoutPromise2 = timeoutPromise(3000);
        const timeoutPromise3 = timeoutPromise(3000);

        await timeoutPromise1;
        await timeoutPromise2;
        await timeoutPromise3;
      }

      let startTime = Date.now();
      timeTest().then(() => {
        let finishTime = Date.now();
        let timeTaken = finishTime - startTime;
        alert("Time taken in milliseconds: " + timeTaken);
      })
    </script>
  </body>
</html>

在这里,我们将三个Promise对象存储在变量中,这样可以同时启动它们关联的进程。

接下来,我们等待他们的结果 - 因为promise都在基本上同时开始处理,promise将同时完成;当您运行第二个示例时,您将看到弹出框报告总运行时间仅超过3秒!

您必须仔细测试您的代码,并在性能开始受损时牢记这一点。

5.5 Async/await 的类方法

最后值得一提的是,我们可以在类/对象方法前面添加async,以使它们返回promises,并await它们内部的promises。查看 ES class code we saw in our object-oriented JavaScript article,然后使用异步方法查看我们的修改版本:

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

六、选择正确的方法

结束之前,回顾一下已经讨论的编程技术和特性:什么时候用哪个。有推荐,也有常见的陷阱提醒。

6.1 异步回调

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

Single delayed operationRepeating operation重复操作Multiple sequential operations 多顺序调用Multiple simultaneous operations并行调用
NoYes (递归回调)Yes (嵌套回调)No

6.1.1 缺陷

  • 嵌套回调可能很麻烦且难以阅读(即“回调地狱”)

  • 每层嵌套都需要调用一次失败回调,而使用promises,您只需使用一个.catch()代码块来处理整个链的错误。

  • 异步回调不是很优雅。

  • Promise回调总是按照它们放在事件队列中的严格顺序调用;异步回调不是。

  • 当传入到一个第三方库时,异步回调对函数如何执行失去完全控制。

6.1.2 浏览器兼容性

非常好的一般支持,尽管API中回调的确切支持取决于特定的API。

6.2 setTimeout()

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

Single delayed operationRepeating operation重复操作Multiple sequential operations 多顺序调用Multiple simultaneous operations并行调用
YesYes (recursive timeouts)Yes (nested timeouts)No

6.7.1 代码示例

这里浏览器将在执行匿名函数之前等待两秒钟,然后将显示警报消息

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

6.7.2 缺陷

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

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

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

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

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

6.3 setInterval()

setInterval()是一种允许您在每次执行之间以设定的时间间隔重复运行函数的方法。不如requestAnimationFrame()有效,但允许您选择运行速率/帧速率。

Single delayed operationRepeating operation重复操作Multiple sequential operations 多顺序调用Multiple simultaneous operations并行调用
NoYesNo(除非同一个)No

6.7.1 代码示例

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

const createClock = setInterval(displayTime, 1000);

6.7.2 缺陷

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

6.4 requestAnimationFrame()

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

Single delayed operationRepeating operation重复操作Multiple sequential operations 多顺序调用Multiple simultaneous operations并行调用
NoYesNo(除非同一个)No

6.7.1 代码示例

一个简单的动画旋转器

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

6.7.2 缺陷

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

6.5 Promises

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

Single delayed operationRepeating operation重复操作Multiple sequential operations 多顺序调用Multiple simultaneous operations并行调用
NoNoYesYes Promise.all()

6.5.1 代码示例

6.5.2 缺陷

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

6.6 Promise.all()

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

Single delayed operationRepeating operation重复操作Multiple sequential operations 多顺序调用Multiple simultaneous operations并行调用
NoNo NoYes

6.6.1 代码示例

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

6.6.2 缺陷

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

6.7 Async/await

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

Single delayed operationRepeating operation重复操作Multiple sequential operations 多顺序调用Multiple simultaneous operations并行调用
NoNoYesYes (Promise.all())

6.7.1 代码示例

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

6.7.2 缺陷

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值