聊聊promise

417 阅读17分钟

导读

看下面一段代码:

function foo() {
    setTimeout(() => {
        console.log('a');
    }, 1000);    
}

function bar() {
    console.log('b');
}

foo()
bar()

这段代码输出结果为:

image.png 我们发现是先打印出b,后打印出a

这是由于,因为js是单线程执行的,一次只能干一件事,当调用foo时,执行引擎发现foo里面的内容输出需要等待时间,于是便把这一进程挂起来,让它自己去等,先执行bar的调用,将bar执行输出b,等待时间完毕后,便输出了a。我们称这一过程为异步

通俗点讲:遇到需要耗时的代码,那就先挂起,先执行不耗时的代码,等到不耗时的代码执行完了,执行引擎腾出手了,再来执行耗时代码。

补充:有异步就会有同步

同步:代码会按照顺序依次执行,每一行代码只有在前一行代码执行完毕后才会开始执行。在同步模式下,程序会阻塞(暂停)直到当前任务完成,然后继续执行下一行代码。

因为有了异步这个概念,我们写代码会遇到些问题,如下:

let data = null

function a() {
    setTimeout(() => {
        data = {name: 'a'}
    }, 1000);
}

function b() {
    console.log(data.name + 'b');
}

a()
b()

运行后出错: image.png 为什么会出错,这很好理解,根本就是a的结果需要时间,而b的结果需要a的结果才能输出,因为异步,执行引擎会先执行b函数,而这时,a的结果还没出来,于是执行引擎报错了。

在es6之前,我们可以通过回调函数来解决此类问题,如下:

let data = null

function a() {
    setTimeout(() => {
        data = {name: 'a'}
        b()
    }, 1000);
}

function b() {
    console.log(data.name + 'b');
}

a()

image.png 通过将b的调用放入a函数setTimeout中,间接的将异步转为同步,解决了此类问题,可谓妙哉!

但在某种情况下,这也许并不是一件轻松的事,例如,当回调的数量达到某种级别,代码会显得过于繁杂和冗余,a等待b的结果,b等待c的结果,c等待......,层层递进,我们一般吐槽这种为回调地狱,显然我们都不希望看到这种代码的出现,那将是一件非常糟糕的事!!!(嵌套过深,一旦出现问题很难排查

于是在es6中,引入了promise来减轻我们的压力,释放天性......

promise简要解读

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。本质上也是将异步转为同步去解决问题,只不过Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。使得代码更加的优雅。

Promise对象有以下两个特点。

  1. 不可变性:Promise对象的状态一旦确定下来,无论是变为fulfilled还是rejected,都是不可逆的。这意味着外部代码无法通过任何方式改变一个已经resolve(包括fulfilled或rejected)的Promise的状态。这一特性确保了异步操作的结果是一致且可预测的,对于维护代码的稳定性和理解程序的执行流程至关重要。
  2. 记忆性(也称确定性) :一旦Promise的状态从pending变为fulfilled或rejected(即resolve),它就会“记住”这个结果,后续任何时候添加到该Promise上的回调函数(通过.then.catch)都会立即按照Promise当前的状态执行。即使在Promise创建很久之后注册这些回调,它们也能立刻获得之前异步操作的结果,而不需要再次等待。这一点大大简化了异步编程模型,使得开发者可以专注于逻辑处理,而不是管理复杂的异步控制流。

这些机制使得Promise成为处理异步操作的理想选择,尤其是在需要链式调用多个异步操作或者并行处理多个异步任务的场景下。通过明确的链式调用和错误处理机制,Promise有助于提升代码的可读性和可维护性。

基本用法

Promise对象是一个构造函数,可以生成Promise实例。

下面代码创造了一个Promise实例。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果(value为你想要返回的东西),作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定都要提供。它们都接受Promise对象传出的值(valueerror)作为参数。

下面是一个Promise对象的简单例子。

function timeout() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
            console.log('a is ok');
            resolve('a')
        }, 1000);  
  });
}

timeout().then((value) => {
  console.log(value);
});

//a is ok
//a

上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(1000ms)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数,输出。

