前言:个人学习笔记,初识Promise

1.Promise解决了什么问题?

用来解决 回调地狱 问题

回调地狱:在JS中,为了实现某些逻辑经常会写出层层嵌套的 回调函数,如果嵌套过多,会极大的影响代码可读性和逻辑,这种情况被称为回调地狱。比如说把一个函数A作为回调函数,但是该函数又接收一个函数B作为参数,甚至B还能接受函数C作为参数,这样层层嵌套,人们称为回调地狱,代码阅读性非常的差。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
        let sayHello = function (name, callback) {
            setTimeout(function () {
                console.log(name);
                callback();
            }, 1000);
        }
        sayHello('first', function () {
            sayHello('second', function () {
                sayHello('third', function () {
                    console.log('end');
                });
            });
        })

回调函数:回调是一个函数,它作为参数传递给另一个函数,并在其父函数完成后执行。

1
2
3
4
5
6
7
8
9
        function doSomething(msg, callback) {
            alert(msg);
            if (typeof callback === 'function') {
                callback();
            }
        }
        doSomething('回调函数', function () {
            alert('匿名函数实现回调');
        });

回调既可以同步回调,也可以异步回调。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
        // 同步回调
        function getNodes(param, callback) {
            let list = JSON.stringify(param);
            if (typeof callback === 'function') {
                callback(list)
            }
        }
        getNodes('[1,2,3]', function (nodes) {
            console.log(nodes);
        });
        //ajax回调
        $.get('ajax/test.html', function (data) {
            $('box').html(data);
        })

        // 点击事件回调 
        $('#myBtn').click(function () {
            alert('click myBtn!');
        })

感受:JS虽然灵活,但是灵活的同时,带来的缺点就是,写代码的过程中需要加一些判断,比如上面这段代码中判断callback是不是函数,在别的强类型语言中,这个callback如果不是函数,可能写的时候编辑器就会直接提醒,传入参数的类型不对。所以就会出现TypeScript。

注意回调函数中的this指向:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
        var clientData = {
            id: 096545,
            fullName: "Not Set",
            setUserName: function (firstName, lastName) {
                this.fullName = firstName + " " + lastName;
            }
        }
        function getUserInput(firstName, lastName, callback) {
            //调用回调函数存储,此时是window调用的这个传进来的函数
            callback(firstName, lastName);
        }
        getUserInput("Barack", "Obama", clientData.setUserName);
        console.log(clientData.fullName);  //Not Set
        console.log(window.fullName);  //Barack Obama

上面这段代码中回调函数的this是指向window的,此时可以使用call或apply改下this的指向

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
        var clientData = {
            id: 096545,
            fullName: "Not Set",
            setUserName: function (firstName, lastName) {
                this.fullName = firstName + " " + lastName;
            }
        }
        function getUserInput(firstName, lastName, callback) {
            //调用回调函数存储,此时是window调用的这个传进来的函数
            callback.call(clientData, firstName, lastName);
        }
        getUserInput("Barack", "Obama", clientData.setUserName);
        console.log(clientData.fullName);  //Barack Obama
        console.log(window.fullName);  //undefined

问题:JS底层是怎么实现异步的,相关内容:V8引擎是如何实现异步的,EventLoop相关原理深度解析,JS执行过程中的堆栈变化,异步是不是等同于并行以及其它相关底层问题。

链式调用:同一对象多次其属性或方法的时候,我们需要多次书写对象进行“.” 或 () 操作;链式调用是一种简化此过程的一种编码方式,使代码简洁、易读。

函数柯里化:

2.Promise使用

资料相关:

Promise是一个对象,代表一个异步操作的最终完结或失败。

(1)Promise特点

1.在本轮 事件循环 运行完成之前,回调函数是不会被调用的。

2.即使异步操作已经完成(成功或失败),在这之后通过 then() 添加的回调函数也会被调用。

3.通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序进行执行。

4.Promise 很棒的一点就是链式调用(chaining)(Promise可以是使用链式调用)

(2)链式调用

连续执行两个或者多个异步操作是一个常见的需求,在上一个操作执行成功之后,开始下一个的操作,并带着上一步操作所返回的结果。我们可以通过创造一个 Promise 链来实现这种需求。基本上,每一个 Promise 都代表了链中另一个异步过程的完成。

注意:一定要有返回值,否则,callback 将无法获取上一个 Promise 的结果。(如果使用箭头函数,() => x 比 () => { return x; } 更简洁一些,但后一种保留 return 的写法才支持使用多个语句。)。

(3)Catch 的后续链式操作

