【阮一峰Promise】案例+解读

 

https://github.com/mqyqingfeng/Blog/issues/98

该文章含读程序及promise的相关介绍:https://www.cnblogs.com/lunlunshiwo/p/8852984.html

首先,Promise是一个对象,如同其字面意思一样,代表了未来某时间才会知道结果的时间,不受外界因素的印象。

Promise一旦触发,其状态只能变为fulfilled或者rejected,并且已经改变不可逆转。

Promise的构造函数接受一个函数作为参数,该参数函数的两个参数分别为resolve和reject,其作用分别是将Promise的状态由pending转化为fulfilled或者rejected,并且将成功或者失败的返回值传递出去。then有两个函数作为Promise状态改变时的回调函数,当Promise状态改变时接受传递来的参数并调用相应的函数。then中的回调的过程为异步操作。

catch方法是对.then(null,rejectFn)的封装(语法糖),用于指定发生错误时的回掉函数。一般来说,建议不要再then中定义rejected状态的回调函数,应该使用catch方法代替。

all和race都是竞速函数,all结束的时间取决于最慢的那个,其作为参数的Promise函数一旦有一个状态为rejected,则总的Promise的状态就为rejected;而race结束的时间取决于最快的那个,一旦最快的那个Promise状态发生改变,那个其总的Promise的状态就变成相应的状态,其余的参数Promise还是会继续进行的。

1.回调地狱引发的问题:

(1) 难以复用:回调的顺序确定下来之后,想对其中的某些环节进行复用也很困难,牵一发而动全身。

(2)堆栈信息被断开:

我们知道,JavaScript 引擎维护了一个执行上下文栈,当函数执行的时候,会创建该函数的执行上下文压入栈中,当函数执行完毕后,会将该执行上下文出栈。

如果 A 函数中调用了 B 函数,JavaScript 会先将 A 函数的执行上下文压入栈中,再将 B 函数的执行上下文压入栈中,当 B 函数执行完毕,将 B 函数执行上下文出栈,当 A 函数执行完毕后,将 A 函数执行上下文出栈。

这样的好处在于,我们如果中断代码执行,可以检索完整的堆栈信息,从中获取任何我们想获取的信息。

可是异步回调函数并非如此,比如执行 fs.readdir 的时候,其实是将回调函数加入任务队列中,代码继续执行,直至主线程完成后,才会从任务队列中选择已经完成的任务,并将其加入栈中,此时栈中只有这一个执行上下文,如果回调报错,也无法获取调用该异步操作时的栈中的信息,不容易判定哪里出现了错误。

此外,因为是异步的缘故,使用 try catch 语句也无法直接捕获错误。

(不过 Promise 并没有解决这个问题)

(3)借助外层变量

当多个异步计算同时进行,比如这里遍历读取文件信息,由于无法预期完成顺序,必须借助外层作用域的变量,比如这里的 count、errored、stats 等,不仅写起来麻烦,而且如果你忽略了文件读取错误时的情况,不记录错误状态,就会接着读取其他文件,造成无谓的浪费。此外外层的变量,也可能被其它同一作用域的函数访问并且修改,容易造成误操作。

之所以单独讲讲回调地狱,其实是想说嵌套和缩进只是回调地狱的一个梗而已,它导致的问题远非嵌套导致的可读性降低而已。

2.Promise 的局限性

(1)错误被吃掉

首先我们要理解,什么是错误被吃掉,是指错误信息不被打印吗?

并不是,举个例子:

throw new Error('error');
console.log(233333);

在这种情况下,因为 throw error 的缘故,代码被阻断执行,并不会打印 233333,再举个例子:

const promise = new Promise(null);
console.log(233333);

以上代码依然会被阻断执行,这是因为如果通过无效的方式使用 Promise,并且出现了一个错误阻碍了正常 Promise 的构造,结果会得到一个立刻跑出的异常,而不是一个被拒绝的 Promise。

然而再举个例子:

let promise = new Promise(() => {
    throw new Error('error')
});
console.log(2333333);

这次会正常的打印 233333,说明 Promise 内部的错误不会影响到 Promise 外部的代码,而这种情况我们就通常称为 “吃掉错误”。

其实这并不是 Promise 独有的局限性,try..catch 也是这样,同样会捕获一个异常并简单的吃掉错误。

而正是因为错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为什么会一般推荐在 Promise 链的最后添加一个 catch 函数,因为对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。

(2)单一值

Promise 只能有一个完成值或一个拒绝原因,然而在真实使用的时候,往往需要传递多个值,一般做法都是构造一个对象或数组,然后再传递,then 中获得这个值后,又会进行取值赋值的操作,每次封装和解封都无疑让代码变得笨重。

说真的,并没有什么好的方法,建议是使用 ES6 的解构赋值:

Promise.all([Promise.resolve(1), Promise.resolve(2)])
.then(([x, y]) => {
    console.log(x, y);
});

(3)无法取消:Promise 一旦新建它就会立即执行,无法中途取消。