输出顺序也有考究,如下:

Promise 新建后就会立即执行。

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完后才会执行,即'Hi!'会比resolved先输出,resolved最后输出。因此在上一个例子中也就能明白,'a is ok'会比'a'更先输出。

如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出的错误即说明异步操作出现问题

resolve函数的参数是另一个 Promise 实例的话,如下。

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => {
            console.log('a is ok');
            resolve('a')
        }, 1000);
});

const p2 = new Promise(function (resolve, reject) {
  // ...
  resolve(p1);
})

上面代码中,p1p2都是 Promise 的实例,但是p2resolve方法将p1作为参数,即一个异步操作的结果返回另一个异步操作。

注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。

在这里,p1的状态是由pending --> resolved,因此只有等p1状态发生改变时,p2的回调函数才立刻执行。

注意:调用resolvereject并不会终结 Promise 的参数函数的执行。就如之前所说的,它们总是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务,如下:

new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

上面代码中,调用resolve(1)以后,后面的console.log(2)还是会执行,并且会首先打印出来。

一般来说,调用resolvereject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolvereject的后面。鉴于代码更加通畅合乎常理,为了不执行resolvereject后面的语句,可以在它们前面加上return语句,这样就不会执行后面的语句了

new Promise((resolve, reject) => {
  return resolve(1);
  // 后面的语句不会执行
  console.log(2);
})

Promise.prototype.then()

Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数。可以只写其中一个,也可以写两个。在这里更加详细的讲解then的使用。

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

xq().then(function(res) {
  return marry(res);
}).then(function (comments) {
  console.log("resolved: ", comments);
}, function (err){
  console.log("rejected: ", err);
});

上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为resolved,就调用第一个回调函数,如果状态变为rejected,就调用第二个回调函数。

在这里,第一个then传入了一个参数即resolved状态的回调函数;第二个then传入了两个参数,一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数。

采用箭头函数的代码更简洁。

xq().then(
  res => marry(res)
).then(
  comments => console.log("resolved: ", comments),
  err => console.log("rejected: ", err)
);

也有另一种嵌套的方法写

xq().then(() => {
    marry().then(() => {
        baby()
    })
}

但不建议这种方法写,不优雅。

Promise.prototype.catch()

Promise.prototype.catch()方法用于捕获发生错误时的回调函数。正如之前所说的,异步操作也会出现失败(reject),需要精准捕获。

getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // 处理 getJSON 和 前一个回调函数运行时发生的错误
  console.log('发生错误!', error);
});

上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。

如果 Promise 状态已经变成resolved,再抛出错误是无效的。

const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok

上面代码中,Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。

Promise 对象的错误会像“冒泡”一样一直向后传递,直到被捕获为止。也就是说,后面catch语句总能捕获前面的错误。

getJSON('/post/1.json').then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // some code
}).catch(function(error) {
  // 处理前面三个Promise产生的错误
});

上面代码中,一共有三个 Promise 对象:一个由getJSON()产生,两个由then()产生。它们之中任何一个抛出的错误,都会被最后一个catch()捕获。

一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),而是使用catch方法干这件事。如下对比:

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面所有then方法执行中的错误。因此,建议总是使用catch()方法,而不使用then()方法的第二个参数。

如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有报错,进程不会中断,后面的代码依旧会执行。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  console.log('everything is great');
});

setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123

上面代码中,someAsyncThing()函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined,但是不会退出进程、终止脚本执行,2 秒之后还是会输出123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。

再看下面的例子。

const promise = new Promise(function (resolve, reject) {
  resolve('ok');
  setTimeout(function () { throw new Error('test') }, 0)
});
promise.then(function (value) { console.log(value) });
// ok
// Uncaught Error: test

catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on

上面代码运行完catch()方法指定的回调函数,会接着运行后面那个then()方法指定的回调函数。如果没有报错,则会跳过catch()方法。

Promise.resolve()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// carry on

上面的代码因为没有报错,跳过了catch()方法,直接执行后面的then()方法。此时,要是后面then()方法里面报错,就与前面的catch()无关了。catch()无法捕捉后面的错误

