Promise学习心得

为什么要使用Promise

Promise是一个代表了异步操作最终完成或者失败的对象

  1. 主要解决回调地狱
    在这里插入图片描述
    先看一个对比例子:
//回调地狱代码:
doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log('Got the final result: ' + finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

//promise方案:
doSomething()
.then(result => doSomethingElse(value))
.then(newResult => doThirdThing(newResult))
.then(finalResult => console.log(`Got the final result: ${finalResult}`))
.catch(failureCallback);

//async/await方案:
async function foo() {
  try {
    let result = await doSomething();
    let newResult = await doSomethingElse(result);
    let finalResult = await doThirdThing(newResult);
    console.log(`Got the final result: ${finalResult}`);
  } catch(error) {
    failureCallback(error);
  }
}
  1. 有助于以一种有效的方式对异步进行分组
  2. Promises/A+规范早已经有其他语言实现
  3. 链式调用

回调和promise方式区别

  1. 回调需要立即调用方法,而Promise可以作为Promise对象返回并在以后调用。
  2. 在Callback中,我们必须显式地处理错误
  3. 在Callback中,我们必须调用传递的回调函数来传递错误

第三方实现的Promise库

什么是Promise

Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象

Promise状态:

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。

状态变化示意图

在这里插入图片描述

Promise对象有以下两个特点

  1. 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

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

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

语法

new Promise( function(resolve, reject) {...} /* executor */  );

参数

executor

executor是带有 resolve 和 reject 两个参数的函数 。Promise构造函数执行时立即调用executor 函数, resolve 和 reject 两个函数作为参数传递给executor(executor 函数在Promise构造函数返回新建对象前被调用)。resolve 和 reject 函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。executor 内部通常会执行一些异步操作,一旦完成,可以调用resolve函数来将promise状态改成fulfilled,或者在发生错误时将它的状态改为rejected。
如果在executor函数中抛出一个错误,那么该promise 状态为rejected。executor函数的返回值被忽略。

方法
Promise.all(iterable)

Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败。这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常被用于处理多个promise对象的状态集合。

Promise.race(iterable)

当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象。

Promise.reject(reason)

返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法

Promise.resolve(value)

返回一个状态由给定value决定的Promise对象。如果该值是一个Promise对象,则直接返回该对象;如果该值是thenable(即,带有then方法的对象),返回的Promise对象的最终状态由then方法执行决定;否则的话(该value为空,基本类型或者不带then方法的对象),返回的Promise对象状态为fulfilled,并且将该value传递给对应的then方法。通常而言,如果你不知道一个值是否是Promise对象,使用Promise.resolve(value) 来返回一个Promise对象,这样就能将该value以Promise对象形式使用。

Promise 原型上的方法
Promise.prototype.catch(onRejected)

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

Promise.prototype.then(onFulfilled, onRejected)

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

Promise.prototype.finally(onFinally) chrome 63+

添加一个事件处理回调于当前promise对象,并且在原promise对象解析完毕后,返回一个新的promise对象。回调会在当前promise运行完毕后被调用,无论当前promise的状态是完成(fulfilled)还是失败(rejected)
finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

手动实现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 })
  );
};

从上面的实现还可以看到,finally方法总是会返回原来的值。

如何处理错误

处理链式调用的错误有两种方式

'use strict';
// 第一种方式
yourPromise.catch(function (error) {
 // Your Error Callback
});
// 第二种方式
yourPromise.then(undefined, function (error) {
 // Your Error Callback
});
get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})
//等价于
get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

如果方法体中出现语法错误或者调用Promise.reject(Err),程序会延调用链向下找到 .catch或者then方法中onRejection callback。执行错误处理。

代码示例1
代码示例2

错误处理顺序

错误顺序取决于.catch或者then方法中onRejection callback的定义顺序。

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

以上代码的执行流程如图:
在这里插入图片描述

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 会吃掉错误”。

这个脚本放在服务器执行,退出码就是0(即表示执行成功)。不过,Node 有一个unhandledRejection事件,专门监听未捕获的reject错误,上面的脚本会触发这个事件的监听函数,可以在监听函数里面抛出错误。

process.on('unhandledRejection', function (err, p) {
  throw err;
});

上面代码中,unhandledRejection事件的监听函数有两个参数,第一个是错误对象,第二个是报错的 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

上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。

如何将多个promise组合成一个

通过Promise.all方法将多个promise组合成一个

