ES6 学习 -- Promise

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zgh0711/article/details/80764582

ES6 中新增了很多很好用的新特性,Promise 就是其中之一,现在也得到了广泛的运用,今天就来学习下 Promise 。
在老的 JS 里面,异步函数的处理是一个很麻烦的事情,而且一不小心还会陷入多重回调嵌套的 ‘回调地狱’,Promise 就是为了解决这个问题而诞生的。
这篇文章是我在学习 Promise 时做的笔记,学习资料来源 Promise 入门,这是慕课网的一个视频课程,另外还有一份资料是阮一峰老是的 ES6 入门里面的 Promise 对象

用途

  • 主要用于异步计算
  • 可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果
  • 可以在对象之间传递和操作 Promise,帮助我们处理队列

特点

Alt text
从上面的图中可以看出 Promise 的几个特点
- Promise 是一个代理对象,它和原先要进行的操作并无关系。对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
- 它通过引入一个回调,避免更多的回调

状态

Promise 有三个状态,
- Pending:初始状态,刚开始实例化时的状态
- fulfilled:操作成功,执行完毕,调用 resolve 时
- rejected:操作失败,执行完毕,调用 rejected 时

Promise 状态发生改变,就会触发 .then()里面的响应函数处理后续步骤。
Promise 状态一经改变就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
Alt text

缺点

任何事物都不是完美的,有优点就会有缺点,Promise 的缺点体现在以下几个方面
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

最简单的例子

console.log('Here we go!')
  new Promise(resolve => {
    setTimeout(() => {
      resolve('hello')
    }, 1000)
  }).then(value => {
    console.log(value + '!!')
  });

这段代码运行后得到的结果为:

Here we go!
hello!!

即使不懂 Promise 是什么以及怎么运行的,我们也可以从结果反向推导出它的运行方式。首先,Promise 的 resolve 函数会在一秒后产生一个值,这个值产生后会将它传递给 then 函数并将它打印出来。也可以理解为 then 函数接受了 resolve 函数的处理结果。

二步执行的例子

下面再来看一个分二步执行的例子

console.log('Here we go!')
  new Promise(resolve => {
    setTimeout(() => {
      resolve('hello')
    }, 1000)
  }).then(value => {
    console.log(value)
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(value)
      })
    });
  }).then(value => {
    console.log(value + '!!')
  });

上面的代码运行结果为:

Here we go!
hello
hello!!

同样通过结果反推可以知道,Promise 中 resolve 的结果先是传给了第一个 then,然后第一个 then 里面又实例化了一个 promise,并将刚才的结果再次 resolve ,第二个 then 中拿到的 value 其实是经过了第一个 then 处理之后的结果。当然,第一个 then 中也可以 resolve 另外一个结果出来,那样的话第二个 then 中拿到的 value 就不再是一开始的值了。说的有点绕,结合代码应该是很容易理解的。

对已完成的 Promise 执行 .then

考虑一种情况,当一个 Promise 执行完后,我们并不马上调用 then 处理结果,而是过一段时间或者是在一个合适的时机再去处理,这样的话能否拿到正确的数据呢。

console.log('Here we go!')
  let promise = new Promise(resolve => {
    setTimeout(()=>{
      console.log('Promise fulfilled')
      resolve('hello world')
    },1000)
  });

  setTimeout(()=>{
    promise.then(value => {
      console.log(value)
    });
  },2000)

运行结果

Here we go!
Promise fulfilled
hello world

可以看到,then 是在 Promise 执行完之后延迟一秒再执行的,还是拿到了正确的数据。

在 .then 函数中不返回新的 Promise ,会怎样

与前面二步调用的例子有些相似,当第一个 then 中不返回 Promise 的时候,他其实仍然会返回一个值,

console.log('Here we go!')
  new Promise(resolve => {
    setTimeout(() => {
      resolve('hello')
    }, 1000)
  }).then(value => {
    console.log(value)
    return false
  }).then(value => {
    console.log(value + '!!')
  });

上面的代码运行结果为:

Here we go!
Hello
false!!

可以看到,虽然没有返回 Promise,但是返回的 false 仍然被第二个 then 接收到了,如果在第一个 then 中不主动返回任何值,那么第二个 then 中接收到的 value 就变成了 undefind

.then()函数

  • .then() 接收二个函数作为参数,分别代表 fulfilled 和 rejected
  • .then() 返回一个新的 Promise 实例,所以它可以链式调用
  • 当前面的 Promise 状态改变时,.then()根据其最终状态,选择特定的状态响应函数执行
  • 状态响应函数可以返回新的 Promise,或其它值
  • 如果返回新的 Promise,那么下一级 .then() 会在新 Promise 状态改变之后执行
  • 如果返回其它任何值,则会立刻执行下一级 .then()

.then() 的嵌套,当 .then() 里面还有 .then() 的情况

