一学就会Promise

同步代码与异步代码

在学习 Promise 之前,我们需要理解同步代码和异步代码

  • 同步代码:逐行执行,需原地等待结果后,才继续向下执行

实际上浏览器是按照我们书写代码的顺序一行一行地执行,浏览器会等待代码的解析和工作,在上一行代码完成后才会执行下一行。这样做是很有必要的,因为每一行新的代码都是建立在前面代码的基础之上,这也使得它成为一个同步程序

同步执行模式简单直观,易于理解和调试,但是当涉及耗时较长的操作时,如果都采用同步方式,可能会导致程序响应变慢甚至出现卡顿现象,于是便产生了异步的概念

  • 异步代码:调用后耗时,不阻塞代码继续执行(不必原地等待),在将来完成后触发一个回调函数

异步编程技术使你的程序可以在执行一个可能长期运行的任务的同时继续对其他任务做出反应,而不必等待该耗时任务的完成。与此同时,你的程序也将在任务完成后显示结果

例如我们使用定时器延迟打印数据,对按钮点击事件进行事件监听,定时器内代码执行不会影响主线程(即执行JS代码的线程)内的后续代码的执行:

js 复制代码
const result = 0 + 1
console.log(result)

setTimeout(() => {
  console.log(2)
}, 2000)

document.querySelector('.btn').addEventListener('click', () => {
  console.log(3)
})
console.log(4)
// 运行页面 2s 内点击: 1432
// 运行页面 2s 后点击: 1423

对于同步和异步代码,红宝书上有一个很有意思的现象:

js 复制代码
//在使用 var 的时候,最常见的问题就是对迭代变量的奇特声明和修改:
for (var i = 0; i < 5; ++i) { 
 setTimeout(() => console.log(i), 0) // 5、5、5、5、5 
}
//之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5。
//在之后执行超时逻辑时,所有的 i 都是同一个变量,因而输出的都是同一个最终值。

//在使用 let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。
for (let i = 0; i < 5; ++i) { 
 setTimeout(() => console.log(i), 0) // 0、1、2、3、4
}
//每个 setTimeout 引用的都是不同的变量实例,所以 console.log 输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。

产生这种现象的原因主要是 var 定义的变量会有变量提升问题,且变量i没有块级作用域,于是每次输出的i其实都是同一个变量,加上setTimeout()是异步执行的,于是每次setTimeout()输出时,输出的是循环后最终的i

使用let声明的变量没有出现这种问题的主要原因是let变量有块级作用域,每次传入 setTimeout() 回调函数中的i其实是不同的变量i ,因此最后能输出正确的结果

Promise

Promise定义

ECMAScript 6 增加了对 Promises/A+ 规范的完善支持,即 Promise(期约) 类型。一经推出,Promise 就大受欢迎,成为了主导性的异步编程机制 。所有现代浏览器都支持 ES6 期约,很多其他浏览器 API(如fetch()和 Battery Status API)也以期约为基础。此外 Promise 支持链式调用,可以解决回调函数地狱问题

Promise 对象是一个构造函数,用来生成 Promise 实例,可以包裹一个异步操作

javascript 复制代码
const promise = new Promise(function(resolve, reject) {异步操作});

# Promise 构造函数:Promise(excutor){}
(1) excutor 函数:执行器 (resolve,reject) => {}
(2) resolve 函数:内部定义成功时我们调用的函数 value => {}
(3) reject 函数: 内部定义失败时我们调用的函数 reason => {}
// excutor 会在 Promise 内部立即同步调用,异步操作会在执行器中执行

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject,称为函数类型的数据 ,它们是两个函数,由 JavaScript 引擎提供,不用自己部署。当然这里的 resolve 和 reject 是可以随意命名的(默认这么命名),要把这里的 resolve 和 reject 与Promise静态方法中的 resolve 和 reject 分开,它们并不相同!

  • 在异步任务成功时,调用resolve函数,将 Promise 对象的状态从未完成(pending)变为成功(resolved),并将异步操作的结果作为参数传递出去

  • 在异步任务失败时,调用reject函数,将 Promise 对象的状态从未完成(pending)变为失败(rejected),并将异步操作报出的错误作为参数传递出去

  • Promise 实例生成以后,可以使用 then 方法分别指定 resolve 状态和 rejected 状态的回调函数,当然这里的错误接收可以使用 catch 方法 (本质上是个语法糖)

    js 复制代码
    new Promise(function (resolve, reject) {
        //成功调用 resolve()
        //失败调用 reject()
    }).then(res => {
       //成功执行代码
    }).catch(res => {
       //失败执行代码
    })
    ## Promise 的状态
    * 实例对象中的一个属性 【PromiseState】
    ## Promise 对象的值
    * 实例对象中的另一个属性 【PromiseResult】
    * 保存着对象【成功/失败】的结果,只能由 resolve 和 reject 修改

    总的来说:当 resolvereject 修改 promise 对象状态之后,通过检测 promise 对象的状态,决定执行 then 还是 catch 回调方法。在这个过程中:resolvereject 起到的作用是修改对象状态、通知回调函数以及传递回调函数可能需要的参数。 这样做的好处就是:把逻辑判断和回调函数分开处理。通俗来讲,这俩函数就是个干苦力的中间人,任劳任怨,连名字都可以被随意更改!

Promise 只能用resolve和reject以及throw抛出数据信息,return不能抛出信息。不过在使用 async 声明的异步函数可以使用 return(后文会有详细说明)

