为什么需要 promise ?
Javascript
是⼀⻔单线程语⾔,所以早期我们解决异步的场景时,⼤部分情况都是通过回调函数来进⾏。
例如在浏览器中发送 ajax 请求,就是常⻅的⼀个异步场景,发送请求后,⼀段时间服务端响应之后我们才能拿到结果。如果我们希望在异步结束之后执⾏某个操作,就只能通过回调函数这样的⽅式进⾏操作。
var dynamicFunc = function(cb) {
setTimeout(function() {
cb();
}, 1000);
}
dynamicFunc(function() {console.log(123)});
例如上⾯这个例⼦,这⾥的 dynamicFunc
就是⼀个异步的函数,⾥⾯执⾏的
setTimeout
会在
1s
之后调 ⽤传⼊的 cb
函数。按照上⾯的调⽤⽅式,最终
1s
之后,会打印
123
这个结果。
同样的,如果后续还有内容需要在异步函数结束时输出的话,就需要多个异步函数进⾏嵌套,⾮常不利 于后续的维护。
setTimeout(function() {
console.log(123);
setTimeout(function() {
console.log(321);
// ...
}, 2000);
}, 1000);
为了能使回调函数以更优雅的⽅式进⾏调⽤,在 ES6
中
js
产⽣了⼀个名为
promise
的新规范,他让异步操作的变得近乎「同步化」。
Promise 基础
在⽀持
ES6
的⾼级浏览器环境中,我们通过
new Promise()
即可构造⼀个
promise
实例。
这个构造函数接受⼀个函数,分别接受两个参数,
resolve
和
reject
,代表着我们需要改变当前实例的状
态到
已完成
或是
已拒绝
。
function promise1() {
return new Promise(function(resolve, reject) {
// 定义异步的内容
setTimeout(function() {
console.log('1s 后输出');
// 输出完成后,调⽤函数传⼊的 resolve 函数,将该 promise 实例标记为已完成,当前 promise 串
⾏继续执⾏
resolve();
}, 1000);
});
}
function promise2() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('2s 后输出');
resolve();
}, 2000);
});
}
上⾯的两个
promise
实例,串联起来即可写为:
promise1().then(function() { return promise2(); });
也可以简写为
promise1().then(promise2);
浏览器中执⾏之后,即可看到,
1s
之后出现
1s
后输出
字样,再经过
2s
出现
2s
后输出
字样。在这个例⼦中我们能看到。当前 promise
如果状态变为已完成(执⾏了
resolve
⽅法),那么就会去执⾏
then
⽅法中的下⼀个 promise
函数。
同样的,如果我们的
promise
变为已拒绝状态(执⾏了
reject
⽅法),那么就会进⼊后续的异常处理函数中。
function promise3() {
return new Promise(function(resolve, reject) {
var random = Math.random() * 10; // 随机⼀个 1 - 10 的数字
setTimeout(function() {
if (random >= 5) {
resolve(random);
} else {
reject(random);
}
}, 1000);
});
}
var onResolve = function(val) {
console.log('已完成:输出的数字是', val);
};
var onReject = function(val) {
console.log('已拒绝:输出的数字是', val);
}
// promise 的 then 也可以接受两个函数,第⼀个参数为 resolve 后执⾏,第⼆个函数为 reject 后执⾏
promise3().then(onResolve, onReject);
// 也可以通过 .catch ⽅法拦截状态变为已拒绝时的 promise
promise3().catch(onReject).then(onResolve);
// 也可以通过 try catch 进⾏拦截状态变为已拒绝的 promise
try {
promise3().then(onResolve);
} catch (e) {
onReject(e);
}
这个例⼦使⽤了三种⽅式拦截最终变为「已拒绝」状态的
promise
,分别是
使⽤
then
的第⼆个参数,使
⽤
.catch
⽅法捕获前⽅
promise
抛出的异常,使⽤
try catch
拦截代码块中
promise
抛出的异常
同时我们还可以发现,在改变
promise
状态时调⽤
resolve
和
reject
函数的时候,也可以给下⼀步
then中执⾏的函数传递参数。这个例⼦中我们把随机⽣成的数字传给了 resolve
和
reject
函数,我们也就能在then 中执⾏函数的时候拿到这个值。
总结⼀下本⼩节的内容:
- promise 会有三种状态,「进⾏中」「已完成」和「已拒绝」,进⾏中状态可以更改为已完成或已拒绝,已经更改过状态后⽆法继续更改(例如从已完成改为已拒绝)。
-
ES6 中的 Promise 构造函数,我们构造之后需要传⼊⼀个函数,他接受两个函数参数,执⾏第⼀个参数之后就会改变当前 promise 为「已完成」状态,执⾏第⼆个参数之后就会变为「已拒绝」状态。
-
通过 .then ⽅法,即可在上⼀个 promise 达到已完成时继续执⾏下⼀个函数或 promise。同时通过resolve 或 reject 时传⼊参数,即可给下⼀个函数或 promise 传⼊初始值。
-
已拒绝的 promise ,后续可以通过 .catch ⽅法或是 .then ⽅法的第⼆个参数或是 try catch 进⾏捕 获。
如何封装异步操作为 promise
我们可以将任何接受回调的函数封装为⼀个
promise
,下⾯举⼏个简单的例⼦来说明。
// 原函数
function dynamicFunc(cb) {
setTimeout(function() {
console.log('1s 后显示');
cb();
}, 1000);
}
var callback = function() {
console.log('在异步结束后 log');
}
// ⽤传⼊回调函数的⽅式执⾏
dynamicFunc(callback);
上⾯的例⼦就是最传统的,使⽤传⼊回调函数的⽅式在异步结束后执⾏函数。我们可以通过封装
promise
的⽅式,将这个异步函数变为
promise
。
function dynamicFuncAsync() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('1s 后显示');
resolve();
});
});
}
var callback = function() {
console.log('在异步结束后 log');
}
dynamicFuncAsync().then(function() { callback(); });
再举⼀个例⼦,发送
ajax
请求也可以进⾏封装:
function ajax(url, success, fail) {
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
success(this.response);
} else {
fail(new Error(this.statusText));
}
};
client.send();
};
ajax('/ajax.json', function() {console.log('成功')}, function() {console.log('失败')});
我们可以看到,调⽤
ajax
⽅法时需要传⼊
success
和
fail
的回调函数进⾏调⽤。我们可以不传⼊回调函数,通过封装 promise
的⽅式,在原来的执⾏回调函数的地⽅更改当前
promise
的状态,就可以通过链式调⽤。
function ajaxAsync(url) {
return new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
client.send();
});
};
ajax('/ajax.json')
.catch(function() {
console.log('失败');
})
.then(function() {
console.log('成功');
})
总结⼀下当前⼩节:
- 我们可以轻松的把任何⼀个函数或者是异步函数改为 promise,尤其是异步函数,改为 promise 之后即可进⾏链式调⽤,增强可读性。
- 将带有回调函数的异步改为 promise 也很简单,只需要在内部实例化 promise 之后,在原来执⾏回调函数的地⽅执⾏对应的更改 promise 状态的函数即可。