在理解什么是异步之前我们应该先理解什么是同步

同步

同步的定义

任何时候只能做一件事情, 只有一个主线程,其他的事情都阻塞了,直到前面的操作完成
举一个例子,比如你去医院挂号,不管要等多长时间,你都会在窗口等着,拿到号才会离开,同步也是这样,一定要等任务执行完了,得到结果,才执行下一个任务。

例子

来看一个简单的例子:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>Simple synchronous JavaScript example</title>
  6. </head>
  7. <body>
  8. <button>Click me</button>
  9. <script>
  10. const btn = document.querySelector('button');
  11. btn.addEventListener('click', () => {
  12. alert('You clicked me!');
  13. let pElem = document.createElement('p');
  14. pElem.textContent = 'This is a newly-added paragraph.';
  15. document.body.appendChild(pElem);
  16. });
  17. </script>
  18. </body>
  19. </html>

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

  1. 先取得一个在DOM里面的 引用。
  2. 点击按钮的时候,添加一个 click 事件监听器:
    1. alert() 消息出现。
    2. 一旦alert 结束,创建一个 元素。
    3. 给它的文本内容赋值。
    4. 最后,把这个段落放进网页。
      每一个操作在执行的时候,其他任何事情都没有发生 — 网页的渲染暂停. 因为前篇文章提到过 JavaScript is single threaded. 任何时候只能做一件事情, 只有一个主线程,其他的事情都阻塞了,直到前面的操作完成。
      所以上面的例子,点击了按钮以后,段落不会创建,直到在alert消息框中点击ok,段落才会出现。

异步

上面的例子顺序执行,同一时刻只会发生一件事,如果一个函数依赖于另一个函数的结果,它只能等待那个函数结束才能继续执行,这对我们的体验很不好。

Mac 用户有时会经历过这种旋转的彩虹光标,操作系统通过这个光标告诉用户:“现在运行的程序正在等待其他的某一件事情完成,才能继续运行,都这么长的时间了,你一定在担心到底发生了什么事情”。
image.jpeg
这对用户来说是很糟糕的体验,没有充分利用计算机的计算能力——尤其是在计算机普遍都有多核CPU的时代,坐在那里等待毫无意义,你完全可以在另一个处理器内核上干其他的工作,计算机完成耗时任务的时候通知你。这样你可以同时完成其他工作,这就是异步编程的出发点。

阻塞

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

就前面提到的种种原因(比如,和阻塞相关)很多网页API特性使用异步代码,
在JavaScript代码中,你经常会遇到两种异步编程风格:老派callbacks(回调),新派promise。下面就来分别介绍。

callbacks(回调)

异步callbacks 其实就是函数,只不过是作为参数传递给那些在后台执行的其他函数. 当那些后台运行的代码结束,就调用callbacks函数,通知你工作已经完成,或者其他有趣的事情发生了。

举一个例子,比如你去了一家网红餐厅,只能排号就餐,你取了号之后可以不在那里傻傻的等着,可以去逛街、买奶茶……那怎么知道什么时候能去吃饭呢? 可以用电话接收就餐通知,通知到你的时候就可以去吃饭了。这个过程就叫做回调。

  1. function f1(x) {
  2. console.log(x)
  3. }
  4. function f2(fn) {
  5. fn(' 你好')
  6. }
  7. f2(f1)

上面的代码里,我没有调用f1,但是我把f1传给了f2,f2调用了f1,那么f1是我写给f2的调用函数,f1是回调。
fn(‘你好’)中的fn就是f1,’你好’会被赋值给参数x,所以x就是’你好’,x可以被改成任何其他名字,它只是表示第一个参数而已。

异步和回调的关系

异步任务需要在得到结果是通知JS拿结果,那该怎么通知呢?

  • 可以让JS留一个函数地址(电话号码)给浏览器
  • 异步任务完成时浏览器调用该函数地址(拨打电话)
  • 同时把结果作为参数传给该函数(告诉你可以去吃饭了)
  • 这个函数是我写给浏览器调用的,所以是回调函数。