javascript 复制代码
const p = new Promise(function (resolve, reject) {
  return '666'
}).then(res => {
  console.log(res)
}).catch(err => {
  console.log(err)
})
setTimeout(() => {
  console.log(p)
}, 1000)

期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介。这样看似乎不太好理解,下面引入红宝书中的例子:

js 复制代码
try { 
 throw new Error('foo'); 
} catch(e) { 
 console.log(e); // Error: foo
} 
try { 
 Promise.reject(new Error('bar')); 
} catch(e) { 
 console.log(e); 
} 
// Uncaught (in promise) Error: bar

上述的例子中,拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队列来处理的。因此,主线程的try/catch块并不能捕获该错误。代码一旦开始以异步模式执行,则唯一与之交互的方式就是使用异步结构,更具体地说,就是promise的方法

Promise状态

一个Promise对象,必然处于以下几种状态之一:

  • 待定(pending):待定,初始状态,既没有被兑现也没有被拒绝
  • 已兑现(fulfilled):已兑现,表示操作成功完成
  • 已拒绝(rejected):已失败,表示操作失败

对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态

Promise对象状态一旦改变(从pending变为fulfilled和从pending变为rejected),就不能再变,这个过程是不可逆的

Promise整体流程:

Promise实例方法

期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁。这些方法可以访问异步操作返回的数据,处理期约成功或失败的结果,连续对期约求值,或者添加只有期约进入终止状态时才会执行的代码

then()

then()是实例状态发生改变时的回调函数,第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数

then()方法返回的是一个新的Promise实例(promise能链式调用的原因)

  • 语法:

    js 复制代码
    then(onResolved)
    then(onResolved, onRejected)
  • 替代 catch 写法:

    js 复制代码
    new Promise(function(resolve, reject) {
      reject('出错啦')
    }).then(null,(res) =>{
      console.log(res)
    })

两个处理程序参数都是可选的。而且传给 then() 的任何非函数类型的参数都会被默认忽略。如果想只提供 onRejected 参数,那就要在 onResolved 参数的位置上传入 undefined / null,这样有助于避免在内存中创建多余的对象

  • onResolved处理函数的返回值 :当一个 Promise 完成(fulfilled)或者失败(rejected)时,返回函数将被异步调用。具体的返回值依据以下规则返回。如果 then 中的回调函数:

    本节使用setTimeout输出的是执行完后的 Promise 对象的状态。直接打印 同/异步 代码的执行顺序问题,会出现打印 pending 状态的 Promise 对象的情况

    • 返回了一个值 ,那么 then 返回的 Promise 将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值

      javascript 复制代码
      const p = new Promise((resolve, reject) => {
        // 模拟异步操作
        setTimeout(() => {
          const data = { message: "Hello, World!" };
          resolve(data);
        }, 1000);
      }).then((result) => {
        console.log(result); //Hello, World!
        // 返回一个新的值
        return result.message.toUpperCase();
      })
      
      setTimeout(() => {
        console.log(p) //Promise {<fulfilled>: 'HELLO, WORLD!'}
      }, 1000);
    • 没有返回任何值 ,那么 then 返回的 Promise 将会成为接受状态 ,并且该接受状态的回调函数的参数值为 undefined

      javascript 复制代码
      const p = new Promise((resolve, reject) => {
        // 模拟异步操作
        setTimeout(() => {
          const data = { message: "Hello, World!" };
          resolve(data);
        }, 1000);
      }).then((result) => {
        console.log(result); //Hello, World!
        // 不返回值
      })
      
      setTimeout(() => {
        console.log(p) //Promise {<fulfilled>: undefined}
      }, 1000);
    • 抛出一个错误 ,那么 then 返回的 Promise 将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值

      javascript 复制代码
      const p = new Promise((resolve, reject) => {
        // 模拟异步操作
        setTimeout(() => {
          const data = { message: "Hello, World!" };
          resolve(data);
        }, 1000);
      }).then((result) => {
        throw new Error("Error in then");
      });
      
      p.then((result) => {
        console.log(result); // { message: "Hello, World!" }
      }).catch((err) => {
        console.log(err); //Error: Error in then at xxx
      });
      
      setTimeout(() => {
        console.log(p) //Promise {<rejected>: Error: Error in then at file xxx
      }, 1000)
    • 返回一个已经是接受状态的 Promise ,那么 then 返回的 Promise 也会成为接受状态 ,并且将那个 Promise 的接受状态的回调函数的参数值作为该被返回的 Promise 的接受状态回调函数的参数值

      js 复制代码
      const p = new Promise(function (resolve, reject) {
        resolve('666')
      }).then(res => {
        console.log(res)
        return new Promise(function (resolve, reject) {
          resolve('777')
        }) //返回成功状态的promise对象
      })
      setTimeout(() => {
        console.log(p) //Promise {<fulfilled>: '777'}
      }, 0);
    • 返回一个已经是拒绝状态的 Promise ,那么 then 返回的 Promise 也会成为拒绝状态 ,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的 Promise 的拒绝状态回调函数的参数值

      javascript 复制代码
      const p = new Promise(function (resolve, reject) {
        resolve('成功')
      }).then(res => {
        console.log(res)
        return new Promise(function (resolve, reject) {
          reject('拒绝')
        }) //返回成功状态的promise对象
      })
      p.then(res => {
        console.log(res)
      }).catch(err => {
        console.log(err) //拒绝
      })
      setTimeout(() => {
        console.log(p) //Promise {<rejected>: '拒绝'}
      })
    • 返回一个未定状态(pending)的 Promise ,那么 then 返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的

      javascript 复制代码
      const p = new Promise(function (resolve, reject) {
        resolve('成功')
      }).then(res => {
        return new Promise(function (resolve, reject) {})
      })
      console.log(p) //Promise {<pending>}

