Web前端之路让前端飞程序员

你不知道的JavaScript(中卷)|Promise(一)

2017-11-22  本文已影响447人  xpwei

什么是Promise

function add(getX, getY, cb) {
    var x, y;
    getX(function (xVal) {
        x = xVal;
        // 两个都准备好了?
        if (y != undefined) {
            cb(x + y); // 发送和
        }
    });
    getY(function (yVal) {
        y = yVal;
        // 两个都准备好了?
        if (x != undefined) {
            cb(x + y); // 发送和
        }
    });
}
// fetchX() 和fetchY()是同步或者异步函数
add(fetchX, fetchY, function (sum) {
    console.log(sum); // 是不是很容易?
});
function add(xPromise, yPromise) {
    // Promise.all([ .. ])接受一个promise数组并返回一个新的promise,
    // 这个新promise等待数组中的所有promise完成
    return Promise.all([xPromise, yPromise])
        // 这个promise决议之后,我们取得收到的X和Y值并加在一起
        .then(function (values) {
            // values是来自于之前决议的promisei的消息数组
            return values[0] + values[1];
        });
}
// fetchX()和fetchY()返回相应值的promise,可能已经就绪,
// 也可能以后就绪
add(fetchX(), fetchY())
    // 我们得到一个这两个数组的和的promise
    // 现在链式调用 then(..)来等待返回promise的决议
    .then(function (sum) {
        console.log(sum); // 这更简单!
    });

fetchX() 和fetchY() 是直接调用的,它们的返回值(promise!)被传给add(..)。这些promise代表的低层值的可用时间可能是现在或将来,但不管怎样,promise归一保证了行为的一致性。我们可以按照不依赖于时间的方式追踪值X和Y。它们是未来值。
第二层是add(..)(通过Promise.all([ .. ]))创建并返回的promise。我们通过调用then(..)等待这个promise。add(..)运算完成后,未来值sum就准备好了,可以打印出来。我们把等待未来值X和Y的逻辑隐藏在了add(..)内部。

在add(..)内部,Promise.all([..])调用创建了一个promise(这个promise等待promiseX和promiseY的决议)。链式调用.then(..)创建了另外一个promise。这个promise由return values[0] + values[1]这一行立即决议(得到加运算的结果)。因此,链add(..)调用终止处的调用then(..)——在代码结尾处——实际上操作的是返回的第二个promise,而不是由Promise.all([..])创建的第一个promise。还有,尽管第二个then(..)后面没有链接任何东西,但它实际上也创建了一个新的promise,如果想要观察或者使用它的话就可以看到。

Promise的决议结果也可能是拒绝而不是完成的。拒绝值和完成的Promise不一样:完成值总是变成给出的,而拒绝值,通常称为拒绝原因,可能是程序逻辑直接设置的,也可能是从运行异常隐式得出的值。
通过Promise,调用then(..)实际上可以接受两个函数,第一个用于完成情况(如前所示),第二个用于拒绝情况:

add(fetchX(), fetchY())
    .then(
    // 完成处理函数
    function (sum) {
        console.log(sum);
    },
    // 拒绝处理函数
    function (err) {
        console.error(err); // 烦!
    }
);

从外部看,由于Promise封装了依赖于时间的状态——等待低层值的完成或拒绝,所以Promise本身是与时间无关的。因此,Promise可以按照可预测的方式组成(组合),而不是关心时序或低层的结果。
另外,一旦Promise决议,它就永远保持在这个状态。此时它就称为了不变值,可以根据需求多次查看。

function foo(x) {
    // 可是做一些可能耗时的工作
    // 构造并返回一个promise
    return new Promise(function (resolve, reject) {
        // 最终调用resolve(..)或者reject(..)
        // 这是这个promise的决议回调
    });
}
var p = foo(42);
bar(p);
baz(p);

new Promise( function(..){ .. } )模式通常称为revealing constructor。传入的函数会立即执行(不会像then(..)中的回调一样异步延迟),它有两个参数,在本例中我们将其分别称为resolve和reject。这些是promise的决议函数。resolve(..)通常标识完成,而reject(..)则标识拒绝。