在这种情况下,因为 .then() 返回的还是 Promise 实例,所以会等里面的 then 执行完之后再去执行外面的 then ,对于我们来说,此时最好将其展开,会让代码更易阅读,来看一段代码

console.log('start')
  new Promise(resolve => {
    console.log('step 1')
    setTimeout(() => {
      resolve(100)
    }, 1000)
  }).then(value => {
    return new Promise(resolve => {
      console.log('setp 1-1')
      resolve(110)
    }, 1000).then(value => {
      console.log('step 1-2')
      return value
    }).then(value => {
      console.log('step 1-3')
      return value
    })
  }).then(value => {
    console.log(value)
    console.log('step 2')
  })

执行结果为:

start
step 1
step 1-1
step 1-2
step 1-3
110
step 2

在上面的代码中,第一个 then 里面的 Promise 实例又有二个 then,通过结果我们可以看到,代码执行顺序是先将里层的二个 then 执行完毕之后再去执行外层的 then,是顺序执行的,就像个队列一样,而且值也是可以一直往下传递的。

基于上面的分析,而且我们知道 then 返回的还是一个 Promise 实例,所以我们可以把上面的代码改造下,让它变得更易阅读,也更符合人的思维习惯。

console.log('start')
  new Promise(resolve => {
    console.log('step 1')
    setTimeout(() => {
      resolve(100)
    }, 1000)
  })
    .then(value => {
      return new Promise(resolve => {
        console.log('setp 1-1')
        resolve(110)
      }, 1000)
    })
    .then(value => {
      console.log('step 1-2')
      return value
    })
    .then(value => {
      console.log('step 1-3')
      return value
    })
    .then(value => {
      console.log(value)
      console.log('step 2')
    })

可以看到,改造后的代码中,第一个 then 只是返回一个 Promise,中间的二步 then 被提取出来,这样四个 then 就变成了平级关系,阅读体验比第一种写法好了很多,运行结果也是一样的。

错误处理

Promise 会自动捕获内部异常,并交给 rejected 响应函数处理。这句话的意思是,在执行器中如果发生了错误(我们自己抛出或其他代码抛出错误),那么 Promise 的状态就会被改为 rejected,之后就会调用 rejected 函数进行处理,也会向后面去寻找 catch 的响应函数去进行处理。

console.log('start')
  new Promise(resolve => {
    setTimeout(() => {
      throw new Error('This is error')
    }, 1000)
  })
    .then(value => {
      console.log(value + '!!')
    })
    .catch(error => {
      console.log('error ==>', error.message)
    })

上面的代码是错误的第一种处理方法,还有另外一种方法也是可以处理错误的

console.log('start')
  new Promise((resolve,reject) => {
    setTimeout(() => {
      reject('This is error')
    }, 1000)
  })
    .then(value => {
      console.log(value + '!!')
    },err =>{
      console.log('Error:',err)
    })

一般来说,不要在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方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch方法,而不使用then方法的第二个参数。

.catch() + .then() 在catch 后面继续接 then 会怎样

比如说我们有一个很长的队列,每一步操作可能都会发生错误,在捕获错误进行处理之后,我们希望它能够继续执行或是怎样。

console.log('here we go');

new Promise(resolve => {
    setTimeout(() => {
        resolve();
    }, 1000);
})
    .then( () => {
        console.log('start');
        throw new Error('test error');
    })
    .catch( err => {
        console.log('I catch:', err);
        // 下面这一行的注释将引发不同的走向
        // throw new Error('another error');
    })
    .then( () => {
        console.log('arrive here');
    })
    .then( () => {
        console.log('... and here');
    })
    .catch( err => {
        console.log('No, I catch:', err);
    });

运行结果

here we go
start
I catch: Error: test error
    at eval (HelloWorld.vue?18db:25)
 arrive here
... and here

可以看到在第一个 catch 中捕获到了 error,然后它后面的二个 then 正常执行了,这是因为 catch 返回的其实也是一个 Promise,它在捕获到错误后,它的状态其实是 fulfilled,然后就会去顺序执行后面的 then,并且因为错误已经被第一个 catch 捕获完了,所以第二个 catch 是捕获不到错误的。

这时如果将第一个 catch 中注释的代码放开,也就是在第一个 catch 里面再抛出一个错误,运行结果就会变成下面这样

here we go
start
I catch: Error: test error
    at eval (HelloWorld.vue?18db:25)
No, I catch: Error: another error
    at eval (HelloWorld.vue?18db:31)

从结果可以看出,第一个 catch 抛出错误之后,它后面的二个 then 都不再执行了,而第二个 catch 捕获到了第一个 catch 中抛出的错误。

从上面的实验以及结合 Promise 和 catch 的特点,强烈建议在所有队列后面都加上 catch,这样当队列发生了错误的时候,我们就能将它捕获来进行处理。因为队列的执行全部是异步的,在队列生成的时候是没有问题的,在队列执行的过程中可能会发生一些意外情况,如果不做错误处理,可能会发生一些意想不到的问题。