总的来说:

  • then中的onResolved函数返回的是值(或者不返回任何东西),则then函数返回的是 fulfilled 状态的Promise对象
  • then中的onResolved函数返回的是Promise对象 ,则then函数返回的Promise对象状态与onResolved函数返回的Promise对象状态保持一致

catch()

catch()方法是.then(null, onRejected).then(undefined, onRejected)的别名,用于指定发生错误时的回调函数,。这个方法只接收一个参数:onRejected 处理程序。事实上,这个方法就是一个语法糖

一般来说,使用catch()方法代替then()第二个参数,与then()方法一样

  • 语法:

    js 复制代码
    catch(onRejected)

catch()方法返回一个新的Promise实例,无论当前的 promise 状态如何,这个新的 promise 在返回时总是处于待定状态。如果 onRejected 方法抛出了一个错误或者返回了一个被拒绝的 promise,那么这个新的 promise 也会被拒绝(rejected),否则它最终会被兑现(fulfilled)

实际上就是两点:

  • catch中的onRejected函数正常返回时,新Promise实例的状态为fullfilled
  • catch中的onRejected函数报错时,新Promise实例的状态为rejected
javascript 复制代码
// catch 中的 onRejected 函数正常返回时, 新Promise实例的状态为fullfilled
const p = new Promise((resolve, reject) => {
  reject('Error!');
}).catch((error) => {
  console.log('onRejected function:', error); //onRejected function: Error!
})
setTimeout(() => {
  console.log(p) //Promise {<fulfilled>: undefined}
}, 0);

// catch 中的 onRejected 函数报错时, 新Promise实例的状态为rejected
const p1 = new Promise((resolve, reject) => {
  reject('Error!');
}).catch((error) => {
  console.log('onRejected function:', error); //onRejected function: Error!
  throw new Error('Thrown Error');
})
setTimeout(() => {
  console.log(p1) //Promise {<rejected>: Error: Thrown Error
}, 0);

抛出错误时的陷阱

  • 大多数情况下,抛出错误会调用 catch() 方法:
javascript 复制代码
const p1 = new Promise((resolve, reject) => {
  throw new Error("哦吼!");
});

p1.catch((e) => {
  console.error(e); // "b 哦吼!"
});
  • 在异步函数内部抛出的错误会像未捕获的错误一样:
javascript 复制代码
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    throw new Error("未捕获的异常!");
  }, 1000);
});

p2.catch((e) => {
  console.error(e); // 永远不会被调用
});

未被捕获的原因实际上就是代码执行顺序的问题,p2.catch先于throw new Error("未捕获的异常!");执行了,要想解决该问题,直接使用链式调用catch

  • 在调用 resolve 之后抛出的错误会被忽略:
javascript 复制代码
const p3 = new Promise((resolve, reject) => {
  resolve();
  throw new Error("Silenced Exception!");
});

p3.catch((e) => {
  console.error(e); // 这里永远不会执行
});

finally()

finally()方法用于给期约添加 onFinally 处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。这个方法可以避免onResolvedonRejected处理程序中出现冗余代码。但 onFinally 处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用于添加清理代码

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作finally()方法返回 Promise 实例对象

  • 语法:

    js 复制代码
    promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···})

如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行。无论是 then()catch()还是 finally()添加的处理程序都是如此

js 复制代码
let p1 = Promise.resolve(); 
let p2 = Promise.reject(); 
//setTimeout(functionRef, delay, param1, param2, /* ... ,*/ paramN)
p1.then(() => setTimeout(console.log, 0, 1)); 
p1.then(() => setTimeout(console.log, 0, 2)); 
// 1 
// 2 
p2.then(null, () => setTimeout(console.log, 0, 3)); 
p2.then(null, () => setTimeout(console.log, 0, 4)); 
// 3 
// 4 
p2.catch(() => setTimeout(console.log, 0, 5)); 
p2.catch(() => setTimeout(console.log, 0, 6)); 
// 5 
// 6 
p1.finally(() => setTimeout(console.log, 0, 7)); 
p1.finally(() => setTimeout(console.log, 0, 8)); 
// 7 
// 8

回调地狱与链式调用

回调地狱

异步行为是JavaScript的基础,但在早期JavaScript实现并不理想,只支持定义回调函数来表明异步操作的完成。串联多个异步操作是很常见的问题,通常需要深度嵌套回调函数,但嵌套深度比较大时,代码可读性会变得非常差,整体代码也会变得十分臃肿,这里就形成了回调函数地狱问题(俗称回调地狱)

我们可以对回调地狱进行总结:

  • 概念:在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱
  • 缺点:可读性差,异常无法捕获,耦合性严重,牵一发而动全身

回调地狱问题,对于es6后的JavaScript来说,此种解决方案已经被遗弃,代替的有更好的实现方法,这里只是举例让大家了解JavaScript发展历史中的遗留问题

网上有一张图非常生动的展示了回调地狱的臃肿与可读性差:

Promise链式调用

Promise链式调用是解决回调地狱的一种方法。依靠then()方法会返回一个新生成的 Promise 对象特性,继续串联下一环任务,直到结束。then()回调函数中的返回值,会影响新生成的 Promise 对象最终状态和结果。

使用 Promise 链式调用的好处和作用如下:

  • 好处:通过链式调用,解决回调函数嵌套问题
  • 作用:使用 Promise 链式调用,解决回调函数地狱问题