你可能会猜测bar(..)和baz(..)的内部实现或许如下:

function bar(fooPromise) {
    // 侦听foo(..)完成
    fooPromise.then(
        function () {
            // foo(..)已经完毕,所以执行bar(..)的任务
        },
        function () {
            // 啊,foo(..)中出错了!
        }
    );
}
    // 对于baz(..)也是一样

Promise决议并不一定要像前面将Promise作为未来值查看时一样会涉及发送消息。它也可以只作为一种流程控制信号,就像前面这段代码中的用法一样。
另外一种实现方式是:

function bar() {
    // foo(..)肯定已经完成,所以执行bar(..)的任务
}
function oopsBar() {
    // 啊,foo(..)中出错了,所以bar(..)没有运行
}
// 对于baz()和oopsBaz()也是一样
var p = foo(42);
p.then(bar, oopsBar);
p.then(baz, oopsBaz);

这里没有把promise p传给bar(..)和baz(..),而是使用promise控制bar(..)和baz(..)何时执行,如果执行的话。最主要的区别在于错误处理部分。
在第一段代码的方法里,不论foo(..)成功与否,bar(..)都会被调用。并且如果收到了foo(..)失败的通知,它会亲自处理自己的回退逻辑。显然,baz(..)也是如此。
在第二段代码中,bar(..)只有在foo(..)成功时才会被调用,否则就会调用oppsBar(..)。baz(..)也是如此。
这两种方法本身并谈不上对错,只是各自适用于不同的情况。
另外,两端代码都以使用promise p调用then(..)两次结束。这个事实说明了前面的观点,就是Promise(一旦决议)一直保持其决议结果(完成或拒绝)不变,可以按照需要多次查看。

具有then方法的鸭子类型
在Promise领域,一个重要的细节是如何确定某个值是不是真正的Promise。或者更直接地说,它是不是一个行为方式类似于Promise的值?
既然Promise是通过new Promise(..)语法创建的,那你可以就认为可以通过p instanceof Promise来检查。但遗憾的是,这并不足以作为检查方法,原因有许多。
其中最主要的是,Promise值可能是从其他浏览器窗口(iframe等)接受到的。这个浏览器窗口自己的Promise可能和当前窗口/iframe的不同,因此这样的检查无法识别Promise实例。
还有,库或框架可能会选择实现自己的Promise,而不是使用原生ES6 Promise实现。实际上,很有可能你是在早期根本没有Promise实现的浏览器中使用由库提供的Promise。
识别Promise(或者行为类似于Promise的东西)就是定义某种称为thenable的东西,将其定义为任何具有then(..)方法的对象和函数。我们认为,任何这样的值就是Promise一致的thenable。
根据一个值的形态(具有哪些属性)对这个值的类型做出一些假定。这种类型检查(type check)一般用术语鸭子类型来表示——“如果它看起来像只鸭子,叫起来像只鸭子,那它一定就是只鸭子”。于是,对thenable值的鸭子类型检测就大致类似于:

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

如果你试图使用恰好有then(..)函数的一个对象或函数值完成一个Promise,但并不希望它被当作Promise或thenable,那就有点麻烦了,因为它会自动被识别为thenable,并被按照特定的规则处理。
即使你并没有意识到这个值有then(..)函数也是这样:

var o = { then: function () { } };
// 让v [[Prototype]]-link到o
var v = Object.create(o);
v.someStuff = "cool";
v.otherStuff = "not so cool";
v.hasOwnProperty("then"); // false

v看起来根本不像Promise或thenable。它只是一个具有一些属性的简单对象。你可能只是想要像其他对象一样发送这个值。
但是你不知道的是,v还[[Prototype]]连接到了另外一个对象o,而后者恰好具有一个then(..)属性。所以thenable鸭子类型检测会把v认作一个thenable。
甚至不需要是直接有意支持的:

Object.prototype.then = function(){};
Array.prototype.then = function(){};
var v1 = { hello: "world" };
var v2 = [ "Hello", "World" ];

