资讯专栏INFORMATION COLUMN

【笔记】你不知道的JS读书笔记——Promise

mumumu / 862人阅读

摘要:写在前面这一章的顺序对于未接触过使用过的童鞋而言略抽象了,前边几章主要为了说明和之前的异步方式相比有什么优势和它能解决什么问题,后边才详解的设计和各种场景下如何使用。建议先了解和简单使用过后再阅读,效果更佳。

写在前面:
Promise这一章的顺序对于未接触过使用过Promise的童鞋而言略抽象了,前边几章主要为了说明Promise和之前的异步方式相比有什么优势和它能解决什么问题,后边才详解Promise的API设计和各种场景下如何使用Promise。

建议先了解和简单使用过Promise后再阅读,效果更佳。

正文

3.1 什么是Promise

之前的方式:

利用回调函数封装程序中的continuation

回调交给第三方

第三方调用回调

实现正确功能

Promise方式:
第三方提供了解其任务何时结束的能力

Promise的异步特性是基于任务的(图示如下)

一种处理异步的思路:为了统一现在和将来,把它们都变成将来,即所有操作都成了异步的

书中关于Promise是个啥的观点:

</>复制代码

  1. 一种封装和组合未来值的易于复用的机制
    一种在异步任务中作为两个或更多步骤的流程控制机制,时序上的this-then-that —— 关注点分离

Promise设计的重要基础