Promise使用:每个 Promise 对象中管理一个异步任务,用 then 返回 Promise 对象,串联起来

下面为大家举一个使用 Promise 链式调用的例子:

  • 使用 axios 实现省市区联动

  • 通过 then 返回的 Promise 对象实现 axios 请求同步

    js 复制代码
    let pname
    axios({ url: '省份信息接口地址' })
      .then(res => {
        pname = res.data.list[0]
        document.querySelector('.province').innerHTML = pname
        return axios({ url: '省份对应城市信息接口地址', params: { pname } })
      })
      .then(res => {
        const cname = res.data.list[0]
        document.querySelector('.city').innerHTML = cname
        return axios({ url: '省市对应地区信息接口地址', params: { pname, cname } })
      })
      .then(res => {
        document.querySelector('.area').innerHTML = res.data.list[0]
      })
  • 整个 axios 链式结构如下:

Promise链式调用在 JavaScript 中处理异步操作时非常常见,这种调用链的形式使得异步代码更加清晰和可读

拒绝期约与拒绝错误处理

注:本节内容为红宝书对应小节内容的提炼,有助于对后文中异步代码报错问题的理解

拒绝期约类似于 throw() 表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理。在期约的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由。因此以下这些期约都会以一个错误对象为由被拒绝:

js 复制代码
let p1 = new Promise((resolve, reject) => reject(Error('foo'))); 
let p2 = new Promise((resolve, reject) => { throw Error('foo'); }); 
let p3 = Promise.resolve().then(() => { throw Error('foo'); }); 
let p4 = Promise.reject(Error('foo')); 
setTimeout(console.log, 0, p1); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p2); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p3); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p4); // Promise <rejected>: Error: foo

期约可以以任何理由拒绝,包括 undefined,但最好统一使用错误对象。这样做主要是因为创建错误对象可以让浏览器捕获错误对象中的栈追踪信息,而这些信息对调试是非常关键的

上述例子中的 4 个错误的栈追踪信息如下:

js 复制代码
Uncaught (in promise) Error: foo 
 at Promise (test.html:5) 
 at new Promise (<anonymous>) 
 at test.html:5 
                 
Uncaught (in promise) Error: foo 
 at Promise (test.html:6) 
 at new Promise (<anonymous>) 
 at test.html:6 
                 
Uncaught (in promise) Error: foo 
 at test.html:8 

Uncaught (in promise) Error: foo 
 at Promise.resolve.then (test.html:7)

Promise.resolve().then() 的错误最后才出现,这是因为它需要在运行时消息队列中添加处理程序;也就是说,在最终抛出未捕获错误之前它还会创建另一个期约 (微任务,后文有详解)

此例子引出了一个很重要的现象:

  • 正常情况下,在通过 throw() 关键字抛出错误时,JavaScript 运行时的错误处理机制会停止执行抛出错误之后的任何指令
javascript 复制代码
throw Error('foo'); 
console.log('bar'); // 这一行不会执行
// Uncaught Error: foo
  • 在 Promise 中抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时继续执行同步指令
js 复制代码
Promise.reject(Error('foo')); 
console.log('bar'); 
// bar 
// Uncaught (in promise) Error: foo
//由此可知:异步错误只能通过异步的 onRejected 处理程序捕获

then()和 catch() 的 onRejected 处理程序在语义上相当于 try/catch。出发点都是捕获错误之后将其隔离,同时不影响正常逻辑执行。为此,onRejected 处理程序的任务应该是在捕获异步错误之后返回一个解决的期约。下面的例子中对比了同步错误处理与异步错误处理:

js 复制代码
console.log('begin synchronous execution'); 
try { 
 throw Error('foo'); 
} catch(e) { 
 console.log('caught error', e); 
} 
console.log('continue synchronous execution'); 
// begin synchronous execution 
// caught error Error: foo 
// continue synchronous execution 

new Promise((resolve, reject) => { 
 console.log('begin asynchronous execution'); 
 reject(Error('bar')); 
}).catch((e) => { 
 console.log('caught error', e); 
}).then(() => { 
 console.log('continue asynchronous execution'); 
}); 
// begin asynchronous execution 
// caught error Error: bar 
// continue asynchronous execution

Promise关键问题

  • 一个 promise 指定多个成功/失败回调函数,都会调用吗?

    js 复制代码
    //当promise改变为对象状态时都会调用
    let p = new Promise((resolve, reject) => {
        resolve('OK')
    })
    p.then(res => {
        console.log(res)
    })
    p.then(res => {
        console.log(res)
    })
    //此处也可以改写成promise链式写法

    当promise对象状态改变时,对应的成功/失败均会调用

  • 改变 promise 状态 和执行回调函数谁先谁后?(本质上就是代码执行顺序问题,后文有详细讲解)

    js 复制代码
    let p = new Promise((resolve, reject) => {
        //resolve('OK')
        setTimeout(() => {
            resolve('ok')
        },1000) //宏任务,后执行
    })
    p.then(res => {
        console.log(res) //微任务先执行,后文有详细介绍
    })
  • promise 异常穿透?

    javascript 复制代码
    let p = new Promise((resolve, reject) => {
      reject('error')
    }).then(res => {
      console.log(res)
    }).then(res => {
      console.log(res)
    }).catch(err => {
    // 直接穿透前面的then函数,执行catch函数
      console.log(err) //error
    })

    (1)当使用 promise 的 then 链式调用时,可以在最后指定失败的回调

    (2)前面任何操作出了异常,都会传到最后失败的回调中处理

  • 如何中断 promise 链?

    当使用 promise 的 then 链式调用时,在中间中断,不再调用后面的回调函数

    • 在回调函数中返回一个 pendding 状态的 promise 对象(可能会造成内存泄露
    • 直接通过throw抛出错误对象
    • 使用reject
    • 在回调函数中返回一个 pendding 状态的 promise 对象
    javascript 复制代码
    let p = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('ok')
      }, 1000)
    }).then(res => {
      console.log(res) //想在此处中断
      return new Promise(() => { }) //返回 pendding 状态的 promise 的对象,中断!
    }).then(res => {
      console.log(res)
    }).then(res => {
      console.log(res)
    })
    • 直接通过throw抛出错误对象
    javascript 复制代码
    let p = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('ok')
      }, 1000)
    }).then(res => {
      console.log(res) //想在此处中断
      throw new Error('error here')
    }).then(res => {
      console.log('ok1')
    }).then(res => {
      console.log('ok2')
    }).catch(error => {
      console.log('catch error:', error)
    })
    • 使用reject
    javascript 复制代码
    let p = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('ok')
      }, 1000)
    }).then(res => {
      console.log(res) //想在此处中断
      return Promise.reject('error here')
    }).then(res => {
      console.log('ok1')
    }).then(res => {
      console.log('ok2')
    }).catch(error => {
      console.log('catch error:', error)
    })