注意,异步任务虽然需要用到回调函数来通知结果,但是回调函数不一定只用在异步任务里,也可以用在同步任务里,array.fotEach(n=>console.log(n))就是同步回调。

  1. const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus'];
  2. gods.forEach(function(eachName, index) {
  3. console.log(index + '. ' + eachName);
  4. });

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

判断同步异步

如果一个函数的返回值处于setTimeout、AJAX(即XMLHttpRequest)、AddEventListener这三个东西内部,那么这个函数就是异步函数。
虽然AJAX可以设置为同步的,但是这样做会使请求期间页面卡住。
我们先来写一个异步函数

  1. function 摇骰子() {
  2. setTimeout(() => { // 箭头函数
  3. return parseInt(Math.random() * 6) + 1
  4. }, 1000)
  5. // return undefined
  6. }

摇骰子()没有写return,返回值是undefined;箭头函数里有return, 返回真正的结果;因为它真正的结果在setTimeout里,所以它是一个异步函数。

  1. const n = 摇骰子()
  2. console.log(n) // undefined

上面这种写法并不能异步结果,因为它的return是undefined。

这时就可以用回调拿到异步结果了,写一个函数然后把函数地址给他。

  1. function f1(x){ console.log(x) }
  2. 摇骰子(f1)

然后摇骰子函数得到结果后把结果作为参数传给f1。

  1. function 摇骰子(fn) {
  2. setTimeout(() => { // 箭头函数
  3. fn(parseInt(Math.random() * 6) + 1)
  4. }, 1000)
  5. }

简化代码

  1. function f1(x){ console.log(x) }
  2. 摇骰子(f1)

由于f1声明之后只用了一次,我们可以把它简化成箭头函数。

  1. 摇骰子 (x => { console.log(x) })

那么既然x也只是起到中间过渡的作用,那我们把x也省略掉。

  1. 摇骰子 (console.log)

注意 如果参数个数不一致就不能这样简化

  1. //简化版
  2. const array = ['1', '2', '3'].map(parseInt)
  3. console.log(array)
  4. //[1, NaN, NaN]
  5. //简化版的完整写法
  6. const array1 = ['1', '2', '3'].map((item, i, arr) => {
  7. return parseInt(item, i, arr)
  8. //parseInt('1', 0, arr) => 1
  9. //parseInt('2', 1, arr) => NaN
  10. //parseInt('3', 2, arr) => NaN
  11. })
  12. console.log(array1)
  13. //[1, NaN, NaN]
  14. //正确写法
  15. const arr = ['1', '2', '3'].map((item, i, arr) => {
  16. return parseInt(item)
  17. })
  18. console.log(arr)
  19. //[1, 2, 3]

小结

  • 异步任务不能拿到结果
  • 于是我们传一个回调给异步任务
  • 异步任务完成时调用回调
  • 调用的时候把结果作为参数

Promises

Promise的MDN文档
Promises/A+规范
Promise 是一个拥有 then 方法的对象或函数,它代表了一个异步操作的最终完成或者失败。因为大多数人仅仅是使用已创建的 Promise 实例对象,所以本教程将首先说明怎样使用 Promise,再说明如何创建 Promise。

本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。

Then 方法

一个promise必须提供一个then方法以访问其当前值、终值和据因。
promisethen 方法接受两个参数:

  1. promise.then(onFulfilled, onRejected)

