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

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

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

正文github

3.1 什么是Promise

以前的方式:api

  • 利用回调函数封装程序中的continuation
  • 回调交给第三方
  • 第三方调用回调
  • 实现正确功能

Promise方式:
第三方提供了解其任务什么时候结束的能力数组

Promise的异步特性是基于任务的(图示以下)
任务队列.pngpromise

一种处理异步的思路:为了统一如今和未来,把它们都变成未来,即全部操做都成了异步的浏览器

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

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

Promise设计的重要基础并发

  • Promise必定是异步执行的,即便是当即完成的Promise(相似 new Promise((resolve)=>{ resolve(42) })),也没法被同步观察到
  • 一旦Promise决议,它就永远保持在这个状态,变成了不变值(immuatable value),这是设计中最基础和最重要的因素
  • Promise至多只能有一个决议值(一个!一个!一个!)

引伸:异步

  • Promise的决议结果能够给多方屡次查看
  • 安全、可靠

3.2 Promise的检测

基于thenable的鸭子类型

if(
  p !== null && 
  (
    typeof p === 'object' ||
    typeof p === 'function'
  ) && 
  typeof p.then === 'function'
) {
  // 假定这是一个thenable
}
else {
  // 不是thenable
}

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

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一直不决议呢?使用竞态的高级抽象机制:

// 超时工具
function timeoutPromise(delay){
  return new Promise( (resolve, reject) => {
    setTimeout( function () {
      reject('Timeout!');
    }, delay);
  } )
}

// 设置某个Promise foo()超时
Promise.race( [
  foo(),
  timeoutPromise(3000)
] )
.then(
  function () {
    // foo(..)及时完成
  },
  function (err) {
    // foo(..)被拒绝或者超时
    // 经过查看err肯定错误状况
  }
);

3.3.4 调用次数过少或过多

若是建立Promise的代码试图屡次调用resolve(..)或reject(..),或者二者都调用,Promise只会接受第一次决议,后续调用都会被忽略

3.3.5 未能传递参数/环境值

Promise至多只能有一个决议值

若是使用多个参数调用resolve(..)或reject(..),第一个参数以后的全部参数都会被忽略

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

3.3.6 吞掉错误或异常

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

  1. Promise建立过程或其决议确认以前的任什么时候间点上(注:书中原文查看其决议结果过程当中任什么时候间点,我的认为可能翻译得有点问题,应该要强调是其决议以前)
  2. Promise决议确认后在查看结果时(then(..)注册的回调中)出现了js异常错误

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

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

let p = new Promise ( function(resolve, reject){
    foo.bar(); // foo undefined 将抛出错误 Promise=>reject
    resolve( 42 ); // 不会执行到这里
});
p.then(
    function fulfilled(){
        // 不会执行到这里
    },
    function rejected(err){
        // err是一个TypeError异常
    }
)

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

let q = new Promise ( function(resolve, reject){
    resolve( 42 );
})
q.then(
    function fulfilled(){
        foo.bar(); // foo undefined 将抛出错误 致使then返回的Promise被reject
    },
    function rejected(err){
        // 不会执行到这里
    }
).then(
    function fulfilled(){
        // 不会执行到这里
    },
    function rejected(err){
        // err是一个TypeError异常
    }
)

3.3.7 构建可信任的Promise

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

  • 传入一个非Promise、非thenable的当即值, 会获得一个用该值填充的Promise
  • 传入一个真正的Promise,会返回同一个Promise
  • 传入一个非Promise的thenable值,会试图展开这个值,持续到提取出一个具体的非类Promise的最终值

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

// 传入一个当即值
let p = Promise.resolve(42);
p.then( res => {
    console.log('Promise.resolve(42).then:',res);
})
let p1 = Promise.resolve({});
p1.then( res => {
    console.log('Promise.resolve({}).then:',res);
})
// 传入一个 thenable 尝试展开
let p2 = Promise.resolve({
    then: function(cb) { cb(42)}
});
p2.then( res => {
    console.log('Promise.resolve(thenable).then:', res);
}, err => {
    console.log('Promise.resolve(thenable).then:', err);
})
// 注意 这种状况其实也是当即值!!!
let p3 = Promise.resolve(
    setTimeout(()=>{
        return 'inside a continuation'  
    },1000)
); // settimeout函数返回当前定时器引用=>耶 当即值
p3.then( res => {
    console.log('Promise.resolve(看起来是个异步).then:', res); 
})

3.4 Promise链式流

Promise不只仅是一个单步执行this-then-that的操做机制,这只是它的构成部件,实际上Promise是能够链接到一块儿使用表示一系列异步步骤:

  • 每次对Promise调用then(..),它都会建立并返回一个新的Promise,咱们能够将其连接起来;(并不局限于要求then中返回一个Promise)
  • 无论从then(..)调用的完成回调(第一个参数)返回的值是什么,它都会被自动设置为被连接Promise(上一点中的)的完成(resolve)(必定要理解这句话)

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

栗子:

// 返回当即值

    let p = Promise.resolve(21);
    p
    .then( function(v) {
        console.log(v);  // 21

        // 返回当即值
        return v * 2;
    })
    // 这里是连接的Promise
    .then ( function(v) {
        console.log(v);  // 42
    });
// 返回Promise并引入异步

    let p = Promise.resolve(21);
    p
    .then ( function(v) {
        // 返回一个异步Promise
        return new Promise( (resolve, reject) => {
            setTimeout(() => {
                resolve(v*2);
            }, 1000);
        });
    })
    .then ( function(v) {
        // 前一步延迟1s后执行
        console.log(v);
    })

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

3.5 错误处理

几种错误处理方式:

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

function foo() {
        setTimeout(() => {
            baz.bar();  // 错误代码
        }, 100);
    }
    try{
        foo();  // 以后将抛出全局错误
    }
    catch (err) {
        // 不会走到这里
    }

foo()中有本身的异步完成函数,其中任何异步错误都没法捕捉到

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

function foo(cb) {
        setTimeout(() => {
            try {
                var x = baz.bar();  //  错误代码
                cb(null, x);
            }
            catch (err) {
                cb(err);
            }
        }, 100);
    }

    foo( function(err, val) {
        if(err) {
            console.error(err);  //  报错惹
        }
        else {
            console.log(val);
        }
    })

分离回调模式(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时在开发者未注意的状况下出现未捕获错误而报出全局错误的方案

具体请看:

{
    let p = Promise.reject(21); // 将触发全局报错 Uncaught (in promise) 21

    let p1 = Promise.reject(21).then (  // 拒绝前,注册了一个错误处理函数
        (res) => {
            // 不会走到这里 
        },
        (err) => {
            console.log(`注册了一个错误处理函数:${err}`);
        }
    )
    Promise.prototype.defer = function (){
        // 做者提出的一个API  
        // 简单实现就是单纯的返回这个Promise自己 
        return this;
    }

    let p2 = Promise.reject(21).defer(); // p2的结果在未来会被查看,如今暂时不要报全局错误

    let foo = Promise.resolve(21);

    foo
    .then (function(v) {
        return p2; // 这里查看p2的结果
    }, function (err) {
        // 不会走到这里
    })
    .catch (function(v) {
        console.log(v); // p2的结果
    })
}

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决议能够被屡次查看),具体能够看栗子

let foo = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(21);
        }, 301);
    });
    let timeout = function(time) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('timeout');
            }, time);
        })
    }
    // foo会被默默忽略
    Promise.race( [
        foo, 
        timeout(300)
    ])
    .then( (res) => {
        console.log(`Promise.race: ${res}`);
    })
    .finally( (res) => {
        console.log(`Promise.race: ${res}`);    // finally回调是不会提供任何参数的,详情可看 https://github.com/tc39/proposal-promise-finally
    })
    // 观察者模式
    if(!Promise.observe){
        Promise.observe = function(pr, cb){
            // 观察pr的决议
            pr.then( 
                function fulfilled (msg){
                    // 完成时
                    Promise.resolve(msg).then(cb);
                },
                function reject (msg){
                    // 拒绝时 传递错误消息 但注意观察者promise是resolve的
                    Promise.resolve(msg).then(cb);
                }
            );
            // 返回最初的promise
            return pr;
        }
    }
    // 仍是上一个超时的例子
    Promise.race( [
        Promise.observe(
            foo,
            function cleanup (msg){
                console.log(`Promise.observe: ${msg}`); // foo即便没有在超时以前完成 也能够获取其决议状况
            }
        )
        .then 
    ])

3.6.3 all([ .. ])和race([ .. ])的变体


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

3.6.4 并发迭代

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

  • 接收一个数组的值(能够是Promise或其余值)
  • 接收一个在每一个值上运行的一个函数
  • 返回一个Promise,其完成值是一个数组,该数组保存任务执行以后的异步完成值(保持映射顺序)

这里也主要看栗子

if(!Promise.map) {
        Promise.map = function(vals, cb) {
            // 等待全部map的promise决议的新的promise
            return Promise.all(
                // 对vals使用map将每一个值转出promise,值数组->Promise数组
                vals.map( function(val){
                    // 将val值替换成调用cb函数后决议的新的promise
                    return new Promise( function(resolve){
                        // resolve reject传入到cb函数中
                        cb(val, resolve);
                    })
                })
            )
        }
    }
    // 使用Promise.map
    var p1 = Promise.resolve(21);
    var p2 = Promise.resolve(30);
    var p3 = Promise.reject('opps');

    Promise.map( [p1,p2,p3], function(pr, resolve){
        Promise.resolve(pr)
        .then( val => {
            resolve( val*2 );
        },
            resolve  // 注意:不能发出拒绝信号,若是发出会致使Promise.map被拒绝,其余map结果也会被丢弃
        )
    })
    .then( (vals) => {
        console.log(vals);
    })

TODO:Promise API 概述详解单独成篇

相关文章
相关标签/搜索