</>复制代码

  1. Promise一定是异步执行的,即使是立即完成的Promise(类似 new Promise((resolve)=>{ resolve(42) })),也无法被同步观察到

  2. 一旦Promise决议,它就永远保持在这个状态,变成了不变值(immuatable value,这是设计中最基础和最重要的因素

  3. Promise至多只能有一个决议值(一个!一个!一个!)

引申:

Promise的决议结果可以给多方多次查看

安全、可靠

3.2 Promise的检测

基于thenable的鸭子类型

</>复制代码

  1. if(
  2. p !== null &&
  3. (
  4. typeof p === "object" ||
  5. typeof p === "function"
  6. ) &&
  7. typeof p.then === "function"
  8. ) {
  9. // 假定这是一个thenable
  10. }
  11. else {
  12. // 不是thenable
  13. }

这种方式显然是有些问题的,但是目前通用的方式

3.3 Promise如何解决信任问题

信任问题见 异步篇

3.3.1 调用过早

避免Zalgo这类副作用:一个任务有时同步完成,有时异步完成,可能导致竞态条件

Promise从定义上保证了不会存在这种问题:参考3.1 设计基础 — 即使是立即完成的Promise,也无法被同步观察到

3.3.2 调用过晚

Note: 调用过晚强调的是调用顺序?

Promise创建对象调用resolve(..)或reject(..)时,这个Promise的then注册的观察回调就会自动调度(注意是被调度而不是执行) —— 在下一个异步时机点上依次被调用执行,它们相互之间是不会互相影响或延误的

3.3.3 回调未调用

Promise一旦决议则一定会通知决议(传入then的完成回调或拒绝回调调用),即使是Javascript运行错误也会调用拒绝回调

如果某个Promise一直不决议呢?使用竞态的高级抽象机制:

</>复制代码

  1. // 超时工具
  2. function timeoutPromise(delay){
  3. return new Promise( (resolve, reject) => {
  4. setTimeout( function () {
  5. reject("Timeout!");
  6. }, delay);
  7. } )
  8. }
  9. // 设置某个Promise foo()超时
  10. Promise.race( [
  11. foo(),
  12. timeoutPromise(3000)
  13. ] )
  14. .then(
  15. function () {
  16. // foo(..)及时完成
  17. },
  18. function (err) {
  19. // foo(..)被拒绝或者超时
  20. // 通过查看err确定错误情况
  21. }
  22. );
3.3.4 调用次数过少或过多

如果创建Promise的代码试图多次调用resolve(..)或reject(..),或者两者都调用,Promise只会接受第一次决议,后续调用都会被忽略

3.3.5 未能传递参数/环境值

Promise至多只能有一个决议值

如果使用多个参数调用resolve(..)或reject(..),第一个参数之后的所有参数都会被忽略

Promise其实也是传入回调函数,故函数中照样能根据作用域规则访问到对应的环境数据

3.3.6 吞掉错误或异常

这里说的错误或异常可能出现在两个过程:

Promise创建过程或其决议确认之前的任何时间点上(注:书中原文查看其决议结果过程中任何时间点,个人认为可能翻译得有点问题,应该要强调是其决议之前)

Promise决议确认后在查看结果时(then(..)注册的回调中)出现了js异常错误

这两种错误都不会被丢弃,但针对它们的处理方式有所不同:

针对1:
该Promise会被立即拒绝,但注意这个异常也被变成了异步行为

</>复制代码

  1. let p = new Promise ( function(resolve, reject){
  2. foo.bar(); // foo undefined 将抛出错误 Promise=>reject
  3. resolve( 42 ); // 不会执行到这里
  4. });
  5. p.then(
  6. function fulfilled(){
  7. // 不会执行到这里
  8. },
  9. function rejected(err){
  10. // err是一个TypeError异常
  11. }
  12. )

针对2:
这个时候当前Promise已经决议,其决议结果是个不可变值
then(..)调用返回的下一个Promise被拒绝

</>复制代码

  1. let q = new Promise ( function(resolve, reject){
  2. resolve( 42 );
  3. })
  4. q.then(
  5. function fulfilled(){
  6. foo.bar(); // foo undefined 将抛出错误 导致then返回的Promise被reject
  7. },
  8. function rejected(err){
  9. // 不会执行到这里
  10. }
  11. ).then(
  12. function fulfilled(){
  13. // 不会执行到这里
  14. },
  15. function rejected(err){
  16. // err是一个TypeError异常
  17. }
  18. )
3.3.7 构建可信任的Promise

Promise.resolve(..) 规范化传入的值:

传入一个非Promise、非thenable的立即值, 会得到一个用该值填充的Promise

传入一个真正的Promise,会返回同一个Promise

传入一个非Promise的thenable值,会试图展开这个值,持续到提取出一个具体的非类Promise的最终值

具体看例子(传入Promise的情况略)

</>复制代码

  1. // 传入一个立即值
  2. let p = Promise.resolve(42);
  3. p.then( res => {
  4. console.log("Promise.resolve(42).then:",res);
  5. })
  6. let p1 = Promise.resolve({});
  7. p1.then( res => {
  8. console.log("Promise.resolve({}).then:",res);
  9. })
  10. // 传入一个 thenable 尝试展开
  11. let p2 = Promise.resolve({
  12. then: function(cb) { cb(42)}
  13. });
  14. p2.then( res => {
  15. console.log("Promise.resolve(thenable).then:", res);
  16. }, err => {
  17. console.log("Promise.resolve(thenable).then:", err);
  18. })
  19. // 注意 这种情况其实也是立即值!!!
  20. let p3 = Promise.resolve(
  21. setTimeout(()=>{
  22. return "inside a continuation"
  23. },1000)
  24. ); // settimeout函数返回当前定时器引用=>耶 立即值
  25. p3.then( res => {
  26. console.log("Promise.resolve(看起来是个异步).then:", res);
  27. })
3.4 Promise链式流

Promise不仅仅是一个单步执行this-then-that的操作机制,这只是它的构成部件,实际上Promise是可以连接到一起使用表示一系列异步步骤:

每次对Promise调用then(..),它都会创建并返回一个新的Promise,我们可以将其链接起来;(并不局限于要求then中返回一个Promise)

不管从then(..)调用的完成回调(第一个参数)返回的值是什么,它都会被自动设置为被链接Promise(上一点中的)的完成(resolve)(一定要理解这句话)

再仔细看看第二点,结合上文 3.3.7 Promise.resolve(..)的能力,这是Promise链式流在每一步都能有异步能力的关键!

栗子:

</>复制代码

  1. // 返回立即值
  2. let p = Promise.resolve(21);
  3. p
  4. .then( function(v) {
  5. console.log(v); // 21
  6. // 返回立即值
  7. return v * 2;
  8. })
  9. // 这里是链接的Promise
  10. .then ( function(v) {
  11. console.log(v); // 42
  12. });

</>复制代码

  1. // 返回Promise并引入异步
  2. let p = Promise.resolve(21);
  3. p
  4. .then ( function(v) {
  5. // 返回一个异步Promise
  6. return new Promise( (resolve, reject) => {
  7. setTimeout(() => {
  8. resolve(v*2);
  9. }, 1000);
  10. });
  11. })
  12. .then ( function(v) {
  13. // 前一步延迟1s后执行
  14. console.log(v);
  15. })

Promise链不仅仅是一个表达多步异步序列的流程控制,还可以从一个步骤到下一个步骤的消息通道

3.5 错误处理

几种错误处理方式:

try...catch结构不能应用于异步模式

</>复制代码

  1. function foo() {
  2. setTimeout(() => {
  3. baz.bar(); // 错误代码
  4. }, 100);
  5. }
  6. try{
  7. foo(); // 之后将抛出全局错误
  8. }
  9. catch (err) {
  10. // 不会走到这里
  11. }

foo()中有自己的异步完成函数,其中任何异步错误都无法捕捉到

node.js api或库中常见的err-first模式

</>复制代码

  1. function foo(cb) {
  2. setTimeout(() => {
  3. try {
  4. var x = baz.bar(); // 错误代码
  5. cb(null, x);
  6. }
  7. catch (err) {
  8. cb(err);
  9. }
  10. }, 100);
  11. }
  12. foo( function(err, val) {
  13. if(err) {
  14. console.error(err); // 报错惹
  15. }
  16. else {
  17. console.log(val);
  18. }
  19. })

分离回调模式(split-callback)
一个回调用于完成情况,一个回调用于拒绝情况
Promise采用的就是这种方式

先参考 3.3.6 再进行详细讨论:

Promise决议前、决议后产生的错误处理方式有所不同
错误的使用Promise API产生的错误会阻碍正常Promise对象的构造,这种情况下会立即抛出异常(这种情况应该死都不要出现 0 0)

3.5.1 绝望的陷阱

由于Promise链式特点,其链上的最后一步,不管是什么,总是存在着在未被查看的Promise中出现未捕获错误的可能性

即理论上来说:总有可能有错误未被捕获,而出现全局报错

P.S. 这也是个人认为使用Promise最头疼的一点

3.5.2 处理未捕获的情况

关于如何解决3.5.1提出问题的一些思路

增加done(..)作为链式调用的终点,在其中可以查看未捕获的错误,并且不会创建和返回新的Promise

依靠浏览器 追踪Promise对象在被垃圾回收时是否有拒绝(未捕获的错误),获得其报告 (什么功能?@TODO),可是如果Promise未被垃圾回收呢?

3.5.2 成功的坑

该小节讨论的是从作者角度提出一种避免在使用Promise时在开发者未注意的情况下出现未捕获错误而报出全局错误的方案

具体请看:

</>复制代码

  1. {
  2. let p = Promise.reject(21); // 将触发全局报错 Uncaught (in promise) 21
  3. let p1 = Promise.reject(21).then ( // 拒绝前,注册了一个错误处理函数
  4. (res) => {
  5. // 不会走到这里
  6. },
  7. (err) => {
  8. console.log(`注册了一个错误处理函数:${err}`);
  9. }
  10. )
  11. Promise.prototype.defer = function (){
  12. // 作者提出的一个API
  13. // 简单实现就是单纯的返回这个Promise本身
  14. return this;
  15. }
  16. let p2 = Promise.reject(21).defer(); // p2的结果在将来会被查看,现在暂时不要报全局错误
  17. let foo = Promise.resolve(21);
  18. foo
  19. .then (function(v) {
  20. return p2; // 这里查看p2的结果
  21. }, function (err) {
  22. // 不会走到这里
  23. })
  24. .catch (function(v) {
  25. console.log(v); // p2的结果
  26. })
  27. }
3.6 Promise模式

基于Promise构建的异步抽象模式

3.6.1 Promise.all([ .. ])

类似门(gate)这种机制:需要等待两个或更多并行/并发的任务都完成才能继续,它们的完成顺序并不重要,但必须都要完成,门才能打开并让流程控制继续

Promise.all([ .. ])的参数接收一个数组:

数组中的每个值都会交给Promise.resolve(..) 过滤以保证传入值是一个真正的Promise (Promise.resolve(..)的作用参考 3.3.7 构建可信任的Promise

数组为空,主promise就会立即完成

返回一个Promise:

传入的所有promise完成,该promise标记完成,返回消息是一个由所有传入promise的完成消息组成的数组,与调用API时传入的顺序一致(与完成顺序无关)

如果传入的promise中有任何一个被拒绝的话,该promise会立即被拒绝,并丢弃来自其他所有promise的全部结果(其他promise还是会执行),返回错误消息是被拒绝的那个promise的错误消息(注意,promise一旦决议结果不会变更,故仅有第一个被拒绝的promise错误消息会被主promise返回)

每个promise都必须关联一个拒绝/错误处理函数,特别是从Promise.all([ ... ])返回的那一个

3.6.2 Promise.race([ ... ])

类似门闩(shuan)竞态:一旦有任何一个Promise决议为完成,就标记为完成;一旦有任何一个Promise决议为拒绝,它就会拒绝

Promise.race([ ... ])的参数接收一个数组:

被Promise.resolve(...)过滤那是当然的

传入立即值没有任何意义,肯定是第一个立即值取胜

如果传入一个空数组,会导致该Promise永远不会决议!千万不要这么做

返回一个Promise:

和Promise.all([ ... ])不同,返回消息不是一个数组,因为只能接收一个promise的完成消息

关于这两个API需要注意

在all和race中存在着被忽略或丢弃的promise,如果这些promise中保存着重要的数据或资源或者开发者需要记录这些promise失败的事实,又该怎么办呢?

finally API就是基于这种情况提出的:Promise需要一个finally(...)回调注册,这个回调在Promise决议后总是会被调用,并允许执行任何必要的清理工作

注:书中提到finally还未被规范支持,而在18年1月已经正式加入到提案中了,可参考 https://github.com/tc39/propo... 和 https://github.com/tc39/propo...

书中还提到了一种观察模式(基于同一个Promise决议可以被多次查看),具体可以看栗子

</>复制代码

  1. let foo = new Promise((resolve, reject) => {
  2. setTimeout(() => {
  3. resolve(21);
  4. }, 301);
  5. });
  6. let timeout = function(time) {
  7. return new Promise((resolve, reject) => {
  8. setTimeout(() => {
  9. resolve("timeout");
  10. }, time);
  11. })
  12. }
  13. // foo会被默默忽略
  14. Promise.race( [
  15. foo,
  16. timeout(300)
  17. ])
  18. .then( (res) => {
  19. console.log(`Promise.race: ${res}`);
  20. })
  21. .finally( (res) => {
  22. console.log(`Promise.race: ${res}`); // finally回调是不会提供任何参数的,详情可看 https://github.com/tc39/proposal-promise-finally
  23. })
  24. // 观察者模式
  25. if(!Promise.observe){
  26. Promise.observe = function(pr, cb){
  27. // 观察pr的决议
  28. pr.then(
  29. function fulfilled (msg){
  30. // 完成时
  31. Promise.resolve(msg).then(cb);
  32. },
  33. function reject (msg){
  34. // 拒绝时 传递错误消息 但注意观察者promise是resolve的
  35. Promise.resolve(msg).then(cb);
  36. }
  37. );
  38. // 返回最初的promise
  39. return pr;
  40. }
  41. }
  42. // 还是上一个超时的例子
  43. Promise.race( [
  44. Promise.observe(
  45. foo,
  46. function cleanup (msg){
  47. console.log(`Promise.observe: ${msg}`); // foo即使没有在超时之前完成 也可以获取其决议情况
  48. }
  49. )
  50. .then
  51. ])
3.6.3 all([ .. ])和race([ .. ])的变体


@TODO 自行实现 Promise.any finally map等扩展API

3.6.4 并发迭代

实现一个异步的map(..)工具

接收一个数组的值(可以是Promise或其他值)

接收一个在每个值上运行的一个函数

返回一个Promise,其完成值是一个数组,该数组保存任务执行之后的异步完成值(保持映射顺序)

这里也主要看栗子

</>复制代码

  1. if(!Promise.map) {
  2. Promise.map = function(vals, cb) {
  3. // 等待所有map的promise决议的新的promise
  4. return Promise.all(
  5. // 对vals使用map将每个值转出promise,值数组->Promise数组
  6. vals.map( function(val){
  7. // 将val值替换成调用cb函数后决议的新的promise
  8. return new Promise( function(resolve){
  9. // resolve reject传入到cb函数中
  10. cb(val, resolve);
  11. })
  12. })
  13. )
  14. }
  15. }
  16. // 使用Promise.map
  17. var p1 = Promise.resolve(21);
  18. var p2 = Promise.resolve(30);
  19. var p3 = Promise.reject("opps");
  20. Promise.map( [p1,p2,p3], function(pr, resolve){
  21. Promise.resolve(pr)
  22. .then( val => {
  23. resolve( val*2 );
  24. },
  25. resolve // 注意:不能发出拒绝信号,如果发出会导致Promise.map被拒绝,其他map结果也会被丢弃
  26. )
  27. })
  28. .then( (vals) => {
  29. console.log(vals);
  30. })

TODO:
Promise API 概述详解多带带成篇

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94606.html

相关文章

  • 笔记你不知道JS读书笔记——异步

    摘要:异步请求线程在在连接后是通过浏览器新开一个线程请求将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件循环队列中。 基础:浏览器 -- 多进程,每个tab页独立一个浏览器渲染进程(浏览器内核) 每个浏览器渲染进程是多线程的,主要包括:GUI渲染线程 JS引擎线程 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎) JS引擎线程负...

    junnplus 评论0 收藏0
  • 你不知道JavaScript》 (中) 阅读摘要

    摘要:这时候控制台看到的是对象的快照,然而点开看详情的话是这段代码在运行的时候,浏览器可能会认为需要把控制台延迟到后台,这种情况下,等到浏览器控制台输出对象内容时,可能已经运行,因此会在点开的时候显示,这是的异步化造成的。 本书属于基础类书籍,会有比较多的基础知识,所以这里仅记录平常不怎么容易注意到的知识点,不会全记,供大家和自己翻阅; 上中下三本的读书笔记: 《你不知道的JavaScri...

    stackvoid 评论0 收藏0
  • 你不知道JS读书笔记---作用域及闭包

    摘要:注此读书笔记只记录本人原先不太理解的内容经过阅读你不知道的后的理解。作用域及闭包基础,代码运行的幕后工作者引擎及编译器。 注:此读书笔记只记录本人原先不太理解的内容经过阅读《你不知道的JS》后的理解。 作用域及闭包基础,JS代码运行的幕后工作者:引擎及编译器。引擎负责JS程序的编译及执行,编译器负责词法分析和代码生成。那么作用域就像一个容器,引擎及编译器都从这里提取东西。 ...

    denson 评论0 收藏0
  • 你不知道JS读书笔记之闭包在循环中应用

    摘要:闭包在循环中的应用延迟函数的回调会在循环结束时才执行事实上,当定时器运行时即使没给迭代中执行的是多有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个出来。 闭包在循环中的应用 延迟函数的回调会在循环结束时才执行;事实上,当定时器运行时即使没给迭代中执行的是 setTime(..., 0),多有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个6出来。 for(var...

    weapon 评论0 收藏0
  • JS笔记

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。异步编程入门的全称是前端经典面试题从输入到页面加载发生了什么这是一篇开发的科普类文章,涉及到优化等多个方面。 TypeScript 入门教程 从 JavaScript 程序员的角度总结思考,循序渐进的理解 TypeScript。 网络基础知识之 HTTP 协议 详细介绍 HTT...

    rottengeek 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<