Promise


无论是 Node.js 或者是浏览器中,JavaScript 的运行流程,多半会是非同步方式,就目前来说,可以使用setTimeout来简单地模拟非同步,例如:

function asyncFoo(n, callback) {
    setTimeout(() => {
        callback(n * Math.random());
    }, 2000);    
}

asyncFoo(10, r => console.log(r));

执行完setTimeout后,函数立即返回,而在时间到时,指定的回调函数才会执行,在事情简单时,这种方式没什么问题,然而,若希望事件发生时后,依序执行下一次非同步时,就会引发回调地狱的问题:

asyncFoo(10, r1 => {
    asyncFoo(r1, r2 => {
        asyncFoo(r2, r3 => {
            console.log(r3);
        });
    });
});

就算使用了 ES6 的箭头函数,在这种非同步模式下,可读性也是迅速降低,如果有个asyncFoo可以返回Promise,那事情会好办的多:

function asyncFoo(n) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(n * Math.random()); // 完成约定
        }, 2000);
    });
}

asyncFoo(10)
    .then(r1 => asyncFoo(r1))
    .then(r2 => asyncFoo(r2))
    .then(r3 => console.log(r3));

Promise是 ES6 新增的 API,在创建Promise实例时,可以传入一个回调函数,该函数具有两个参数,可命名为resolvereject,这两个参数会各自接受函数,若调用resolve,表示此次Promise的任务完成,若Promise实例曾使用then组合下一次非同步操作,那么会调用指定的下个函数,then也会返回一个Promise,因此,虽说是非同步,然而编写风格上,就会像是顺序的。

如果指定的任务无法达成,约定就无法满足(fulfilled),此时可以由传入的reject函数来背弃(reject)约定,例如:

new Promise((resolve, reject) => {
    let n = Math.floor(Math.random() * 10);
    if(n !== 0) {
        resolve(n);
    } else {
        reject('zero');
    }
}).then(
    n    => console.log(n),
    shit => console.log('shit happens', shit)
);

Promisethen方法可以接受两个函数,一个是在满足约定时执行,另一个是在背弃约定被时执行,上面的例子在随机数为 0 时会背弃约定。

如果约定在执行任务时发生异常,会隐含地背弃约定,当约定被背弃时,可以在then的第二个参数指定函数来处理,也可以使用catch指定函数来处理:

function dividedRandom(n) {
    return new Promise(resolve => {
        let r = n / Math.floor(Math.random() * 10);
        if(Number.isFinite(r)) {
            resolve(r);
        } else {
            throw 'divided by zero';
        }
    });
}

dividedRandom(10)
    .then(n => console.log(n))
    .catch(err => console.log(err));

当你只关心约定是否被背弃时,可以只编写catch,从而避免了必须在then的第一个参数指定 nope 函数的情况:

dividedRandom(10)
    .then(
         () => {}, // 舍事都不做
         err => console.log(err)
    );

你可以有多个then来组合多个操作,然后接上一个catch,只要先前的约定中,有某个非同步操作发生了错误,就会背弃约定,从而执行catch指定的函数。

如果你有多个Promise,并不关心满足约定的顺序,只要最后的结果是按照指定约定的顺序排列就可以时,可以使用Promise.all,它接受一个Promise组成的数组,例如:

Promise.all([dividedRandom(10), dividedRandom(10), dividedRandom(10)])
       .then(results => console.log(results[0], results[1], results[2]))
       .catch(err    => console.log(err));

如果你有多个Promise,并不关心哪一个约定先满足,只要有个约定满足就可以的话,可以使用Promise.race,它接受一个Promise组成的数组,并返回一个Promise,只要其中有个约定先满足或背弃,返回的Promise就算满足或背弃约定,数组中其他约定依旧会继续其任务,只是无论满足或背弃,都不被Promise.race返回的Promise考量,一个使用Promise.race的例子是:

Promise.race([dividedRandom(10), dividedRandom(10), dividedRandom(10)])
       .then(result => console.log(result))
       .catch(err   => console.log(err));

Promisethen可以接受Promise,如果不在乎前一个Promise的结果,只需要在前一个Promise完成后执行时使用。例如:

asyncFoo(10)
    .then(asyncFoo(20));

当约定与生成器结合时,可以产生有趣的操作风格,例如,若编写一个async函数如下:

function async(g) {
    let it = g();

    function consume(iteratorResult) {
        if(iteratorResult.done) {
            return;
        }

        let iteratorValue = iteratorResult.value;

        if(iteratorValue instanceof Promise) {
            iteratorValue.then(r    => consume(it.next(r)))
                         .catch(err => it.throw(err));
        } else {
            it.throw(`${iteratorValue} not a promise`);
        }
    }

    try {
        consume(it.next());
    } catch(err) {
        it.throw(err);
    }
}

async(function*() {
    let r1 = yield asyncFoo(10);
    let r2 = yield asyncFoo(r1);
    let r3 = yield asyncFoo(r2);
    console.log(r3);
});

跟一开始的这个风格比比看:

asyncFoo(10)
    .then(r1 => asyncFoo(r1))
    .then(r2 => asyncFoo(r2))
    .then(r3 => console.log(r3));

async的版本更像是语法层面的支持,实际上,在 ES8 中新增的asyncawait就在语法层面上提供了这类支持:

async function task() {
    let r1 = await asyncFoo(10);
    let r2 = await asyncFoo(r1);
    let r3 = await asyncFoo(r2);
    console.log(r3);
}

task();

你不用自行编写一个async函数了,而await在语义上,也会比yield来得清楚。


展开阅读全文