v1和v2都会被认作thenable。如果有任何其他代码无意或恶意地给Object.prototype、Array.prototype或任何其他原生原型添加then(..),你无法控制也无法预测。并且,如果指定的是不调用其参数作为回调的函数,那么如果有Promise决议到这样的值,就会永远挂住!

Promise信任问题
先回顾一下只用回调编码的信任问题。那一个回调传入工具foo(..)时可能出现如下问题:

调用过早
这个问题主要就是担心代码是否会引入类似Zalgo这样的副作用。在这类问题中,一个任务有时同步完成,有时异步完成,这可能会导致竞态条件。
根据定义,Promise就不必担心这种问题,因为即使是立即完成的Promise(类似于new Promise(function(resolve){resolve(42);}))也无法被同步观察到。
也就是说,对一个Promise调用then(..)的时候,即使这个Promise已经决议,提供给then(..)的回调也总会被异步调用。
不再需要插入你自己的setTimeout(..,0) hack,Promise会自动防止Zalgo出现。

调用过晚
和前面一点类似,Promise创建对象调用resolve(..)或reject(..)时,这个Promise的then(..)注册的观察回调就会被自动调度。可以确信,这些被调度的回调在下一个异步事件点上一定会被触发。
同步查看是不可能的,所以一个同步任务链无法以这种方式运行来实现按照预期有效延迟另一个回调的发生。也就是说,一个Promise决议后,这个Promise上所有的通过then(..)注册的回调都会在下一个异步时机点上依次被立即调用。这些回调中的任意一个都无法影响或延误对其他回调的调用。

p.then(function () {
    p.then(function () {
        console.log("C");
    });
    console.log("A");
});
p.then(function () {
    console.log("B");
});
    // A B C

这里,“C”无法打断或抢占“B”,这是因为Promise的运作方式。

Promise调度技巧
但是,还有很重要的一点需要指出,有很多调度的细微差别。在这种情况下,两个独立Promise上链接的回调的相对顺序无法可靠预测。
如果两个promise p1和p2都已经决议,那么p1.then(..);和p2.then(..)应该最终会先调用p1的回调,然后是p2的那些。但还有一些细微的场景可能不是这样的:

var p3 = new Promise(function (resolve, reject) {
    resolve("B");
});
var p1 = new Promise(function (resolve, reject) {
    resolve(p3);
});
p2 = new Promise(function (resolve, reject) {
    resolve("A");
});
p1.then(function (v) {
    console.log(v);
});
p2.then(function (v) {
    console.log(v);
});
// A B <-- 而不是像你可能认为的B A

p1不是用立即值而是用另一个promise p3决议,后者本身决议为值“B”。规定的行为时把p3展开到p1,但是是异步地展开。所以,在异步任务队列中,p1的回调排在p2的回调之后。
要避免这样的细微区别带来的噩梦,你永远都不应该依赖于不同Promise间回调的顺序和调度。实际上,好的编码实践方案根本不会让多个回调的顺序有丝毫影响,可能的话就要避免。

回调未调用
首先,没有任何东西(甚至是JavaScript错误)能阻止Promise向你通知它的决议(如果它决议了的话)。如果你对一个Promise注册了一个完成回调和一个拒绝回调,那么Promise在决议时总是会调用其中的一个。
但是,如果Promise本身永远不被决议呢?即使这样,Promise也提供了解决方案,其使用了一种称为竞态的高级抽象机制:

// 用于超时一个Promise的工具
function timeoutPromise(delay) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            reject("Timeout!");
        }, delay);
    });
}
// 设置foo()超时
Promise.race([
    foo(), // 试着开始foo()
    timeoutPromise(3000) // 给它3秒钟
])
    .then(
    function () {
        // foo(..)及时完成!
    },
    function (err) {
        // 或者foo()被拒绝,或者只是没能按时完成
        // 查看err来了解是哪种情况
    }
    );