Promise静态方法

resolve()

Promise.resolve(value)

  • value:成功的数据或 promise 对象
  • resolve()返回一个成功或失败的 promise 对象(不改变原promise对象状态)
javascript 复制代码
//如果传入的参数为 非 Promise 类型的对象,返回结果为成功 promise 对象
Promise.resolve("成功").then(
  (value) => {
    console.log(value); // "成功"
  },
  (reason) => {
    console.log(reason)// 不会被调用
  },
);

//如果传入的参数为 Promise 对象,则参数的结果决定了 resolve 的结果
Promise.resolve(new Promise((resolve, reject) => {
  reject('error')
})).then(
  (value) => {
    console.log(value); // 不会被调用
  },
  (reason) => {
    console.log(reason) //error
  }
)

reject()

Promise.reject(reason)

  • reason:失败的原因
  • 返回一个失败的 promise 对象(快速得到一个 promise 对象,如果为值也会被转换为 promise 对象,且永远是一个失败的 promise 对象
javascript 复制代码
//如果传入的参数为 非 Promise 类型的对象,返回结果为失败的 promise 对象
Promise.reject("失败").then(
  (value) => {
    console.log(value); // 不会被调用
  },
  (reason) => {
    console.log(reason) // "失败"
  },
);

const p = Promise.reject(new Promise((resolve, reject) => {
    resolve('ok')
}))
console.log(p)//依旧为失败的 promise 对象
//Promise {<rejected>: Promise}
//  [[Prototype]]: Promise
//  [[PromiseState]]: "rejected"
//  [[PromiseResult]]: Promise

all( )

Promise.all()方法用于将多个 Promise实例,包装成一个新的 Promise实例

语法:

js 复制代码
const p = Promise.all([p1, p2, p3])

接受一个数组(迭代对象)作为参数,数组成员都应为Promise实例

如果参数包含非 Promise 对象(即普通的值或非 Promise 对象),它们会被视为已经解决的 Promise,且会立即被视为成功

实例p的状态由p1p2p3决定,分为两种:

  • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数
  • 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法

  • 示例一(正常返回)

    javascript 复制代码
    const p1 = Promise.resolve(3);
    const p2 = 1337;
    const p3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve("foo");
      }, 100);
    });
    
    Promise.all([p1, p2, p3]).then((values) => {
      console.log(values); // [3, 1337, "foo"]
    });
  • 示例二(包含非promise对象)

    javascript 复制代码
    const promise1 = Promise.resolve(1);
    const promise2 = new Promise((resolve) => setTimeout(() => resolve(2), 1000));
    const value = 3;
    
    Promise.all([promise1, promise2, value])
      .then(results => {
        console.log(results); // [1, 2, 3]
      })
      .catch(error => {
        console.error(error);
      });
  • 示例三(返回值组成一个数组,传递给p的回调函数,参数promise对象自定义了catch方法)

    js 复制代码
    const p1 = new Promise((resolve, reject) => {
      resolve('hello')
    })
      .then((result) => result)
      .catch((e) => e)
    
    const p2 = new Promise((resolve, reject) => {
      throw new Error('报错了')
    })
      .then((result) => result)
      .catch((e) => e)
    
    const p = Promise.all([p1, p2])
      .then((result) => console.log(result)) // ["hello", Error: 报错了 at file xxx]
      .catch((e) => console.log(e)) //不执行
  • 示例四(p2没有自己的catch方法,就会调用Promise.all()catch方法)

    js 复制代码
    const p1 = new Promise((resolve, reject) => {
      resolve('hello')
    }).then((result) => result)
    
    const p2 = new Promise((resolve, reject) => {
      throw new Error('报错了')
    }).then((result) => result)
    
    Promise.all([p1, p2])
      .then((result) => console.log(result))
      .catch((e) => console.log(e)) // Error: 报错了

race( )

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例

语法:

js 复制代码
const p = Promise.race([p1, p2, p3])

只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变率先改变的 Promise 实例的返回值则传递给p的回调函数

示例:

js 复制代码
const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

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

const p = Promise.race([promise1, promise2])
setTimeout(() => {
  console.log(p); //Promise {<fulfilled>: 'two'}
}, 1000);

allSettled()