有可能会在一个回调失败之后继续使用链式操作,即,使用一个 catch,这对于在链式操作中抛出一个失败之后,再次进行新的操作会很有用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
        new Promise((resolve, reject) => {
            console.log('初始化');

            resolve();
        })
            .then(() => {
                throw new Error('有哪里不对了');

                console.log('执行「这个」”');
            })
            .catch(() => {
                console.log('执行「那个」');
            })
            .then(() => {
                console.log('执行「这个」,无论前面发生了什么');
            });

打印结果:

注意:因为抛出了错误 有哪里不对了,所以前一个 执行「这个」 没有被输出。

(4)错误传递

通常,一遇到异常抛出,浏览器就会顺着 Promise 链寻找下一个 onRejected 失败回调函数或者由 .catch() 指定的回调函数。这和以下同步代码的工作原理(执行过程)非常相似。

通过捕获所有的错误,甚至抛出异常和程序错误,Promise 解决了回调地狱的基本缺陷。这对于构建异步操作的基础功能而言是很有必要的。

(5)Promise 拒绝事件

Promise 被拒绝时,会有下文所述的两个事件之一被派发到全局作用域(通常而言,就是window;如果是在 web worker 中使用的话,就是 Worker 或者其他 worker-based 接口)。

(1)rejectionhandled

当 Promise 被拒绝、并且在 reject 函数处理该 rejection 之后会派发此事件。

(2)unhandledrejection

当 Promise 被拒绝,但没有提供 reject 函数来处理该 rejection 时,会派发此事件。

以上两种情况中,PromiseRejectionEvent 事件都有两个属性,一个是 promise 属性,该属性指向被驳回的 Promise,另一个是 reason 属性,该属性用来说明 Promise 被驳回的原因。

因此,我们可以通过以上事件为 Promise 失败时提供补偿处理,也有利于调试 Promise 相关的问题。在每一个上下文中,该处理都是全局的,因此不管源码如何,所有的错误都会在同一个处理函数中被捕捉并处理。

一个特别有用的例子:当你使用 Node.js 时,有些依赖模块可能会有未被处理的 rejected promises,这些都会在运行时打印到控制台。你可以在自己的代码中捕捉这些信息,然后添加与 unhandledrejection 相应的处理函数来做分析和处理,或只是为了让你的输出更整洁。举例如下:

1
2
3
4
5
6
7
window.addEventListener("unhandledrejection", event => {
  /* 你可以在这里添加一些代码,以便检查
     event.promise 中的 promise 和
     event.reason 中的 rejection 原因 */

  event.preventDefault();
}, false);

调用 event 的 preventDefault() 方法是为了告诉 JavaScript 引擎当 Promise 被拒绝时不要执行默认操作,默认操作一般会包含把错误打印到控制台,Node 就是如此的。

理想情况下,在忽略这些事件之前,我们应该检查所有被拒绝的 Promise,来确认这不是代码中的 bug。

(6)在旧式回调 API 中创建 Promise

理想状态下,所有的异步函数都已经返回 Promise 了。但有一些 API 仍然使用旧方式来传入的成功(或者失败)的回调。典型的例子就是 setTimeout() 函数:

1
setTimeout(() => saySomething("10 seconds passed"), 10000);

混用旧式回调和 Promise 可能会造成运行时序问题。如果 saySomething 函数失败了,或者包含了编程错误,那就没有办法捕获它了。这得怪 setTimeout。

幸运地是,我们可以用 Promise 来封装它。最好的做法是,将这些有问题的函数封装起来,留在底层,并且永远不要再直接调用它们:

1
2
3
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));

wait(10000).then(() => saySomething("10 seconds")).catch(failureCallback);

通常,Promise 的构造器接收一个执行函数(executor),我们可以在这个执行函数里手动地 resolve 和 reject 一个 Promise。既然 setTimeout 并不会真的执行失败,那么我们可以在这种情况下忽略 reject。

(7)组合

Promise.resolve() 和 Promise.reject() 是手动创建一个已经 resolve 或者 reject 的 Promise 快捷方法。它们有时很有用。

Promise.all() 和 Promise.race() 是并行运行异步操作的两个组合式工具。

我们可以发起并行操作,然后等多个操作全部结束后进行下一步操作,如下:

1
2
Promise.all([func1(), func2(), func3()])
.then(([result1, result2, result3]) => { /* use result1, result2 and result3 */ });

可以使用一些聪明的 JavaScript 写法实现时序组合:

1
2
[func1, func2, func3].reduce((p, f) => p.then(f), Promise.resolve())
.then(result3 => { /* use result3 */ });

通常,我们递归调用一个由异步函数组成的数组时,相当于一个 Promise 链:

1
Promise.resolve().then(func1).then(func2).then(func3);

我们也可以写成可复用的函数形式,这在函数式编程中极为普遍:

1
2
const applyAsync = (acc,val) => acc.then(val);
const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

composeAsync() 函数将会接受任意数量的函数作为其参数,并返回一个新的函数,该函数接受一个通过 composition pipeline 传入的初始值。这对我们来说非常有益,因为任一函数可以是异步或同步的,它们能被保证按顺序执行:

1
2
const transformData = composeAsync(func1, func2, func3);
const result3 = transformData(data);

在 ECMAScript 2017 标准中, 时序组合可以通过使用 async/await 而变得更简单:

1
2
3
4
let result;
for (const f of [func1, func2, func3]) {
  result = await f(result);
}

8.时序

为了避免意外,即使是一个已经变成 resolve 状态的 Promise,传递给 then() 的函数也总是会被异步调用。

9.嵌套

简便的 Promise 链式编程最好保持扁平化,不要嵌套 Promise,因为嵌套经常会是粗心导致的。可查阅下一节的常见错误中的例子。

10.常见错误

在编写 Promise 链时,需要注意以下示例中展示的几个错误:

1
2
3
4
5
6
7
// 错误示例,包含 3 个问题!

doSomething().then(function(result) {
  doSomethingElse(result) // 没有返回 Promise 以及没有必要的嵌套 Promise
  .then(newResult => doThirdThing(newResult));
}).then(() => doFourthThing());
// 最后,是没有使用 catch 终止 Promise 调用链,可能导致没有捕获的异常

一个好的经验法则是总是返回或终止 Promise 链,并且一旦你得到一个新的 Promise,返回它。下面是修改后的平面化的代码:

1
2
3
4
5
6
7
doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing())
.catch(error => console.log(error));

使用 async/await 可以解决以上大多数错误,使用 async/await 时,最常见的语法错误就是忘记了 await 关键字。

3.Promise

一个Promise对象在被创建出来的时候,不一定值是什么,异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。

(1)Promise状态

1.待定(pending):初始状态,既没有兑现,也没有拒绝。 2.已兑现(fulfilled):意味着操作成功完成。 3.已拒绝(rejected):意味着操作失败。

因为 Promise.prototype.then 和 Promise.prototype.catch 方法返回的是 promise, 所以它们可以被链式调用。

resolved: 已决议(resolved),它表示 promise 已经处于已敲定(settled)状态,或者为了匹配另一个 promise 的状态被"锁定"了。

注意:resolved不在Promise的三种状态之中

(2)Promise()构造函数

用于包装还没有添加 promise 支持的函数。

4.Promise方法

(1)Promise.all()

只有当参数里的所有异步操作都返回成功的时候,Promise.all()才会返回成功,否则,有一个错误,就会返回错误。

语法:

1
Promise.all(iterable);

参数:

一个可迭代对象,如 Array 或 String。

返回值:

1.传入空的可迭代对象时,返回一个已完成状态的Promise

2.传入的参数不包含Promise,返回一个异步完成的Promise

3.其它情况下返回一个处理中(pending)的Promise

使用:

1
2
3
4
5
6
7
8
9
        var p1 = Promise.resolve(3);
        var p2 = 1337;
        var p3 = new Promise((resolve, reject) => {
            setTimeout(resolve, 100, 'foo');
        });

        console.dir(Promise.all([p3, p1, p2]).then(values => {
            console.log(values); // [3, 1337, "foo"]
        }));
1
2
3
4
5
6
7
8
        var p = Promise.all([1, 2, 3]);
        var p2 = Promise.all([1, 2, 3, Promise.resolve(444)]);
        var p3 = Promise.all([1, 2, 3, Promise.reject(555)]);
        setTimeout(function () {
            console.log(p);
            console.log(p2);
            console.log(p3);
        });

(2)Promise.allSettled()

该Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的promise结果。

当您有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise的结果时,通常使用它。

参数:

一个可迭代的对象,例如Array,其中每个成员都是Promise。

注意:此方法需要谷歌76版本以上才会支持

(3)Promise.any()

只要Promise中有一个返回成功就返回成功,所有的都失败才会返回失败,跟 Promise.all()方法正好相反, any 方法和 all 方法有点类似于或和且的关系。

此方法需要谷歌85版本以上才会支持

(4)Promise.race()

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