关于这个Promise超时模式还有更多细节需要考量,后面我们会深入讨论。
很重要的一点是,我们可以保证一个foo()有一个输出信号,防止其永久挂住程序。

调用次数过少或过多
根据定义,回调被调用的正确次数应该是1。“过少”的情况就是调用0次,和前面解释过的“未被”调用是同一种情况。
“过多”的情况很容易解释。Promise的定义方式使得它只能被决议一次。如果出于某种原因,Promise创建代码试图调用resolve(..)或reject(..)多次,或者试图两者都调用,那么这个Promise将只会接受第一次决议,并默默地忽略任何后续调用。
由于Promise只能被决议一次,所以任何通过then(..)注册的(每个)回调就只会被调用一次。
当然,如果你把同一个回调注册了不止一次(比如p.then(f);p.then(f);),那它被调用的次数就会和注册次数相同。响应函数只会被调用一次,但这个保证并不能预防你搬起石头砸自己的脚。

未能传递参数/环境值
Promise至多只能有一个决议值(完成或拒绝)。
如果你没有用任何值显式决议,那么这个值就是undefined,这是JavaScript常见的处理方式。但不管这个值是什么,无论当前或未来,它都会被传给所有注册的(且适当的完成或拒绝)回调。
还有一点需要清楚:如果使用多个参数调用resolve(..)或者reject(..),第一个参数之后的所有参数都会被默默忽略。这看起来似乎违背了我们前面介绍的保证,但实际上并没有,因为这是对Promise机制的无效使用。对于这组API的其他无效使用(比如多次重读调用resolve(..)),也是类似的保护处理,所以这里的Promise行为是一致的。
如果要传递多个值,你就必须要把它们封装在单个值中传递,比如通过一个数组或对象。
对环境来说,JavaScript中的函数总是保持其定义所在的作用域的闭包,所以它们当然可以继续访问你提供的环境状态。当然,对于只用回调的设计也是这样的,因此这并不是Promise特有的优点——但不管怎样,这仍然是我们可以依靠的一个保证。

吞掉错误或异常
基本上,这部分是上个要点的再次说明。如果拒绝一个Promise并给出一个理由(也就是一个出错消息),这个值就会被传给拒绝回调。
如果在Promise的创建过程中或在查看其决议结果过程中的任何时间点上出现了一个JavaScript异常错误,比如一个TypeError或ReferenceError,那这个异常就会被捕捉,并且会使这个Promise被拒绝:

var p = new Promise(function (resolve, reject) {
    foo.bar(); // foo未定义,所以会出错!
    resolve(42); // 永远不会到达这里 :(
});
p.then(
    function fulfilled() {
        // 永远不会到达这里 :(
    },
    function rejected(err) {
        // err将会是一个TypeError异常对象来自foo.bar()这一行
    }
);

foo.bar()中发生的JavaScript异常导致了Promise拒绝,你可以捕捉并对其作出响应。
这是一个重要的细节,因为其有效解决了另外一个潜在的Zalgo风险,即出错可能会引起同步响应,而不出错则会是异步的。Promise甚至把JavaScript异常也变成了异步行为,进而极大降低了竞态条件出现的可能。
但是,如果Promise完成后再查看结果时(then(..)注册的回调中)出现了JavaScript异常错误会怎样呢?即使这些异常不会被丢弃,但你会发现,对它们的处理方式还是有点出乎意料,需要进行一些深入研究才能理解:

var p = new Promise(function (resolve, reject) {
    resolve(42);
});
p.then(
    function fulfilled(msg) {
        foo.bar();
        console.log(msg); // 永远不会到达这里 :(
    },
    function rejected(err) {
        // 永远也不会到达这里 :(
    }
);

这看起来像是foo.bar()产生的异常真的被吞掉了。别担心,实际上并不是这样。但是这里有一个深藏的问题,就是我们没有侦听到它。p.then(..)调用本身返回了另外一个promise,正是这个promise将会因TypeError异常而被拒绝。
为什么它不是简单地调用我们定义的错误处理函数呢?表面上的逻辑应该是这样啊。如果这样的话就违背了Promise的一条基本原则,即Promise一旦决议就不可再变。p已经完成为值42,所以之后查看p的决议时,并不能因为出错就把p再变为一个拒绝。
除了违背原则之外,这样的行为也会造成严重的损害。因为假如这个promise p有多个then(..)注册的回调的话,有些回调会被调用,而有些则不会,情况会非常不透明,难以解释。

