愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

《ECMAScript6入门》学习笔记之Promise

一、异步编程

由于JavaScript引擎是基于单线程、事件循环的,所以同一时刻只允许一个代码块在执行。在同一时刻中,JavaScript引擎只能执行一个代码块,而其他不在同一个事件循环中的代码块会被放在任务队列中,当当前代码块执行完毕后,事件循环(event loop)会执行队列中的下一个任务。传统上,实现异步编程的解决方案有:

1、事件模型

事件模型使用的是观察者模式,典型的例子如:

let button = document.getElementById('my-btn');
button.onclick = function() {
    // ...
}

这样子当click事件发生时,所绑定的这个事件处理程序就会被执行。但是事件模型有一些缺点,如:
1)要保证事件处理程序在事件触发前绑定,如果错过了事件发生的时机再绑定,则不会响应
2)在实现多个独立的异步任务的连接调用时会很复杂,因为必须跟踪每个事件的事件目标(如例子是跟踪button

2、回调函数

例子如:

readFile('example.txt', function(err, contents) {
    // ...
});

通过传入一个函数作为参数,便能够使得在异步操作处理完成后,就执行对应的回调函数,从而实现异步处理。但是回调函数形式的异步编程也有显著的特点:当异步里嵌套异步的时候,就会出现非常多的回调层级,从而造成回调地狱(callback hell),从而难以调试

Promise,则是为了解决传统异步编程方案而引入的更加优雅的异步编程解决方案


二、Promise的基本用法

1、状态

一个Promise对象有三种状态:pending(进行中)、fulfilled(已成功)、rejected(已失败)
Promise对象一经创建,就会立即执行,无法中途取消。而一旦状态发生改变,就不会再变,所以和事件模型不同的是,我们任何时候都能处理Promise对象异步操作的结果

2、生成实例

可以使用Promise构造函数来生成一个Promise实例,如:

const p = new Promise((resolve, reject) => {
    // ...
    if (asyncOperationSucceed) {
        resolve(value);
    } else {
        reject(error)
    }
});

其中:
1)resolve表示异步操作已经完成,执行后将使得Promise对象的状态从pending变为fulfilled
2)reject表示异步操作失败,执行后,Promise的对象的状态从pending变为fulfilled

3、处理异步操作结果

当一个Promise对象生成后,我们就可以使用then()catch()来分别处理resolve后或者reject后的结果,即相当于成功时的回调和失败时的回调,如:

p.then(function(value) {
    // ...
})
p.catch(function(error) {
    // ...
})

而事实上,then()方法的第二个参数可以指定reject时的处理程序,如:

p.then(function(value) {
    // when succeed
}, function(error) {
    // when failed
})

catch()方法实际上相当于:

p.then(null, function(error) {
    // ...
});

而这两个方法均能够接受一个回调函数,回调函数里包含有value或者error,而这个value或者error,是resolve()或者reject()调用时所传入的实参。基于以上知识,我们可以写出一个简单的Promise如:

function ajaxGet(url) {
    return new Promise(function(resolve, reject) {
        const xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (this.readyState !== 4) {
                return;
            }
            if (this.status === 200) {
                resolve(this.response);
            } else {
                reject(new Error(this.statusText));
            }
        }
        xhr.open('GET', url);
        xhr.send();
    });
}

const p = ajaxGet('/someurl/somepath');
p.then(value => {
    console.log(value);
});
p.catch(error => {
    console.error(error);
});

由于then()方法每次执行的结果都是返回一个Promise对象,所以可以链式调用,如:

p.then(value => {
    console.log(value);
}).catch(error => {
    console.error(error);
});
// 这相当于:
const p2 = p.then(value => {
    console.log(value);
});
p2.catch(error => {
    console.error(error);
});

4、嵌套的Promise

Promise中给resolve()传入的值,也可以是另一个Promise对象,如:

const p1 = new Promise((resolve, reject) => {
    // ...
});
const p2 = new Promise((resolve, reject) => {
    // ...
    resolve(p1);
});

这种情况下,p2的状态就取决于p1,当p1是pending时,p2的回调函数就会等p1状态的改变,而如果p1已经完成了,那么p2的回调函数就会立即执行,如:

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('fail')), 3000);
});
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => resolve(p1), 1000);
});
p2
.then(result => console.log(result))
.catch(error => console.log(error));