finally

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

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

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

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

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

Promise.all()

Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
- 返回的实例就是普通 Promise,
- 它接受一个数组作为参数,
- 数组里可以是 Promise 对象,也可以是别的值,只有Promise 会等待状态改变。
- 当所有子 Promise 都完成,该 Promise 完成,返回值是全部值的数组
- 子 Promise 中有任何一个失败,该 Promise 失败,返回值是第一个失败的子 Promise 的结果

console.log('here we go')
  Promise.all([1, 2, 3])
         .then(all => {
           console.log('1:', all)
           return Promise.all([function () {
             console.log('ooxx')
           }, 'xxoo', false])
         })
         .then(all => {
           console.log('2:', all)
           let p1 = new Promise(resolve => {
             setTimeout(() => {
               resolve('I\'m P1')
             }, 1500)
           })
           let p2 = new Promise((resolve, reject) => {
             setTimeout(() => {
               resolve('I\'m P2')
             }, 1000)
           })
           return Promise.all([p1, p2])
         })
         .then(all => {
           console.log('3:', all)
           let p1 = new Promise(resolve => {
             setTimeout(() => {
               resolve('I\'m P1')
             }, 1500)
           })
           let p2 = new Promise((resolve, reject) => {
             setTimeout(() => {
               reject('I\'m P2')
             }, 1000)
           })
           let p3 = new Promise((resolve, reject) => {
             setTimeout(() => {
               reject('I\'m P3')
             }, 2000)
           })
           return Promise.all([p1, p2, p3])
         })
         .then(all => {
           console.log('all', all)
         })
         .catch(err => {
           console.log('Catch:', err)
         })

上面代码的运行结果为:

here we go
1: (3) [1, 2, 3]
2: (3) [ƒ, "xxoo", false]
3: (2) ["I'm P1", "I'm P2"]
Catch: I'm P2

从结果反推可以很容易的验证前面说的 Promise.all() 的几个特点

Promise.all() 最常见的就是和 .map() 结合使用

Promise 实现队列

有时候我们不希望所有动作一起发生,而是按照一定顺序,逐个进行,那么可以使用 then 函数返回 Promise 实例这个特性

let promise = doSomething()
  promise = promise.then(doSomethingElse)
  promise = promise.then(doSomethingElse2)
  promise = promise.then(doSomethingElse3)
  ...

实现队列有二种方式
Alt text
Alt text
Alt text
Alt text

Promise.resolve()

Promise.resolve() 返回一个 fulfilled 状态的 Promise 实例,或原始 Promise 实例
- 参数为空,返回一个状态为 fulfilled 的 Promise 实例
- 参数是一个跟 Promise 无关的值,同样返回一个 fulfilled 的 Promise 实例,不过 fulfilled 响应函数会得到这个参数
- 参数为 Promise 实例,则返回该实例,不做任何修改
- 参数为 thenable,立刻执行它的 then 函数

console.log('start');

Promise.resolve()
    .then( (value) => {
        console.log('Step 1',value);
        return Promise.resolve('Hello');
    })
    .then( value => {
        console.log(value, 'World');
        return Promise.resolve(new Promise( resolve => {
            setTimeout(() => {
                resolve('Good');
            }, 2000);
        }));
    })
    .then( value => {
        console.log(value, ' evening');
        return Promise.resolve({
            then() {
                console.log(', everyone');
            }
        })
    })

执行结果为:

start
Step 1 undefind
Hello World
Good evening
, everyone

Promise.rejected()

与 Promise.resolve() 类似,不同的是它返回的是一个状态为 rejected 的 Promise 实例,它的应用场景比较有限,与 Promise.resolve() 不同的地方在于它不认 thenable,其他的都类似

Promise.race()

与 Promise.all() 类似,区别在于它里面只要有任意一个 Promise 完成就算是完成。

console.log('start');
let p1 = new Promise(resolve => {
    // 这是一个长时间的调用
    setTimeout(() => {
        resolve('I\'m P1');
    }, 10000);
});
let p2 = new Promise(resolve => {
    // 这是个稍短的调用
    setTimeout(() => {
        resolve('I\'m P2');
    }, 2000)
});
Promise.race([p1, p2])
    .then(value => {
        console.log(value);
    });

运行结果

start
I'm P2

这个方法的常见用法是将异步操作和一个定时器绑在一起,如果定时器先触发了,就认为超时,然后告知用户

Promise 在实际开发中的运用,把回调包装成 Promise

把回调包装成 Promise 有二个显而易见的好处
- 可读性更好,不用再把回调嵌套很多层
- 所有返回的结果会生成一个队列,我们可以把这个队列放在其他地方使用

以上,就是这次 Promise 学习的成果,记录下来以加深印象。

猜你喜欢

转载自blog.csdn.net/zgh0711/article/details/80764582