是可信任的Promise吗
Promise并没有完全摆脱回调,它们只是改变了传递回调的位置。我们并不是把回调传递给foo(..),而是从foo(..)得到某个东西(外观上看是一个真正的Promise),然后把回调传给这个东西。
但是,为什么这就比单纯使用回调更值得信任呢?如何能够确定返回的这个东西实际上就是一个可信任的Promise呢?这难道不是一个(脆弱的)纸牌屋,在里面只能信任我们已经信任的?
关于Promise的很重要但是常常被忽略的一个细节是,Promise对这个问题已经有一个解决方案。包含在原生ES6 Promise实现中的解决方案就是Promise.resolve(..)。
如果向Promise.resolve(..)传递一个非Promise、非thenable的立即值,就会得到一个用这个值填充的promise。下面这种情况下,promise p1和promise p2的行为是完全一样的。

var p1 = new Promise(function (resolve, reject) {
    resolve(42);
});
var p2 = Promise.resolve(42);

而如果向Promise.resolve(..)传递一个真正的Promise,就只会返回同一个promise:

var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( p1 );
p1 === p2; // true

更重要的是,如果向Promise.resolve(..)传递了一个非Promise的thenable值,前者就会试图展开这个值,而且展开过程会持续到提取出一个具体的非类Promise的最终值。

var p = {
    then: function (cb) {
        cb(42);
    }
};
// 这可以工作,但只是因为幸运而已
p.then(
    function fulfilled(val) {
        console.log(val); // 42
    },
    function rejected(err) {
        // 永远不会到达这里
    }
);

这个p是一个thenable,但并不是一个真正的Promise。幸运的是,和绝大多数值一样,它是可追踪的。但是,如果得到的是如下这样的值又会怎样呢:

var p = {
    then: function (cb, errcb) {
        cb(42);
        errcb("evil laugh");
    }
};
p.then(
    function fulfilled(val) {
        console.log(val); // 42
    },
    function rejected(err) {
        // 啊,不应该运行!
        console.log(err); // 邪恶的笑
    }
);

这个p是一个thenable,但是其行为和promise并不完全一致。这是恶意的吗?还只是因为它不知道Promise应该如何运作?说实话,这并不重要。不管是哪种情况,它都是不可信任的。
尽管如此,我们还是都可以把这些版本的p传给Promise.resolve(..),然后就会得到期望中的规范化后的安全结果:

Promise.resolve(p)
    .then(
    function fulfilled(val) {
        console.log(val); // 42
    },
    function rejected(err) {
        // 永远不会到达这里
    }
    );

Promise.resolve(..)可以接受任何thenable,将其解封为它的非thenable值。从Promise.resolve(..)得到的是一个真正的Promise,是一个可以信任的值。如果你传入的已经是真正的Promise,那么你得到的就是它本身,所以通过Promise.resolve(..)过滤来获得可信任性完全没有坏处。
假设我们要调用一个工具foo(..),且并不确定得到的返回值是否是一个可信任的行为良好的Promise,但我们可以知道它至少是一个thenable。Promise.resolve(..)提供了可信任的Promise封装工具,可以链接使用:

// 不要只是这么做:
foo(42)
    .then(function (v) {
        console.log(v);
    });
// 而要这么做:
Promise.resolve(foo(42))
    .then(function (v) {
        console.log(v);
    });

对于用Promise.resolve(..)为所有函数的返回值(不管是不是thenable)都封装一层。另一个好处是,这样做很容易把函数调用规范为定义良好的异步任务。如果foo(42)有时会返回一个立即值,有时会返回Promise,那么Promise.resolve(foo(42))就能够保证总会返回一个Promise结果。而且避免Zalgo就能得到更好的代码。