then 方法必须返回一个 promise 对象

  1. promise2 = promise1.then(onFulfilled, onRejected);
  • 如果 onFulfilled 或者 onRejected 返回一个值 x ,则运行下面的 Promise 解决过程[[Resolve]](promise2, x)
  • 如果 onFulfilled 或者 onRejected 抛出一个异常 e ,则 promise2 必须拒绝执行,并返回拒因 e
  • 如果 onFulfilled 不是函数且 promise1 成功执行, promise2 必须成功执行并返回相同的值
  • 如果 onRejected 不是函数且 promise1 拒绝执行, promise2 必须拒绝执行并返回相同的
    拒因 ```javascript var p1 = new Promise((resolve, reject) => { resolve(‘成功!’); // or // reject(new Error(“出错了!”)); });

p1.then(value => { console.log(value); // 成功! }, reason => { console.error(reason); // 出错了! });

  1. 理解上面的“返回”部分非常重要,即:不论 promise1 reject 还是被 resolve promise2 都会被 resolve,只有出现异常时才会被 rejected
  2. <a name="0b82322d"></a>
  3. #### 创建Promise
  4. Promise 对象是由关键字 new 及其构造函数来创建的。该构造函数会把一个叫做“处理器函数”(executor function)的函数作为它的参数。这个“处理器函数”接受两个函数——resolve reject ——作为其参数。当异步任务顺利完成且返回结果值时,会调用 resolve 函数;而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用reject 函数。

const myFirstPromise = new Promise((resolve, reject) => { // ?做一些异步操作,最终会调用下面两者之一: // // resolve(someValue); // fulfilled // ?或 // reject(“failure reason”); // rejected});

  1. 想要某个函数拥有promise功能,只需让其返回一个promise即可。

function myAsyncFunction(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open(“GET”, url); xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText); xhr.send(); }); };

  1. <a name="1a63ac23"></a>
  2. #### 示例
  3. 以AJAX的封装为例解释Promise的用法。

ajax = (method, url, options) => { const { success, fail } = options //析构赋值 const request = new XMLHttpRequest() request.open(method, url) request.onreadystatechange = () => { if (request.readyState === 4) { // 成功就调用success,失败就调用fail if (request.status < 400) { success.call(null, request.response) // 这一句变了 } else if (request.status >= 400) { fail.call(null, request, request.status) // 这一句变了 } } } request.send() }

//调用 ajax(‘get’, ‘/xxx’, { success(response) {}, fail: (request, status) => {} }) // 左边是function缩写,右边是箭头函数

  1. 先改变一下调用

ajax(‘get’, ‘/xxx’, { success(response) {}, fail: (request, status) => {} }) // 上面用到了两个回调还使用了success和fail

// 改成Promise写法 ajax(‘get’, ‘/xxx’) .then((response) => {}, (request, status) = {}) // 虽然也是回调,但是不需要记success和fail了 // then的第一个参数就是success // then的第二个参数就是fail

  1. ajax() 返回了一个含有.then() 方法的对象, 如果要得到这个含有.then() 的对象就要改造ajax的源码了

ajax = (method, url, options) => { return new Promise((resolve, reject) => { // 这一句变了 const { success, fail } = options const request = new XMLHttpRequest() request.open(method, url) request.onreadystatechange = () => { if (request.readyState === 4) { // 成功就调用resolve, 失败就调用reject if (request.status < 400) { resolve.call(null, request.response) // 这一句变了 } else if (request.status >= 400) { reject.call(null, request) // 这一句变了 } } } request.send() }) // 这一句变了 }

  1. <a name="Uzjul"></a>
  2. #### 如何使用 Promise.all

Promise.all(iterable);

  1. 这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常被用于处理多个promise对象的状态集合。

const promise1 = Promise.resolve(3); const promise2 = 42; const promise3 = new Promise((resolve, reject) => { setTimeout(resolve, 100, ‘foo’); });

Promise.all([promise1, promise2, promise3]).then((values) => { console.log(values); }); // 输出: Array [3, 42, “foo”]

  1. <a name="zWLZn"></a>
  2. #### 如何使用 Promise.race

Promise.race(iterable);

  1. Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

const promise1 = new Promise((resolve, reject) => { setTimeout(resolve, 500, ‘one’); });

const promise2 = new Promise((resolve, reject) => { setTimeout(resolve, 100, ‘two’); });

Promise.race([promise1, promise2]).then((value) => { console.log(value); // 都解决了,但是promise2更快 }); // 输出: “two” ```

Promises 对比 callbacks

promises与旧式callbacks有一些相似之处。它们本质上是一个返回的对象,您可以将回调函数附加到该对象上,而不必将回调作为参数传递给另一个函数。
然而,Promise是专门为异步操作而设计的,与旧式回调相比具有许多优点:

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

小结

  • 第一步:
    • return new Promise((resolve,reject)=>{…})
    • 成功调用resolve(result)
    • 失败调用reject(error)
    • resolve和reject会再去调用成功和失败函数
  • 第二步:
    • 使用.then(success,fail)传入成功和失败函数