上例中,p2在1秒后resolve,而p1则在3秒后reject,由于p2的resolve返回的是p1,所以 p2自身的状态无效了,p1的状态会决定p2的状态,p2的then()方法和catch()方法,实际上是对p1的状态做出响应。
注意:在resolve()或者reject()后续还有操作的话,是会执行的,如:

new Promise((resolve, reject) => {
    resolve(1);
    console.log(2);
}).then(v => {
    console.log(1);
});
// 输出:2 1


三、Promise.prototype.then()方法

then()方法用来绑定异步操作的处理程序,then()方法可以接收两个参数,第一个参数表示成功时的回调,而第二个参数表示失败时的回调。
then()方法返回的是一个 新的Promise()实例,所以then()方法可以链式调用,如:

let times = 0;
function timeout(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(`Hello, world ${times++}`), ms);
    });
}

timeout(1000).then(v => {
    console.log(v);
    return timeout(1000);
}).then(v => {
    console.log(v);
});
// Hello world 0
// Hello world 1

而如果then()方法返回的不是一个Promise对象的话,那么它也会被转化为一个Promise()对象,如:

timeout(1000).then(v => {
    console.log(v);
    return 'Better!!';
}).then(v => {
    console.log(v);
});
// Hello, world 0
// Better!!

此外,可以对一个Promise对象绑定多个then(),当resolve的时候,绑定的这几个then()都会执行一遍,如:

function timeout(ms) {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve('Hello'), ms);
    });
}

const p = timeout(1000)
p.then((data) => {
    console.log('Use data first time: ', data);
});
p.then((data) => {
    console.log('Use data second time: ', data);
})

会在1s后同时输出:

Use data first time: Hello
Use data second time: Hello


四、Promise.prototype.catch()

该方法是Promise.prototype.then(null, handler)的别名,当发生以下情况时,catch()所指定的处理程序都能得以执行:
1)一个Promise对象reject的时候
2)一个Promise对象内部发生错误的时候,如:

const p = new Promise((resolve, reject) => {
    throw new Error('err');
});
p.then(data => {
    console.log('OK');
}).catch(data => {
    console.log('Fail');
});
// 输出:Fail

3)前面的then()函数体里发生错误的时候
如果Promise状态已经resolve了,那么这个时候再抛出错误,则是无效的,如:

const p = new Promise((resolve, reject) => {
    resolve();
    throw new Error('err');
});
p.then(data => {
    console.log('OK');
}).catch(data => {
    console.log('Fail');
});
// 输出:OK

在Promise中,错误会被往下传递,直到被捕获为止,如:

const p = getSomePromise();
p.then(data => {
    // ...
}).then(data => {
    // ...
}).catch(error => {
    // ...
});

这里如果第一个then中出现错误,那么会被传递下去,直到最后一个catch捕获了它。同样的,只要两个中有任何一个发生错误,都会被catch捕获。
需要注意的是:只要其中一个环节出现了错误,就会跳过接下来的then(),直接进入catch()
不过,如果没有指定catch()处理程序的话,那么通常情况下Promise内部的错误,是没有办法被外层代码响应的。不过,看以下代码:

const p = new Promise((resolve, reject) => {
    resolve('OK');
    setTimeout(() => {
        throw new Error('error occurred')
    }, 0);
});

这种情况下,错误会在下一个事件循环中才抛出,而在下个事件循环中,Promise就已经运行结束了,所以错误会相当于在Promise体外抛出,成为未捕获错误。


五、Promise.all()

Promise.all()用于将多个Promise实例包装成一个新的Promise实例:

const p = Promise.all([p1, p2, p3]);

其接受的参数必须是一个iterable对象(不一定要是数组),此外每个成员都应该是Promise实例(如果不是的话,则调用Promise.resolve转换)
p的状态是由p1、p2、p3共同决定的:
1)p1、p2、p3都fullfilled,那么p才会是fullfilled,p传给回调函数的参数是一个数组,包含了p1、p2、p3的返回值
2)p1、p2、p3中有一个rejected,那么p就rejected,p传给回调函数的参数是第一个被rejected的实例的返回值
例如:

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => resolve(`p1 on ${+new Date()}`), 1000);
});
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => resolve(`p2 on ${+new Date()}`), 2000);
})
const p3 = new Promise((resolve, reject) => {
    setTimeout(() => resolve(`p3 on ${+new Date()}`), 3000);
})

console.log(`now is ${+new Date()}`);
const p = Promise.all([p1, p2, p3]);
p.then(v => {
    console.log(v);
}).catch(e => {
    console.log(e);
});