(4)无法得知pending状态:当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

3.promise.race

promise.race()方法也可以处理一个promise实例数组但它和promise.all()不同,从字面意思上理解就是竞速,那么理解起来上就简单多了,也就是说在数组中的元素实例那个率先改变状态,就向下传递谁的状态和异步结果。但是,其余的还是会继续进行的。

let p1 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('1s') //1s后输出
    resolve(1)
  },1000)
})
let p10 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('10s') //10s后输出
    resolve(10) //不传递
  },10000)
})
let p5 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('5s') //5s后输出
    resolve(5) //不传递
  },5000)
})
Promise.race([p1, p10, p5]).then((res)=>{
    console.log(res); // 最后输出
})

上述代码输出结果:1s 1 5s 10s

4.红绿灯问题题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)

function red(){
    console.log('red');
}
function green(){
    console.log('green');
}
function yellow(){
    console.log('yellow');
}

var light = function(timmer, cb){
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            cb();
            resolve();
        }, timmer);
    });
};

var step = function() {
    Promise.resolve().then(function(){
        return light(3000, red);
    }).then(function(){
        return light(2000, green);
    }).then(function(){
        return light(1000, yellow);
    }).then(function(){
        step();
    });
}

step();
// const promise = new Promise((resolve, reject) => {
//   // promise.then()
//   if (1) {
//     resolve(value)
//   } else {
//     reject(error)
//   }
//   promise.then((function(value){

//   }, function(error) {

//   }))
// })


// function timeout(ms) {
//   return new Promise((resolve, reject) => {
//     setTimeout(resolve, ms, 'done')
//   })
// }
// timeout(100).then((value) => {
//   console.log(value)
// })


// let promise = new Promise(function(resolve, reject) {
//   console.log('Promise');
//   resolve(1);
// });
// promise.then(function(value) {
//   console.log(value);
// });
// console.log('Hi!');

// new Promise((resolve, reject) => {
//   resolve(1);
//   console.log(2);
// }).then(r => {
//   console.log(r);
// });
// 上面代码中,调用resolve(1)以后,后面的console.log(2)还是会执行,并且会首先打印出来。
// 这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。


// 一般来说,调用resolve或reject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,
// 而不应该直接写在resolve或reject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。
// new Promise((resolve, reject) => {
//   return reject(1);
//   console.log(2);
// }).then(r => {
//   console.log(r);
// }).catch(error => {
//   console.log(error + '+++')
// })
// console.log(3)  



// getJSON("/post/1.json").then(function(post) {
//   return getJSON(post.commentURL);
// }).then(function (comments) {
//   console.log("resolved: ", comments);
// }, function (err){
//   console.log("rejected: ", err);
// });


// 如果 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 的状态一旦改变,就永久保持该状态,不会再变了。


// 代码运行完catch方法指定的回调函数,会接着运行后面那个then方法指定的回调函数。
// 如果没有报错,则会跳过catch方法。
// var x = 1
// 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方法。
// 此时,要是then方法里面报错,就与前面的catch无关了。
// Promise.resolve()
// .catch(function(error) {
//   console.log('oh no', error);
// })
// .then(function() {
//   console.log('carry on');
// });
// // carry on


// // 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方法抛出的错误。
// 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;
// }).catch(function(error) {
//   console.log('carry on', error);
// });
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]


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



// 上面代码中,p1会resolved,p2首先会rejected,但是p2有自己的catch方法,
// 该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。
// 该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,
// 因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。
// 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);

// Promise.all([p1, p2])
// .then(result => console.log(result))
// .catch(e => console.log(e));
// // ["hello", Error: 报错了]


// 如果p2没有自己的catch方法,就会调用Promise.all()的catch方法。
// 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: 报错了


// 需要注意的是,立即resolve()的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,
// 而不是在下一轮“事件循环”的开始时。
// 上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,
// Promise.resolve()在本轮“事件循环”结束时执行,
// console.log('one')则是立即执行,因此最先输出。
// setTimeout(function () {
//   console.log('three');
// }, 0);

// Promise.resolve().then(function () {
//   console.log('two');
// });

// console.log('one');

// one
// two
// three


// // const p = Promise.reject('出错了');
// // 等同于
// const p = new Promise((resolve, reject) => reject('出错了'))

// p.then(null, function (s) {
//   console.log(s)
// });
// // 出错了
// 上面代码生成一个 Promise 对象的实例p,状态为rejected,回调函数会立即执行。



// 注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。
// 这一点与Promise.resolve方法不一致。
// Promise.reject方法的参数是一个thenable对象,执行以后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,
// 而是thenable对象。
// const thenable = {
//   then(resolve, reject) {
//     reject('出错了');
//   }
// };
// Promise.reject(thenable)
// .catch(e => {
//   console.log(thenable) // { then: [Function: then] }
//   console.log(e === thenable)
// })
// // true


// 应用
// (1)加载图片:我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。
const preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};

 

你可能感兴趣的:(js)