Promise.allSettled()方法将一个 Promise 可迭代对象作为输入,并返回一个新的 Promise 实例

当所有输入的 Promise 都已敲定时(包括传入空的可迭代对象时),返回的 Promise 将被兑现,并带有描述每个 Promise 结果的对象数组

语法:

javascript 复制代码
Promise.allSettled(iterable) //promise组成的可迭代对象,如数组

示例:

javascript 复制代码
Promise.allSettled([
  Promise.resolve(33),
  new Promise((resolve) => setTimeout(() => resolve(66), 0)),
  99,
  Promise.reject(new Error("一个错误")),
]).then((values) => console.log(values));

// [
//   { status: 'fulfilled', value: 33 },
//   { status: 'fulfilled', value: 66 },
//   { status: 'fulfilled', value: 99 },
//   { status: 'rejected', reason: Error: 一个错误 }
// ]

async和await语法糖

async 和 await 本质上是官方推出的 Promise 链式调用的优化语法 (ES8 新规范)。在 async 函数内,使用 await 关键字取代 then 函数,等待获取 Promise 对象成功状态的结果值 ,此种用法本质上就是 Promise 链式调用,只不过代码逻辑上形似于同步代码,通过使用 async 和 await 解决回调地狱问题

async关键字

async 关键字用于声明⼀个异步函数(如 async function asyncTask1() {...}

  • async 会自动将常规函数转换成 Promise,返回值也是⼀个 Promise 对象
  • promise 对象的结果由 async 函数执行的返回值决定
  • async 函数内部可以使用 await

MDN对于 async 函数定义如下:

  • async 函数是使用async关键字声明的函数。async 函数是 AsyncFunction 构造函数的实例,并且其中允许使用 await 关键字。asyncawait 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 promise

async 关键字可以用在函数声明、函数表达式、箭头函数和方法上

javascript 复制代码
// 使用 async 函数声明
async function asyncFunctionDeclaration() {...}

// 使用 async 函数表达式
const asyncFunctionExpression = async function() {...}

// 使用 async 箭头函数
const asyncArrowFunction = async () => {...}

// 对象方法中使用 async
const obj = {
  async asyncMethod() {...}
};

使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。 而在参数或闭包方面,异步函数仍然具有普通 JavaScript 函数的正常行为

  • 例如:

    js 复制代码
    //foo()函数仍然会在后面的指令之前执行
    async function foo() { 
     console.log(1); 
    } 
    foo(); 
    console.log(2); 
    // 1 
    // 2

与 Promise 不同的是,async 声明的函数可以使用 return 关键字 。异步函数如果使用 return 关键字返回了值(如果没有 return 则会默认返回 undefined),这个值会被Promise.resolve()包装成一个期约对象。异步函数始终返回期约对象。在函数外部调用这个函数可以得到它返回的期约。当然,直接返回一个期约对象也是一样的

  • 例如:

    js 复制代码
    async function foo() { 
     console.log(1); 
     return 3; 
    } 
    // 给返回的期约添加一个解决处理程序
    foo().then(console.log); //这里会自动将return的值传入console.log()函数中
    console.log(2); 
    // 1 
    // 2 
    // 3
    
    async function foo() { 
     console.log(1); 
     return Promise.resolve(3); 
    } 
    // 给返回的期约添加一个解决处理程序
    foo().then(console.log); 
    console.log(2); 
    // 1 
    // 2 
    // 3

与在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约(被Promise.reject()包装成一个期约对象)。不过,拒绝期约的错误不会被异步函数捕获(需要使用return)

  • 例如:

    js 复制代码
    async function foo() { 
     console.log(1); 
     throw 3; 
    } 
    // 给返回的期约添加一个拒绝处理程序
    foo().catch(console.log);
    console.log(2); 
    // 1 
    // 2 
    // 3
    
    async function foo() { 
     console.log(1); 
     Promise.reject(3); 
     //改成 return Promise.reject(3),则可以正常抛出错误
    } 
    // Attach a rejected handler to the returned promise 
    foo().catch(console.log); 
    console.log(2); 
    // 1 
    // 2 
    // Uncaught (in promise): 3

await关键字

await 用于等待异步的功能执行完毕 const result = await someAsyncCall()

  • await 放置在 Promise 调用之前,会强制 async 函数中其他代码等待,直到 Promise 完成并返回结果,才会恢复异步函数的执行
  • await 只能在 async 函数内部使用
  • await 右侧表达式一般为 promise对象,但也可以是其他值
    • 如果表达式是 promise 对象,await 返回的是 promise 成功的值
    • 如果表达式是其他值,直接将此值作为 await 的返回值
  • 如果 awaitpromise失败了,就会抛出异常,需要通过 try/catch 捕获处理

async/await 中真正起作用的是 await。async 关键字,无论从哪方面来看,都不过是一个标识符。毕竟,异步函数如果不包含 await 关键字,其执行基本上跟普通函数没有什么区别

  • 例如:

    js 复制代码
    async function foo() { 
     console.log(2); 
    } 
    console.log(1); 
    foo(); 
    console.log(3);
    // 1 2 3

JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。因此,即使 await 后面跟着一个立即可用的值,函数的其余部分也被异步求值

  • 红宝书中有一个很经典的例子:

    js 复制代码
    async function foo() { 
     console.log(2); 
     console.log(await Promise.resolve(8));//微任务
     console.log(9); 
    } 
    async function bar() {
     console.log(4); 
     console.log(await 6); //微任务
     console.log(7); 
    } 
    console.log(1); 
    foo(); 
    console.log(3); 
    bar(); 
    console.log(5); 
    // 1 
    // 2 
    // 3 
    // 4 
    // 5 
    // 8
    // 9 
    // 6 
    // 7
    // 同步任务先于异步任务(此处为微任务)执行
    // 微任务根据进入微任务队列的先后顺序,当主队列同步任务执行完后,开始依次执行

相较于 Promise,async/await 有何优势?

  1. 同步化代码的阅读体验(Promise 虽然摆脱了回调地狱,但 then 链式调用的阅读负担还是存在)
  2. 和同步代码更一致的错误处理方式( async/await 可以用 try/catch 做处理,比 Promise 的错误捕获更简洁直观)
  3. 调试时的阅读性, 也相对更友好

await 后面的异步函数中发生了错误(即 Promise 被拒绝),可以通过使用 try...catch 块来捕获和处理这个错误

javascript 复制代码
async function example() {
  try {
    const result = await someAsyncFunction();
    console.log(result); //此行不执行
  } catch (error) {
    console.error("error:", error.message); //error : Async operation failed
  }
}

async function someAsyncFunction() {
  return new Promise((resolve, reject) => {
    // 模拟异步操作
    setTimeout(() => {
      // 模拟异步操作中的错误
      reject(new Error("Async operation failed"));
    }, 1000);
  });
}

// 调用异步函数
example();

事件循环-EventLoop

事件循环

JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其他语言中的模型截然不同(如 C、Java)。事件循环的出现主要是因为JavaScript 是单线程的,为了不阻塞 JavaScript 引擎而设计的一种执行代码的模型

事件循环定义:

  • 一种执行代码和收集异步任务的模型。在调用栈空闲时,反复调用任务队列里回调函数的执行机制,就叫事件循环

为什么有事件循环:

  • JavaScript 是单线程的,同一时间只能做同一件事情,意味着有些耗时操作会阻塞代码执行,导致卡死的现象,所以就有了异步任务,而事件循环就是执行同步和异步任务的一种调度机制

在 JavaScript 内,会优先执行同步代码,遇到异步代码便会交给宿主浏览器环境执行,异步任务有了结果后,把回调函数放入任务队列排队(ES6之前)。当调用栈空闲后,会反复调用任务队列里的回调函数,上述过程不断重复就是事件循环

宏任务与微任务

ES6 之后引入了 Promise 对象, 让 JS 引擎也可以发起异步任务 (宏任务和微任务)

宏任务

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合,由 JS 引擎环境执行

常见的宏任务有:

  • 整体script
  • setTimeout / setInterval
  • UI rendering / UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)
  • DOM事件
  • Ajax(网络请求)