只要可迭代对象中有一个返回,Promise.race()就会立马返回这个状态的值,返回的也是一个Promise

(5)Promise.reject()

Promise.reject()方法返回一个带有拒绝原因的Promise对象。

参数:

reason:表示Promise被拒绝的原因。

返回值:一个给定原因了的被拒绝的 Promise。

(6)Promise.resolve()

Promise.resolve(value)方法返回一个以给定值解析后的Promise 对象。

警告:不要在解析为自身的thenable 上调用Promise.resolve。这将导致无限递归,因为它试图展平无限嵌套的promise。一个例子是将它与Angular中的异步管道一起使用。在此处了解更多信息。

参数:

value:将被Promise对象解析的参数,也可以是一个Promise对象,或者是一个thenable。

返回值:

返回一个带着给定值解析过的Promise对象,如果参数本身就是一个Promise对象,则直接返回这个Promise对象。

5.Promise原型

(1)Promise.prototype.catch()

添加一个拒绝(rejection) 回调到当前 promise, 返回一个新的promise。当这个回调函数被调用,新 promise 将以它的返回值来resolve,否则如果当前promise 进入fulfilled状态,则以当前promise的完成结果作为新promise的完成结果。返回一个Promise,所以catch后面可以继续使用then。

(2)Promise.prototype.then()

添加解决(fulfillment)和拒绝(rejection)回调到当前 promise, 返回一个新的 promise, 将以回调的返回值来resolve.

then() 方法返回一个 Promise。它最多需要有两个参数:Promise 的成功和失败情况的回调函数。

(3)Promise.prototype.finally()

finally() 方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都需要执行的代码提供了一种方式。 这避免了同样的语句需要在then()和catch()中各写一次的情况。

返回值:

返回一个设置了 finally 回调函数的Promise对象。

6.创建Promise

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

7.async/await

async函数

async/await时ES8新增的

8.generator 和 yield

9.try…catch

try…catch

3.涉及到的面试题

(1)Promise.then里抛出的错误能否被try…catch捕获,为什么?

不能,因为try catch只能处理同步的错误,对异步错误没有办法捕获

try catch为什么不能捕获异步错误?

因为try执行时从上到下的,异步代码没有返回,try…catch 怎么捕获

then 的第二个参数和使用 catch 的区别

主要区别就是,如果在 then 的第一个函数里抛出了异常,后面的 catch 能捕获到,而第二个函数捕获不到

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
        new Promise(resolve => resolve(123))
            .then(_ => {
                throw new Error()
            }, e => {
                console.log(256);
                console.log(e);
            }).catch(e => {
                console.log(458);
                console.log(e);
            })

也就是说 then 里面抛出的还是需要后面的catch去捕获,而then的第二个参数回调函数是在之前返回的结果为 rejected 时调用的

那 then 之后的错误如何捕获呢。

1
2
3
4
5
6
7
8
9
function main3() {
  Promise.resolve(true).then(() => {
    try {
      throw new Error('then');
    } catch(e) {
      return e;
    }
  }).then(e => console.log(e.message));
}

只能是在回调函数内部 catch 错误,并把错误信息返回,error 会传递到下一个 then 的回调。

拓展:promise 内部的错误不会冒泡出来,而是被 promise 吃掉了,只有通过 promise.catch 才可以捕获,所以用 Promise 一定要写 catch 啊。

(2)你怎么解决“回掉地狱”的问题?你对Proxy,和Promise的理解,在哪里用到过?

(3)请手写实现一个 promise?

(4)淘宝等网站的倒计时功能如何实现?

(5)写一个 promise 重试函数,可以设置时间间隔和次数

(6)手写 Promise.all

(7)手写并发

(8)promise讲解,all和race的作用

(9)promise输出

(10)迭代器的,yield 和 yield*的区别。还有promise的race。

(11)promise的日常应用

(12)简单描述一下promise,并说明如何在外部进行resolve()

(13)Promise的作用及用法以及内部实现原理?

(14)说一下你对 generator 的了解?

(15)promise.then().then() ,promise.catch().then() ok 不?

(16)js实现带并发限制的调度器,其实就是使用promise限制并发

(17)如何理解 Promise?Promise 对象的含义?

(18)对async和await的理解

(19)vue promise原理

(20)vue promise 同时发起

(21)js 并发请求

(22)如何破坏promise链?

(23)catch 之后的 then 还会执行吗?

会继续执行,因为catch方法返回的是promise,所以后续的then还会继续执行,当然如果在catch中手动返回 Promise.reject(),那后续的then就不会被调用了。