'use strict';
// Usage
Promise.all([Promise1, Promise2,]).then(onFulfilled, onRejected);
// Example
var promiseCall = function (waitSecond, returnData) {
    return function (resolve, reject) {
        setTimeout(resolve, waitSecond, returnData);
    };
};
var p1 = new Promise(promiseCall(1000, "one"));
var p2 = new Promise(promiseCall(2000, "two"));
var p3 = new Promise(promiseCall(3000, "three"));
var p4 = new Promise(promiseCall(4000, "four"));
var p5 = new Promise(function (resolve, reject) {
    reject('5th Promise Rejected');
});
// Calling Promise 1 - 4 in Promise.all()
Promise.all([p1, p2, p3, p4]).then(function (value) {
    console.log(value);
}, function (reason) {
    // Not Called
    console.log(reason);
});
// Expected Output: ["one", "two", "three", "four"]
// Calling Promise 1 - 5 in Promise.all()
Promise.all([p1, p2, p3, p4, p5]).then(function (value) {
    // Not Called
    console.log(value);
}, function (reason) {
    console.log(reason);
});
// Trigger Rejection (Second) Callback if any one is rejected.
// Expected Output: 5th Promise Rejected

代码示例

Promise.race(iterable)方法使用

根据上边方法介绍,race是比赛的意思,当iterable参数里的任意一个子promise改变状态后就进入then方法。

'use strict';
var p1 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 500, "one"); 
});
var p2 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 100, "two"); 
});

Promise.race([p1, p2]).then(function(value) {
  console.log(value); // "two"
  // Both resolve, but p2 is faster
});

代码示例

当Promise.all方法中的iterable为非迭代器时返回什么?

当iterable的值为空、undefined、object、number、boolean时程序会报异常并将错误信息在catch中捕获,或者在onRejection callback中捕获

TypeError: XXXX is not iterable (cannot read property Symbol(Symbol.iterator))

当iterable的值为字符串时为当作字符串数组

var invalidPromise7 = Promise.all('test');
// Trigger FulFillment Callback of then Method.
// Then Method Return: ["t", "e", "s", "t"]

当iterable的值为空字符串或者空数组时触发FulFillment Callback返回空数组

var invalidPromise8 = Promise.all('');
var invalidPromise9 = Promise.all([]);
// Trigger FulFillment Callback of then Method.
// Then Method Return: []

代码示例

当Promise.race方法中的iterable为非迭代器时返回什么?

当iterable的值为空、undefined、object、number、boolean时程序会报异常并将错误信息在catch中捕获,或者在onRejection callback中捕获

TypeError: XXXX is not iterable (cannot read property Symbol(Symbol.iterator))

当iterable的值为字符串时为当作字符串数组

var invalidPromise7 = Promise.race('test');
// Trigger FulFillment Callback of then Method.
// Then Method Return: t

当iterable的值为空字符串或者空数组时由于值为undefined因此不触发回调

var invalidPromise8 = Promise.race('');
varinvalidPromise9 = Promise.race([]);
// Promise value is undefined and trigger no callback.

代码示例

Promise到达fulfilled状态的方式

var responseValue = ‘Success’;
// First Approach
var directPromiseValue = Promise.resolve(responseValue);
// Second Approach
var instantiatePromise = new Promise(function (resolve) {
 resolve(value);
});

如何将硬编码值变为Promise

var directPromiseValue = Promise.resolve('Success');
var directPromiseValue = Promise.reject('Custom Error Object');

代码示例

并行

Promise.all()Promise.race()是并行运行异步操作的两个组合式工具
时序组合可以使用一些优雅的javascript形式

[func1, func2].reduce((p, f) => p.then(f), Promise.resolve());
//等价于
Promise.resolve().then(func1).then(func2);

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

//通过Promise实现时序组合
let applyAsync = (acc,val) => acc.then(val);
let composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));

let transformData = composeAsync(func1, asyncFunc1, asyncFunc2, func2);
transformData(data);

//通过async/await实现时序组合
for (let f of [func1, func2]) {
  await f();
}

时序

Promise对象的回调函数会进入异步任务里边的“微任务”队列

Promise.resolve().then(() => console.log(2));
console.log(1); // 1, 2

完整的事件循环规定见另一篇文章学习Node定时器详解

并行式和顺序式:两者兼得

要实现的功能,获取一个story.json,根据数据中的story.chapterUrls获取每个章节然后添加到页面中
以一个同步获取数据的例子展示逻辑

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'

改成异步方式加载各个章节
方法1:一章一章顺序加载

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

效果图:
在这里插入图片描述

方法2:所有章节全部加载完在进行渲染

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

效果图:
在这里插入图片描述

方法3:第一章下载完后,我们可将其添加到页面。这可让用户在其他章节下载完毕前先开始阅读。第三章下载完后,我们不将其添加到页面,因为还缺少第二章。第二章下载完后,我们可添加第二章和第三章,后面章节也是如此添加。

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence.then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

效果图:
在这里插入图片描述

常见问题

// 错误示例,包含 3 个问题

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

修改后

doSomething()
.then(function(result) {
  return doSomethingElse(result);
})
.then(newResult => doThirdThing(newResult))
.then(() => doFourthThing());
.catch(error => console.log(error));

相关链接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值