catch()方法之中,还能再抛出错误。

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  return someOtherAsyncThing();
}).catch(function(error) {
  console.log('oh no', error);
  // 下面一行会报错,因为 y 没有声明
  y + 2;
}).then(function() {
  console.log('carry on');
});
// oh no [ReferenceError: x is not defined]

上面代码中,catch()方法抛出一个错误,因为后面没有别的catch()方法了,导致这个错误不会被捕获,也不会传递到外层。改写后就能捕获了。

someAsyncThing().then(function() {
  return someOtherAsyncThing();
}).catch(function(error) {
  console.log('oh no', error);
  // 下面一行会报错,因为y没有声明
  y + 2;
}).catch(function(error) {
  console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]

上面代码中,第二个catch()方法用来捕获前一个catch()方法抛出的错误。

Promise.prototype.finally()

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

finally本质上是then方法的特例。

promise
.finally(() => {
  // 语句
});

// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

了解了finally本质后,我们可以自己实现finally,如下:

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

上面代码中,不管前面的 Promise 是fulfilled还是rejected,都会执行回调函数callback。结果符合finally的特性。

Promise.all()

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

const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口即可迭代遍历,且返回的每个成员都是 Promise 实例。

Promise.all()方法正是用来处理多个Promise实例集合,它将这些Promise实例包装成一个新的Promise实例p,使得p的状态由这些Promise实例的状态共同决定,分两种情况所示:

  1. 全部fulfilled:只有当数组中的所有Promise实例都变为fulfilled(即都成功完成),Promise.all()返回的p才会变为fulfilled状态。此时,所有Promise的resolve值(即它们成功的返回值)会组成一个新的数组,传递给p.then方法注册的回调函数。
  2. 任一rejected:如果数组中的任何Promise实例变为rejected(即遇到错误),Promise.all()返回的p会立即变为rejected状态,而无需等待其他Promise的完成。第一个被rejected的Promise实例的reason(即错误原因)会被传递给p.catch方法注册的回调函数。

注意,当在每个Promise实例中都定义了.catch方法来处理拒绝(rejected)状态时,即使某个Promise因为错误而拒绝,如果它的.catch方法能够妥善处理这个错误(比如捕获错误但不抛出新的错误),那么这个Promise链最终会以解决(resolved)状态结束。这意味着对于Promise.all()来说,它关注的是所有传入Promise最终是否都变为解决状态,而不是它们中间是否曾经有拒绝的情况。

具体到例子中:

  • 示例1
const p1 = ... // 最终会resolved
const p2 = ...
    .then(result => result)
    .catch(e => e); // p2虽然内部抛出了错误,但被捕获并返回为一个值,所以p2整体视为resolved

Promise.all([p1, p2])
    .then(...) // 这里会被调用,因为p1和经过catch处理后的p2都是resolved
    .catch(...); // 这里不会被调用

在这个例子中,尽管p2在创建时抛出了错误,但因为它有自己的.catch处理程序,这个错误被转换处理了(转换成了一个包含错误对象的resolved Promise),所以Promise.all([p1, p2])看到的是两个都已解决的Promise,因此调用了.then方法指定的回调。

  • 示例2
const p1 = ...
const p2 = ...
    .then(result => result);

Promise.all([p1, p2])
    .then(...)
    .catch(e => console.log(e)); // 这里会被调用,因为p2直接拒绝且没有被捕获

而在第二个例子中,由于p2没有.catch来处理其内部的错误,当它被拒绝时,这个拒绝状态会直接传递给Promise.all(),使得Promise.all()也变为拒绝状态,从而调用其.catch方法。

总结来说,Promise.all()的行为取决于传入的所有Promise实例最终的状态:只有当所有Promise都解决时,它才会解决;只要有一个Promise拒绝,它就会立即拒绝,除非那个拒绝的Promise有自己的错误处理机制并能成功将其状态转换为解决。

结语

Promise还有些其他的方法和用处,想要了解更多,可以多加阅读相关书籍,学习更多大佬的思想!!!,感谢阅读!!!有错误,评论区指出,感谢大佬的指点!!!