微任务

一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前,由浏览器环境执行

常见的微任务有:

  • Promise.then / Promise.catch/ Promise.finally(Promise 本身是同步的,而 then 和 catch 回调函数是异步的)
  • MutaionObserver
  • process.nextTick(Node.js)

ES6后执行第一个 script 脚本事件宏任务里面同步代码,遇到 宏任务/微任务 交给宿主环境,有结果的回调函数进入对应队列,当执行栈空闲时,清空微任务队列,再执行下一个宏任务,上述过程不断重复

代码执行顺序面试题

JS是异步单线程语言,同步事件执行完才执行事件循环内容(宏任务和微任务)

  • 同步任务 > 微任务 > 宏任务
  • 此外,对于 async 函数,只有从 await 往下才是异步的开始

注:本节console.log输出均为换行输出,只是为了更直观,书写成同一行

script宏任务

html 复制代码
<script>
    console.log(1);
    setTimeout(() => {
        console.log(2); //先进入宏任务队列排队
    }, 0)
    console.log(3);
</script>
<script>
    console.log(4);
    setTimeout(() => {
        console.log(5); //后进入宏任务队列排队
    }, 0)
    console.log(6);
</script>
<!--输出为:1 3 4 6 2 5 -->

先执行第一个script宏任务中的同步代码,异步代码放入宿主环境等待执行。当执行栈空闲时(执行完同步任务),清空微任务队列(执行微任务),再执行下一个宏任务,上述过程不断重复

DOM操作

javascript 复制代码
document.addEventListener('click', () => {
  let p = new Promise(resolve => resolve(1))
  p.then(result => console.log(result)) //微任务
  console.log(2) 
})
document.addEventListener('click', () => {
  let p = new Promise(resolve => resolve(3))
  p.then(result => console.log(result))
  console.log(4)
})
//正确答案 2 1 4 3

给同一元素添加相同的事件监听,会按照添加的顺序执行,因此两个宏任务会依次执行

宏任务执行机制:先等第一个任务的同步任务和微任务全部执行完,才会执行下一个宏任务,于是会输出2 1 4 3而不是2 4 1 3

Promise+setTimeout

题一

js 复制代码
new Promise((resolve, reject) => {
	console.log(1)
	new Promise((resolve, reject) => {
  	  	console.log(2)
    	setTimeout(() => {
    		console.log(3)
   		}, 0)
    	console.log(4)
	})
	console.log(5)
})
setTimeout(() => {
	console.log(6)
}, 1000)
console.log(7)
//输出为:1 2 4 5 7 3 6

Promise 本身是同步的,而 then 和 catch 回调函数是异步的;此外,对于setTimeout来说,延迟时间决定了执行的先后(setTimeout本身是宏任务,会按照进入宏任务队列次序来执行,但其本身有延迟时间,在宿主环境中倒计时结束后才会得到对应的结果,所以整体上看其延迟时间决定了执行的先后)

