[思维导图] Promise - 《你不知道的JavaScript》- 中卷 - 第二部分


3. Promises

  1. 异步回调的缺陷 -> 缺乏顺序性和可信任性

  2. 控制反转 -> 丢失信任性 -> 反转控制反转 -> 第三方提供了解其任务何时结束的能力

  3. 并未摒弃回调 -> 将回调交与可信中介机制

3.1 定义Promise

未来值
  1. 未来值的重要特性 -> 可能成功也可能失败甚至不可达

  2. 现在值与将来值

    • 回调处理未来值的方式 -> 所有操作都变成异步(统一处理现在将来都成为将来)

    • Promise以同步操作的流程描述异步 -> 决议结果可能是拒绝或完成

      • 完成值总是编程给出,拒绝值为拒绝原因(程序逻辑或异常隐式设置)

      • Promise 封装依赖于时间的状态(底层值得完成或拒绝) -> Promise本身与时间无关

      • Promise 可以按照可预测的方式组成(组合) -> 无需关心时序或底层结果

      • Promise 一旦决议(resolved)就永久持有结果状态(不变值)

        • 可以安全的给第三方使用 -> 不能修改

        • 不可变性是 Promise 最基础和最重要的因素

      • Promise 一旦建立无法撤销

      • 若不设置错误处理回调则无法抛出 Promise 内部错误

      • 无法获知 Pending 的 Promise 的执行进度

      • Promise是封装和组合未来值得可重用机制

    • Promise.all([..])和.then各自创建promise

      • then()第一个参数为fulfillment 函数,第二个为rejection函数

  3. 完成事件

    • Promise 可以视作在异步任务中若干步骤的流程控制机制(this-then-that)

    • 只关心completion/continuation事件结束时的通知以继续后续执行

    • Promise 监听来自程序的"事件";而回调方式中"事件"通知将作为回调被任务(程序)调用

    • 反控制反转 -> 将控制权返还给调用代码

      • 回调是控制反转

      • 本质上"事件"对象(Promise)是分离的关注点之间中立的第三方协商者

  4. Promise 事件两种

    • new Promise(function(resolve, reject){..}(revealing constructor)

      • 传入的函数会立即执行(不同于回调的异步延迟)

      • 两个参数resolve(通常为完成 -> 不一定)和reject(拒绝)为决议函数

    • 两种使用方式 -> 适用范围不同(前置条件foo函数返回 Promise 对象)

      • var p = foo(42);bar(p); -> 无论 foo 是否成功都能调用 bar 函数

      • var p = foo(42); p.then(bar, oopsBar); -> 只在 foo 成功后才调用 bar 函数(Promise 对象控制函数调用)

  5. 多次独立调用then()不同于链式调用then()

  6. Promise决议(resolution)不是必须要发送消息,可以只是流程控制信号

  7. ES6原生Promise对象用于传递异步操作的消息 -> 代表未来才会知道结果的事件(通常是异步操作),并提供可进一步处理的统一API接口

    • 对象的状态不受外界影响 -> Promise对象代表一个异步操作

      • 三个状态: Pending(进行中)、Resolved/Fulfilled(已完成)和Rejected(已失败)

      • 只有异步操作结果(承诺)才能决定当前所处状态 -> 其他操作无法改变

    • 一旦状态改变就不能再修改

      • Promise状态改变: Pending->Resolved或Pending->Rejected

      • 状态发生后就冻结 -> 重复调用结果不改变(而事件则是错过后再监听没有结果)

3.2 Thenable Duck Typing(具有then 方法的鸭子类型)

  1. 确定值是否是或者行为类似于真正的Promise -> Promise重要先决条件

  2. p instance Promise检验方式不充分

    • Promise 值可能来自其他浏览器窗口(iframe)

    • 第三方库或框架可能自定义而不使用原生Promise

  3. 识别Promise(或Promise-like)等同于定义thenable -> 任意包含then()方法的对象或函数

  4. 鸭子类型: 类型检查基于值拥有的属性

  5. 鸭子类型检查的局限性 -> thenable定义可能导致对象被无意中识别为 Promise -> 导致难以跟踪的bug

    • 原型链继承

    • pre-ES6第三方库自定义then()方法

  6. 使用thenable duck typing要小心非预期的promise-like行为 -> ES6标准劫持了属性名then

3.3 Promise 信任问题

  1. 只用回调模式的存在信任问题:

    • 调用回调过早

    • 调用回调过晚或不调用

    • 调用回调次数过少或过多

    • 无法传递所需的环境或参数

    • 吞掉可能出现的错误或异常

  2. 过早调用 -> Promise自动阻止Zalgo(无需插入setTimeout(..,0) hack)

    • Promise不能被同步观察 -> 避免竟态(有时同步完成有时异步完成)

    • 无论Promise是否决议then()的回调都被异步执行

  3. 过晚调用

    • 一旦 Promise 决议(调用resolve()或reject()时)则自动调用then()注册的回调

      • 所有then()注册的回调都会在下一个异步时机点上依次立即调用

      • 其中任一回调都无法影响或者延误对其他回调的调用

    • 调度技巧

      • 两个独立 Promise 上链接的回调的相对顺序无法可靠预测

      • 避免依赖于不同 Promise 间回调的顺序和调度

  4. 回调未调用

    • 无法阻止Promise完成时发出其决议的通知(总会调用Promise注册的回调)

      • JS错误也不行

      • 回调函数本身的错误会导致非期望的结果 -> 回调仍已被调用

      • Promise.race 竞争条件(避免程序挂起) -> 解决 Promise 本身未被调用情形


  5. 调用次数过少或过多

    • 回调的合适次数是一次

    • Promise只能被决议一次

      • Promise创建代码多次调用resolve()/reject(),只接受第一次决议完成值(静默忽略后续调用)

      • 任何通过then()注册的(每个)回调只会调用一次

    • 无法阻止用户自己注册多次同一回调 -> 作死

  6. 未能传递参数或环境值

    • Promise最多只有一个决议值(完成或拒绝)

      • 若无显式的完成值则是undefined

      • 无论什么决议值都会在现在或未来传递给所有注册的回调

    • resolve()或reject()自动静默忽略第一个参数外的其他参数

      • 保护处理

      • 将参数封装在单一值(对象或数组)以实现传入多个参数

    • JS中函数总是保持自身定义所在的作用域闭包

      • 函数可以继续访问提供的环境状态

      • 回调同样具有 -> 非Promise特有

  7. 吞掉错误或异常

    • 若拒绝一个 Promise 并给出理由,则该理由将传给reject回调

    • Promise创建或者决议中JS异常错误导致Promise被reject -> 异常错误为拒绝原因

      • 有效解决出错导致同步相应而不出错则是异步的 Zalgo 风险

      • Promises将JS异常转为异步行为

    • then()注册的回调中的异常无法被当前注册的错误处理函数捕捉

      • Promise 一旦决议不可改变 -> 无法因为出错就将 Promise 对象变为拒绝

      • 错误处理函数是为resolved promise 准备的

      • 注意: Promise 链中可被下一个then()调用处理

  8. 函数返回结果是可信任的 Promise 吗

    • Promise只是改变传递回调的位置 -> 没有完全摆脱回调

    • Promise比回调更可信的原因 -> 通过原生ES6 Promise的Promise.resolve()实现

      • 传递非 Promise/非 thenable 值的直接值 -> 获得以该值填充的 promise(等价于new Promise()创建)

      • 传入 promise 则只会返回同一 promise

      • 传入非 Promise 的 thenable 值 -> 尝试展开该值直至提取非 promise-like 的最终值

    • Promise.resolve()提供可信任的 Promise 封装工具(可以链接使用)

      • 解封其接受的任意 thenable 值为非 thenable 值 -> 获得真正的可信任的Promise

      • 封装所有函数的返回值(无论是否thenable) -> 将函数调用规范化为定义良好的异步任务

  9. 建立信任

    Promise模式通过可信任的语义将回调作为参数传递 -> 为回调增加信任语义

    • 反转回调的控制反转问题

    • 将控制权置于可信任的系统(Promise)中

3.4 链式流

  1. Promise 链是表达多步异步序列的流程控制和依次传递的消息的消息通道

  2. Promise 链式流程控制的固有特性

    • Promise 的每次then()调用自动创建并返回新的 Promise 用于后续链接

    • fulfillment 或 rejection 回调返回 Promise 被自动展开并作为当前then()返回的链接 Promise 中决议值

    • fulfillment 或 rejection 回调内部返回值或抛出异常 -> 新返回的可链接 Promise 就相应的决议

      • 若不显式返回值则返回undefined -> 仍然可以执行 promise 链接

  3. Promise序列在每一步保证异步能力的关键原因:

    • Promise.resolve()能直接返回传入的真正 Promise 或展开传入的 thenable 值(持续展开递归前进)

    • fulfillment 或 rejection 处理函数返回 thenable 或 Promise 时同样会展开

  4. Promise 链任意位置捕捉错误的动作相当于将 Promise 链重置回正常运作

  5. then()注册的回调调用时若抛出异常则导致链中下一个 promise 被立即拒绝

    • 若未注册 reject 处理函数则使用默认 reject 处理函数 -> then(function fulfilled(){})

      • 只是重新抛出错误

      • 错误沿Promise链传播直至显式定义的 reject 处理函数

    • 若未注册完成(fulfillment)处理函数则使用默认 fulfillment 处理函数

      • then(null, function rejected(){}) -> 缩写形式为catch(function(err){})

      • 将接受的任意值传递给链中下一步骤

  6. Promise 链的缺点是仍有大量重复样板代码(then()和function(){..})

  7. Promise 构造器的回调函数参数命名

    • 第一个参数应使用 resolve(为 Promise 设定最终值/状态)

      • 类似于 Promise.resolve()结果的二元性

      • 会展开 thenable(类似Promise.resolve()) 或真正的 Promise

    • 第二个参数为 reject -> 不能完成未来值(拒绝 Promise)

      • 不会像resolve()一样展开

      • 直接将传入的 Promise/thenable 值原封不动的设置为拒绝理由

      • 后续拒绝处理函数实际处理的是初始的 Promise/thenable 值(非底层立即值)

  8. then()调用回调参数命名

    • fulfilled()(第一个参数) -> 总是处理完成的情形

    • rejected()(第二个参数)

3.5 错误处理

  1. 同步的try...catch解构无法用于异步代码模式 -> 除非有额外环境支持(generator)

  2. 拒绝Promise -> 显式调用reject()或JS异常

  3. Promise错误处理采用分离回调风格(一个回调用于完成另一个用于拒绝) -> 非常见的error-first风格

    • 假定吞掉所有错误(静默失败)

    • 若异常阻止 Promise 的创建则被立即抛出而不是reject Promise

      • new Promise(null), Promise.all(), `Promise.race(42)

      • 需要合法使用Promise API

    • 在Promise链最后添加catch(..)(最佳实践) -> 仍然无法处理catch()子句中的异常

  4. Pit of Success -> 未来会实现的内容 -> 实现所有错误要么被处理要么被报告(推迟到将来)

    • 默认情况下Promise在下一个任务或事件循环tick上(向开发者终端)报告所有拒绝(若未注册错误处理函数)

    • 被拒绝的 Promise 可以调用defer()函数在查看之前某一时间段内保持的被拒绝状态

      • 阻止自动报告错误 -> 隐式(在拒绝之前注册错误处理函数)和显式(defer())

      • defer()优先级高于 Promise 的自动错误报告

      • defer()简单的返回同一个 promise -> var p = Promise.reject("Oops").defer();

    • 唯一的危险是defer()了一个 Promise 却没有成功查看或处理其被拒绝的理由

3.6 Promise模式

  1. Promise 链(异步序列)中任一时刻只能执行一个异步任务

  2. ES6 Promise 直接支持两个流程控制模式

    1. Promise.all([..])(门) -> 并发执行多个Promise实例 -> 不关心执行顺序

      • 接受数组参数 -> Promise实例,thenable 值或立即值

      • 每个元素都被Promise.resovle()处理为真正的Promise

        • 立即值将仅仅被标准化为对应Promise -> fulfillment

        • 参数数组为空则Promise被立即实现

      • 返回所有 Promise 的完成消息数组 -> 元素顺序与参数数组指定顺序相同(非完成消息的生成顺序)

      • 只有数组中所有成员promise完成主 Promise 才完成 -> 任一Promise 被拒绝导致全部被立即reject

      • 为每个Promise都关联拒绝/错误处理函数 -> 特别是Promise.all([..])返回的 Promise

    2. Promise.race([..])(门闩/竟态) -> 只响应第一个完成的 Promise -> 注意竟态不同于竟态条件

      • 同样接受数组参数 -> Promise实例,thenables和立即值(立即值直接实现Promise -> 实践中无意义)

      • 任一Promise 决议为完成则完成; 任一Promise 决议为拒绝则拒绝

      • 永远不要传入空参数数组 -> 导致 Promise 永远无法决议

      • 超时竞赛 -> Promise.race([..])和Promise.all([..])都能使用

         Promise.race([foo, timeoutPromise(3000)])        .then(              function(){},                function(err){}              );
        
      • finally -> 避免 Promise 被静默忽略

        • 被丢弃或忽略的 Promise 不能被撤销 -> 只能静默忽略

        • Promise 需要finally()回调注册用于后续资源清理 -> ES7支持

        • finally(..)创建并返回新的Promise用于后续链接 p.then(something).finally(cleanup).then(another)

        • 构建静态辅助工具用于查看(不影响)Promise的决议

           if(!Promise.observe) {   Promise.observe = function(pr, cb) {       //观察pr的解析方式       pr.then(           function fulfilled(msg) {             //异步调用回调函数             Promise.resolve(msg).then(cb);           },           function rejected(err) {             //异步调用回调函数             Promise.resolve(err).then(cb);           }        );        //返回原始的Promise        return pr;    }; }Promise.race([   Promise.observe(     foo(),     //在foo()调用后执行清理工作     function cleanup(msg) {}   ),   timeoutPromise(3000)]);
          
  3. all([..])和race([..])的变体

    • none([..]) -> 类似all([..])所有 Promise 都要被拒绝才能转化为完成值

    • any([..]) -> 类似all[..],只需任意一个Promise完成

    • first([..]) -> 类似any(),只要第一个Promise完成就忽略后续任何拒绝或完成

      • 若所有 Promise 都拒绝(主 Promise不会拒绝)则导致挂起

      • 增加额外逻辑跟踪每个 promise 拒绝 -> 若所有 Promise 都被拒绝则在主 promise 上调用reject()

      if(!Promise.first) {  Promie.first = function (prs) {    return new Promise(function (resolve, reject) {        prs.forEach(function (pr) {            Promise.resolve(pr).then(resolve);        });    });  };}
      
    • last([..]) -> 类似first(),只有最后一个Promise完成胜出

  4. 并发迭代 -> 迭代Promise序列

    • 构建forEach/map/some/every的异步版本 -> 若每个 Promise 执行的任务都是同步的则直接使用

    • map返回一个promise,其完成值为所有异步任务完成值构成的数组(保持映射顺序)

      • 不能发送异步拒绝信号 -> 但若映射的回调(cb())内发生同步异常错误则Promise.map()返回拒绝的

      if(!Promise.map) {  Promise.map = function (vals, cb) {    return Promise.all(        vals.map(function (val) {          return new Promise(            function (resolve) {              cb(val, resolve);            }          );        })    );  };}
      

3.7 Promise API 概述

  1. new Promise(..)构造器

    • 必须提供一个同步的或立即调用的异步回调函数

    • 函数接收两个函数回调作为promise的决议

      • reject()直接拒绝promise

      • resolve()可能完成或拒绝

        • 若传入非 Promise 非 thenable 的立即值则promise 使用该值完成

        • 若传入 Promise 或 thenable 值则递归展开直至最终决议值或状态被

  2. Promise.resolve()和Promise.reject()

    • Promise.reject()直接创建已被拒绝的Promise

    • Promise.resolve()创建已完成或拒绝的promise

      • 展开thenable值并采纳thenable的最终决议值

      • 参数为Promise则直接返回

  3. then()和catch()

    • 每个Promise实例都有then()和catch()方法 -> 分别注册完成和拒绝处理函数

    • then()函数参数分别为完成回调拒绝回调

      • 省略任一参数或传入非函数值都自动替换为对应的默认回调

      • 默认完成回调继续传递消息;默认拒绝回调重新抛出(传播)接收错误(原因)

    • catch()只接收拒绝回调作为参数(自动替换默认完成回调) -> 等价于then(null, rejected)

    • then()和catch()都创建并返回新 promise 用于后续链接

      • 完成回调或拒绝回调出现异常 -> 返回新 promise 已拒绝

      • 任一回调返回的非 Promise 非 thenable 值的立即值 -> 作为新 promise 的完成值

      • 完成处理函数返回的 promise 或 thenable 值 -> 被展开并作为新 promise 的决议值

  4. Promise.all([])和Promise.race([])

    • 创建并返回新Promise -> 决议完全由传入的 promise 数组控制

    • Promise.all([..]) (门)

      • 所有传入的 promise 都完成才能完成 -> 结果为所有 promise 完成值构成的数组

      • 任一 promise 被拒绝都导致主 promise 被立即拒绝 -> 获得第一个拒绝 promise 的理由

      • 传入空数组则主 promise 立即完成

    • Promise.race([..](门闩)

      • 只有第一个决议的 promise (完成或拒绝)的决议结果成为新 promise 的决议

      • 传入空数组导致 promise 永远挂起 -> 永远不会决议

3.8 Promise 的局限性

  1. 顺序错误处理

    • Promise 链中错误会无意中被静默忽略

    • 外部方法无法观察可能发生的错误 -> 没有将整个链标识为由若干个体组成的实体

    • 没有为 Promise 链序列的中间步骤保留引用

    • 链中任何位置的错误都会在链中传播直至被某步注册的拒绝处理函数查看 -> 在最后一步注册

    • 完全无法获知(对任何已处理的拒绝错误的)错误通知

  2. 单一值 -> Promise 只能有一个完成值或拒绝理由

    • 不适应于复杂场景 -> 构建对象或数组包装器带来冗余代码

    • 分裂值 -> 提示将问题分解为若干 Promise 的信号 -> 由调用代码 promise 的调用

    • 展开/传递参数 -> 实现每个 promise 一个值得概念

      • pre-ES6使用Function.prototype.apply.bind(fn, null) -> 展开传入的 promise 完成值

      • ES6的解构

  3. 单次决议 -> Promise只能被决议一次(完成或拒绝)

    • 事件/数据流模式需要多值决议处理

    • 每次事件处理都需要中定义完整的 promise 链 -> 违反关注与功能分离(SoC)


  4. 惯性 -> 现存的代码无法理解 Promise -> 将基于回调转为 Promise

    • 使用Promise.wrap()接受 error-first 风格回调并返回生成 promise 的函数

    • 类似 promisory(Promise 工厂)

    if(!Promise.wrap) {    Promise.wrap = function (fn) {        return new function () {            var args = Array.prototype.slice.call(arguments);            return new Promise(function (resolve, reject) {                fn.apply(                    null,                    args.concat(function (err, v) {                        if(err) {                            reject(err);                        } else {                            resolve(v);                        }                    })                );            });        };    };}
    
  5. 无法撤销的 Promise

    • Promise 本身不应该支持撤销 -> 不能导致观察者影响其他观察者查看 Promise 结果

    • 第三方库引入的撤销机制违反未来值得可信机制(外部不变性) -> 导致"远隔作用"的反模式

    • 撤销应是比Promise更高级的抽象

      • 单个Promise并非真正的流控制机制(Promise链才是) -> 撤销涉及流程控制

      • 不能单独撤销 Promise,只能撤销Promise链序列


  6. Promise 性能 -> 不能因噎废食

    • Promise通常比非 Promise 非可信任回调的等价系统速度稍慢

      • Promise make everything async

      • 立即(同步)完成的步骤仍会延迟到任务的下一步

    • 程序关键区域分离出 Promise -> 检测是否是性能瓶颈

编辑于 2017-02-17

文章被以下专栏收录