输出如下:

now is 1506656345 322
fullfilled on 1506656348 326
[
"p1 on 1506656346 323",
"p2 on 1506656347 324",
"p3 on 1506656348 326"
]

可以发现,p在3秒后状态变为fullfilled,然后返回值中包含了3个promise实例各自的返回值。如果其中有一个promise实例rejected,那么就会进入到catch块中,如:


输出如下:

now is 1506664899 171
rejected on 1506664901 173
p2 on 1506664901 173

即2秒后,p2的状态变为rejected,即p的状态也变为rejected。然后回调函数的参数里得到的是p2在reject时带的值


六、Promise.race()

Promise.all() 方法也是将多个Promise实例包装成一个新的Promise实例:

const p = Promise.race([p1, p2, p3]);

p的状态取决于p1、p2、p3中最先改变状态的那个promise实例的状态。
注意:Promise.race()接受参数情况类似于Promise.all(),如果可迭代成员里有非promise实例,就会先调用Promise.resolve方法转为promise实例,然后再进一步处理。
这个方法可以用来实现超时调用,如有p1和p2两个操作,如果在1s内都没有操作完成,就报超时错误:

const p = Promise.race([
    p1,
    p2,
    new Promise((resolve, reject) => {
        setTimeout(() => reject(new Error('request timeout')), 1000);
    })
]);


七、Promise.resolve()

Promise.resolve()能够将现有的方法转为Promise对象,基本用法如:

const p = Promise.resolve(someValue);

以上代码相当于:

const p = new Promise(resolve => resolve(someValue));

传递给它的参数有如下的情况:
1)参数是一个Promise实例
这种情况下,Promise.resolve将不做任何修改,会原封不动地返回这个Promise对象:

const p = new Promise(resolve => resolve('Hello'));
Promise.resolve(p) === p; // true

2)参数是一个thenable对象
thenable对象指的是具有then方法的对象,当参数是一个thenable对象时,会用then方法来转化promise对象,如:

const obj = {
    then(resolve, reject) {
        setTimeout(() => resolve('Hello'), 1000);
    }
}

const p = Promise.resolve(obj);
p.then(v => console.log(v)); // 1秒后输出:Hello

3)参数是非thenable对象,或者根本就不是对象
这种情况下,会直接返回一个新的Promise对象,状态直接为resolved:

const p1 = Promise.resolve('Hello');
p1; // Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: "Hello"}

4)不带任何参数
这种情况下,直接返回的是一个resolved状态的Promise对象,如:

const p = Promise.resolve();
// 相当于
const p = new Promise(resolve => resolve());

这种立即resolve的Promise对象,会在本轮事件循环的结束时执行,而非下一轮事件循环时执行:

setTimeout(() => console.log('three'));
Promise.resolve().then(() => console.log('two'));
console.log('one');

输出:

one
two
three


八、Promise.reject()

用法如:Promise.reject(reason),这会返回一个新的Promise实例,而这个实例的状态为rejected:

const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'));

需要特别注意的是:Promise.reject()方法的参数,会原封不动地传递给后续方法,这和Promise.resolve()是很不一样的,如:

const obj = {
    then(resolve, reject) {
        console.log('出错了!');
    }
}

const p = Promise.reject(obj).catch(x => console.log(x === obj)); // true


九、Promise.try()

Promise.try()的主要作用在于提供一种统一的机制,保证无论在代码块里执行的是同步代码、异步代码或者是不同Promise实现得到的Promise实例,都能够以一种统一的方式来处理,有点类似于try-catch中的try,它目前还是一种提案,ES6 Promise尚未实现它,但是Bluebird已经有了。
一个典型的应用,是更好的错误处理:
我们希望一个Promise函数里,不论发生了什么错误,都能够使用catch()来注册处理函数,但是有如下的情况:

function getUserName(uid) {
    return getUser({uid})
    .then(res => {
        console.log('res');
    })
    .catch(err => {
        console.error('catched: ', err);
    });
}
getUserName(123);

假设这里getUser()执行的时候抛出了一个异常,那么控制台会得到:

Uncaught Error: error!

可见,发生了错误的时候,并没有进入catch()块,而使用Promise.try(),则可以解决这个问题:

function getUserName(uid) {
    return Promise.try(() => {
        getUser({uid})
    }).then(res => {
        console.log('res');
    })
    .catch(err => {
        console.error('catched: ', err);
    });
}

这将得到:

catched: Error: error!