moonshot 2 - Promise

154 阅读6分钟

Promise

从语法的角度来看,Promise是JS中的构造函数,它可以借助new关键字实例化出一个Promise对象。那么这个对象有什么作用呢?简单来说,它的出现是为了更好地解决异步问题。说到异步,就不得不提到JavaScript的单线程特性。下面我们就自底向上地研究。

  • 单线程的JavaScript 学习过操作系统的朋友应该不陌生线程的概念,按理来说,线程越多,就可以同时执行越多的任务,这样更快不是吗?很可惜并不是,我们要明确JavaScript是用来操作DOM元素、与用户进行交互的,所以我们更需要的是避免多线程带来的弊端——线程之间竞争的问题。

  • 异步 与异步相对的是同步。设想你在厨房中正在准备一份午餐,你必须等待锅里的油闷大虾煮好之后再去处理水池里的鱼,然后做一道酸菜鱼汤,这就是同步地执行做饭操作。而如果是异步呢,你就可以在等待油闷大虾做好的同时,去处理水池里的鱼,显然这样你会节省不少时间,而如果在油闷大虾做好之后,你的锅给了你一些提醒,比如说发起闹铃等等,那这就是我们后来要说的回调

  • JavaScript执行机制 但是别忘了,JavaScript是单线程的语言,单线程就意味着所有任务都需要排队,前一个任务完成后,后一个任务才能执行,如果前一个任务耗时较长,那么后一个任务也必须跟着等待。例如我们向服务器发送网络请求,请求一个结果,我们必须等待这个结果送过来才能继续往下执行,后面的任务也被迫需要跟着等待这个结果送过来,这是我们不愿意看到的。因此,我们决定让这一类等待时间长的任务(即异步任务)继续等着,先去执行后面的代码,把后面这些不需要等待的代码(即同步任务)全都执行完了,再回过头看看前面那些等待着的任务有哪些已经等待完成了,然后一个一个地把那些等待完成的任务执行掉。 总结一下就是下面四个步骤:

  1. 所有同步任务都在主线程上执行,形成一个执行栈
  2. 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列中放置一个事件。
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。
  • 异步操作的实现 JavaScript最开始利用回调函数来实现异步。在上述四个步骤中,具体执行异步任务的过程也就是执行回调函数的过程。当异步任务所需要等待的东西都等到了之后,它对应的回调函数才会“可以被执行”,等到执行栈全都执行完之后,才会真正开始执行这个回调函数。

很多时候,我们不得不需要使用多层回调,但当回调层数越来越多,我们的代码会陷入“回调地狱”,就像这样 image.png 显然这样不够好,因此我们需要一种更为优雅的方式来解决异步问题——Promise。

  • Promise
const p = new Promise(function(resolve, reject) {
    setTimeout(function() {
        let data = '某种数据';
        resolve(data);
        // let err = '失败了'
        // reject(err);
    }, 1000);
});

从上述代码我们可以看出,Promise在语法使用上,确实是一种构造函数,它搭配new来实例化出一个Promise对象,这个对象里封装了一个函数(必须是一个函数),这个函数在实例化出这个对象的时候就立即被执行,通常在这个函数的函数体里写异步操作,在上述例子中用setTimeout来模拟异步操作。

事实上,每一个Promise对象都有三种状态,如下图所示 image.png

函数有两个形参resolvereject(其实这两个形参的命名可以随意,只是习惯上写成在这两个单词),这两个形参分别又是两个内置的函数,并且在每一个Promise对象内部只能写一个resolve或一个reject,重复写的都会被忽略。

  1. 若使用resolve(value),则该Promise对象的内置属性state会变成“已满足(fulfilled)”状态,另一个内置属性result会变成传入的value值,通常这个值会是异步操作的结果。
  2. 若使用reject(error),则该Promise对象的内置属性state会变成“已拒绝(rejected)”状态,另一个内置属性result会变成传入的error值,通常这个值会是一个Error对象。
const p = new Promise(...);
// 通常的写法
p.then(function(value) {
    console.log('成功!', value);
}, function(reason) {
    console.log('失败!', reason);
});
// 若只关注成功的状态
p.then(value => console.log(value));
// 若只关注失败的状态
p.catch(reason => console.log(reason));
// 当Promise对象的状态被确定下来(不管是成功还是失败)
p.finally(...)

从上述代码可以看出每一个Promise对象都可以调用thencatchfinally这三个函数

  1. then then是最基础的函数,它几乎可以做到任何事情,通常p.then(function,function)接受两个函数作为形参,并且第一个函数是当Promise对象的状态被改为成功之后执行的函数,第二个函数是当Promise对象的状态被改为失败之后执行的函数。若不考虑失败的状态,则第二个函数可以直接不写,若不考虑成功的状态,则第一个函数需要写成null,或者函数体内部不写内容。
  2. catch 实际上p.catch(function)p.then(null, function)等价,就是一种只关注失败状态的简写形式罢了。
  3. finally 若写了p.finally(function),则不管Promise对象的状态是失败还是成功,只要是其中的一种,就可以执行里面的函数,通常我们在这个函数里做一些收尾的事情。
  • Promise链 我们说过Promise.prototype.then是一个函数,既然是函数就理应有一个返回值,那么它的返回值是什么呢?

还是一个Promise对象!

既然是Promise对象,就一定有内置的状态和值,这个由then函数返回的Promise对象的状态取决于then函数里所写的return

  1. 若then里return的是一个非Promise对象,则实际返回的Promise对象的状态是“成功”,而值就是return语句后面写的那个东西。(即使你没有在then里写return语句,那么就默认return了undefined,这依然不是一个Promise对象,所以状态还是“成功”,值是undefined)。
  2. 若then里return的是一个Promise对象,则then实际返回的Promise对象的状态和值就取决于return后面这个Promise对象。
  3. 若在then里用throw抛出了一个错误,那么实际返回的Promise对象的状态就是“失败”,值就是抛出的那个错误。 说到这里我们不难发现,既然then函数可以返回一个Promise对象,那么我们当然可以形成一个Promise链,用来代替我们很讨厌的“回调地狱”。