链式流
尽管我们之前对此有过几次暗示,但Promise并不只是一个单步执行this-then-that操作的机制。当然,那是构成部件,但是我们可以把多个Promise连接到一起以表示一系列异步步骤。
这种方式可以实现的关键在于以下两个Promise固有行为特性:

var p = Promise.resolve(21);
var p2 = p.then(function (v) {
    console.log(v); // 21
    // 用值42填充p2
    return v * 2;
});
// 连接p2
p2.then(function (v) {
    console.log(v); // 42
});

但是,如果必须创建一个临时变量p2(或p3等),还是有一点麻烦的:

var p = Promise.resolve(21);
p.then(function (v) {
    console.log(v); // 21
    // 用值42完成连接的promise
    return v * 2;
})
    // 这里是链接的promise
    .then(function (v) {
        console.log(v); // 42
    });

现在第一个then(..)就是异步序列中的第一步,第二个then(..)就是第二步。这可以一直任意扩展下去。只要保持把先前的then(..)连到自动创建的每一个Promise即可。
但这里还漏掉了一些东西。如果需要步骤2等待步骤1异步来完成一些事情怎么办?我们使用了立即返回return语句,这会立即完成链接的promise。
使Promise序列真正能够在每一步有异步能力的关键是,回忆一下当传递给Promise.resolve(..)的是一个Promise或thenable而不是最终值时的运作方式。Promise.resolve(..)会直接返回接收到的真正Promise,或展开接收到的thenable值,并在持续展开thenable的同时递归地前进。
从完成(或拒绝)处理函数返回thenable或者Promise的时候也会发生同样的展开:

var p = Promise.resolve(21);
p.then(function (v) {
    console.log(v); // 21
    // 创建一个promise并将其返回
    return new Promise(function (resolve, reject) {
        // 用值42填充
        resolve(v * 2);
    });
})
    .then(function (v) {
        console.log(v); // 42
    });

虽然我们把42封装到了返回的promise中,但它仍然会被展开并最终成为链接的promise的决议,因此第二个then(..)得到的仍然是42。如果我们向封装的promise引入异步,一切都仍然会同样工作:

var p = Promise.resolve(21);
p.then(function (v) {
    console.log(v); // 21
    // 创建一个promise并返回
    return new Promise(function (resolve, reject) {
        // 引入异步!
        setTimeout(function () {
            // 用值42填充
            resolve(v * 2);
        }, 100);
    });
})
    .then(function (v) {
        // 在前一步中的100ms延迟之后运行
        console.log(v); // 42
    });

为了进一步阐释链接,让我们把延迟Promise创建(没有决议消息)过程一般化到一个工具中,以便在多个步骤中复用:

function delay(time) {
    return new Promise(function (resolve, reject) {
        setTimeout(resolve, time);
    });
}
delay(100) // 步骤1
    .then(function STEP2() {
        console.log("step 2 (after 100ms)");
        return delay(200);
    })
    .then(function STEP3() {
        console.log("step 3 (after another 200ms)");
    })
    .then(function STEP4() {
        console.log("step 4 (next Job)");
        return delay(50);
    })
    .then(function STEP5() {
        console.log("step 5 (after another 50ms)");
    })
...

调用delay(200)创建了一个将在200ms后完成的promise,然后我们从第一个then(..)完成回调中返回这个promise,这会导致第二个then(..)的promise等待这个200ms的promise。

如前所述,严格地说,这个交互过程总有两个promise:200ms延迟promise,和第二个then(..)链接到的那个链接promise。但是你可能已经发现了,在脑海中把这两个promise合二为一之后更好理解,因为promise机制已经自动为你把它们的状态合并在了一起。这样一来,可以把return delay(200)看作是创建了一个promise,并用其替换了前面返回的链接promise。

但说实话,没有消息传递的延迟序列对于Promise流程控制来说并不是一个很有用的示例。这里不用定时器,而是构造Ajax请求:

// 假定工具ajax( {url}, {callback} )存在
// Promise-aware ajax
function request(url) {
    return new Promise(function (resolve, reject) {
        // ajax(..)回调应该是我们这个promise的resolve(..)函数
        ajax(url, resolve);
    });
}

我们首先定义一个工具request(..),用来构造一个表示ajax(..)调用完成的promise:

request("http://some.url.1/")
    .then(function (response1) {
        return request("http://some.url.2/?v=" + response1);
    })
    .then(function (response2) {
        console.log(response2);
    });

利用返回Promise的request(..),我们通过使用第一个URL调用它来创建链接中的第一步,并且把返回的promise与第一个then(..)链接起来。
response1一返回,我们就使用这个值构造第二个URL,并发出第二个request(..)调用。第二个request(..)的promise返回,以便异步流控制中的第三步等待这个Ajax调用完成。最后,response2一返回,我们就立即打出结果。
我们构建的这个Promise链不仅是一个表达多步异步序列的流程控制,还是一个从一个步骤到下一个步骤传递消息的消息通道。
如果这个Promise链中的某个步骤出错了怎么办?错误和异常是基于每个Promise的,这意味着可能在链的任意位置捕捉到这样的错误,而这个捕捉动作在某种程序上就相当于在这一位置将整条链“重置”回了正常运作:

// 步骤1:
request("http://some.url.1/")
    // 步骤2:
    .then(function (response1) {
        foo.bar(); // undefined,出错!
        // 永远不会到达这里
        return request("http://some.url.2/?v=" + response1);
    })
    // 步骤3:
    .then(
    function fulfilled(response2) {
        // 永远不会到达这里
    },
    // 捕捉错误的拒绝处理函数
    function rejected(err) {
        console.log(err);
        // 来自foo.bar()的错误TypeError
        return 42;
    }
    )
    // 步骤4:
    .then(function (msg) {
        console.log(msg); // 42
    });

第2步出错后,第3步的拒绝处理函数会捕捉到这个错误。拒绝处理函数的返回值(这段代码中是42),如果有的话,会用来完成交给下一个步骤与(第4步)的promise,这样,这个链现在就回到了完成状态。

正如之前讨论过的,当从完成处理函数返回一个promise时,它会被展开并有可能延迟下一个步骤。从拒绝处理函数返回promise也是如此,因此如果在第3步返回的不是42而是一个promise的话,这个promise可能会延迟第4步。调用then(..)时的完成处理函数或拒绝处理函数如果抛出异常,都会导致(链中的)下一个promise因这个异常而立即被拒绝。

如果你调用promise的then(..),并且只传入一个完成处理函数,一个默认拒绝处理函数就会顶替上来:

var p = new Promise(function (resolve, reject) {
    reject("Oops");
});
var p2 = p.then(
    function fulfilled() {
        // 永远不会达到这里
    }
    // 假定的拒绝处理函数,如果省略或者传入任何非函数值
    // function(err) {
    // throw err;
    // }
);

如你所见,默认拒绝处理函数只是把错误重新抛出,这最终会使得p2(链接的promise)用同样的错误理由拒绝。从本质上说,这使得错误可以继续沿着Promise链传播下去,直到遇到显式定义的拒绝处理函数。
如果没有给then(..)传递一个适当有效的函数作为完成处理函数参数,还是会有作为替代的一个默认处理函数:

var p = Promise.resolve(42);
p.then(
    // 假设的完成处理函数,如果省略或者传入任何非函数值
    // function(v) {
    // return v;
    // }
    null,
    function rejected(err) {
        // 永远不会到达这里
    }
);

你可以看到,默认的完成处理函数只是把接收到的任何传入值传递给下一个步骤(Promise)而已。

then(null,function(err){ .. })这个模式——只处理拒绝(如果有的话),但又把完成值传递下去——有一个缩写形式的API:catch(function(err){ .. })。

简单总结一下使链式流程控制可行的Promise固有特性:

别人想让你看到什么,你的能力又能让你看到什么
上一篇下一篇

猜你喜欢

热点阅读