例如:将promise内部的setTimeout时间改为1000,外部setTimeout改为0,则输出变成 1 2 4 5 7 6 3

题二

js 复制代码
setTimeout(() => {
    console.log(1)
    new Promise((resolve, reject) => {
        resolve(2)
    }).then(res => {
        console.log(res)
        setTimeout(() => {
        	console.log(3)
        }, 1000)
    })
}, 0)
console.log(4)
setTimeout(() => {
    console.log(5)
}, 5000)
console.log(6)
//输出为:4 6 1 2 3 5

微任务先于宏任务执行,Promise本身是同步的

对于相同setTimeout按延迟时间排序执行:

  • 如果把5000改为500,则执行顺序变成:4 6 1 2 5 3
  • 如果把5000改为0,把0改成1000,则执行顺序变成:4 6 5 1 2 3

此类题核心不变:同步代码 > 微任务 > 宏任务,setTimeout按照宏任务队列排队顺序执行,但得到结果的时间取决于其延迟时间

题三

本题为 Node.js 环境(process 对象是 Node.js 环境中的一个全局对象)

javascript 复制代码
console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
//输出为:1 7 6 8 2 4 3 5 9 11 10 12

process.nextTick为微任务,当宏任务中的同步任务和微任务执行完毕之后,才会执行下一个宏任务

async await执行机制

  • await会阻塞其所在表达式中后续表达式的执行,在同一个作用域的await后面的代码是相当于then里面的代码,放到微任务队列
  • async 关键字用于声明⼀个异步函数,此外async 会自动将常规函数转换成 Promise,返回值也是⼀个 Promise 对象

题一

javascript 复制代码
async function async1() {
  console.log(1)
  await async2()
  console.log(2)
}
async function async2() {
  return new Promise((resolve, reject) => {
    reject(new Error(''))
  })
  .catch(err=>{
     console.log(err);
  })
}
console.log(3)
setTimeout(function () {
  console.log(4)
}, 0)
async1()
new Promise(function (resolve) {
  console.log(5)
  resolve()
}).then(function () {
  console.log(6)
})
console.log(7)
//输出为: 3 1 5 7 (打印error) 6 2 4

对于 async 函数,只有从 await 往下才是异步的开始

  • 代码按顺序执行,同步代码console.log(3)执行完毕,输出 3

  • 执行到setTimeout,进入宏任务队列

  • 执行到async1,执行其中的同步代码console.log(1),输出 1(3 1);执行await async2()时,遇到第一个微任务catch(err => console.log(err)),进入微任务队列排队

  • 执行new Promise中的同步代码console.log(5),输出 5(3 1 5) ,第二个微任务then(()=>console.log(6)),进入微任务队列排队

  • 执行console.log(7),输出 7(3 1 5 7)同步任务执行完毕,开始清空微任务队列

  • 执行第一个微任务catch(err => console.log(err)),打印 error(3 1 5 7 (打印error))

  • await async2()执行完毕,在同一个作用域的await后面的代码是相当于then里面的代码,都是放到微任务的,于是后面的console.log(2)进入微任务队列,这里为第三个进入微任务队列的代码

  • 执行第二个微任务then(()=>console.log(6)),输出 6(3 1 5 7 (打印error) 6)

  • 执行第三个微任务then包裹的console.log(2),输出 2(3 1 5 7 (打印error) 6 2)微任务队列清空

  • 执行最后的宏任务setTimeout代码块,输出 4(3 1 5 7 (打印error) 6 2 4)

  • 得到最终结果 3 1 5 7 (打印error) 6 2 4

题二(题一变种)

javascript 复制代码
async function async1() {
  console.log(1)
  await async2()
  console.log(2)
}
async function async2() {
  return new Promise((resolve, reject) => {
    reject(new Error(''))
  })
}
console.log(3)
setTimeout(function () {
  console.log(4)
}, 0)
async1()
new Promise(function (resolve) {
  console.log(5)
  resolve()
}).then(function () {
  console.log(6)
})
console.log(7)
//输出为: 3 1 5 7 6 4 (报错)

在异步代码中,如果一个任务执行失败(即Promise被拒绝),不会立即抛出异常,而是会被放到任务队列中最后抛出(该输出规则为浏览器环境,node环境输出不一样) 。也就是说,该错误会在其他代码执行完毕后,才抛出错误。此外,由于console.log(2)处于await后,类似于处于then方法中,因此该行也不会执行(promise变成reject时,then方法不会执行)。由此,输出结果为:3 1 5 7 6 4 (抛错)

题三

javascript 复制代码
async function foo () {
  console.log(2)
  console.log(await Promise.resolve(8))
  console.log(9)
}
async function bar () {
  console.log(4)
  console.log(await new Promise((resolve, reject) => {
    resolve(6)
  }))
  console.log(7)
}
console.log(1)
foo()
console.log(3)
bar()
console.log(5)
//输出为:1 2 3 4 5 8 9 6 7

await会阻塞其所在表达式中后续表达式的执行,在同一个作用域的await后面的代码是相当于then里面的代码,放到微任务队列

相关推荐
永乐春秋12 分钟前
WEB攻防-通用漏洞&文件上传&js验证&mime&user.ini&语言特性
前端
鸽鸽程序猿14 分钟前
【前端】CSS
前端·css
ggdpzhk16 分钟前
VUE:基于MVVN的前端js框架
前端·javascript·vue.js
小曲曲1 小时前
接口上传视频和oss直传视频到阿里云组件
javascript·阿里云·音视频
学不会•2 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
EasyNTS3 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
活宝小娜5 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点5 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow5 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o5 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app