【异步机制】
javascript提供了丰富的异步机制提升处理性能,大概有下面几种:
- 回调函数
- 事件监听
- 发布/订阅
- Promise 对象
- Generator 函数 其中最后两种是ES6提出的语法,也是目前很常用的异步方案。此外还有基于Generator 的语法糖 async/await关键字,也非常好用。
Promise
简单说就是一个容器对象,里面保存着某个未来才会结束的事件(通常是一个异步操作)的状态,状态包含已完成、已失败,在定义Promise时,需要注明什么情况下对应什么状态,并调用相应的回调方法,在使用Promise时则要通过.then和.catch方法分别传入已完成和已失败状态的回调方法(建议用.catch代替.then可传入的reject方法)。其好处是统一了语法格式,更好的语义,避免回调地域。
我们先看一个最简单的Promise例子:生成一个0-2之间的随机数,如果小于1,则等待一段时间后返回成功,否则返回失败:
function test(resolve, reject) {
var timeOut = Math.random() * 2;
log('set timeout to: ' + timeOut + ' seconds.');
setTimeout(function () {
if (timeOut < 1) {
log('call resolve()...');
resolve('200 OK');
}
else {
log('call reject()...');
reject('timeout in ' + timeOut + ' seconds.');
}
}, timeOut * 1000);
}
这个test()函数有两个参数,这两个参数都是函数,如果执行成功,我们将调用resolve('200 OK'),如果执行失败,我们将调用reject('timeout in ' + timeOut + ' seconds.')。可以看出,test()函数只关心自身的逻辑,并不关心具体的resolve和reject将如何处理结果。
有了执行函数,我们就可以用一个Promise对象来执行它,并在将来某个时刻获得成功或失败的结果:
var p1 = new Promise(test);
var p2 = p1.then(function (result) {
console.log('成功:' + result);
});
var p3 = p2.catch(function (reason) {
console.log('失败:' + reason);
});
变量p1是一个Promise对象,它负责执行test函数。注意,在new Promise时,test函数就已经开始执行了,通常会在该函数中执行一些异步逻辑并在异步完成时调用resole/reject从而执行绑定的回调函数(此时早已退出new Promise的执行过程)。正是基于这种语法逻辑,使用Promise可以避免多层异步操作的回调地域。
由于test函数在内部是异步执行的,当test函数执行成功时,我们告诉Promise对象:
// 如果成功,执行这个函数:
p1.then(function (result) {
console.log('成功:' + result);
});
当test函数执行失败时,我们告诉Promise对象:
p2.catch(function (reason) {
console.log('失败:' + reason);
});
// Promise对象可以串联起来,所以上述代码可以简化为:
new Promise(test).then(function (result) {
console.log('成功:' + result);
}).catch(function (reason) {
console.log('失败:' + reason);
});
Promise.all
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (results) {
console.log(results); // 获得一个Array: ['P1', 'P2']
});
Promise.race
// 有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。或者增加一个Promise指定超时时间。这种情况下,用Promise.race()实现:
var p1 = new Promise(function (resolve, reject) {
setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise(function (resolve, reject) {
setTimeout(resolve, 600, 'P2');
});
Promise.race([p1, p2]).then(function (result) {
console.log(result); // 'P1'
});
// 由于p1执行较快,Promise的then()将获得结果'P1'。p2仍在继续执行,但执行结果将被丢弃。
Util.loading();
Promise.race([
window.fetch(this.qryServer.url, {
method: "POST",
headers: {
"mode": "cors",
"Content-Type": "application/json",
"Authorization": this.qryServer.authorization
},
cache: 'no-cache',
body: JSON.stringify({
"uid": this.telUid,
"calledNbr": phone
})
}),
new Promise((resolve, reject) => {
setTimeout(reject, 20 * 1000, new Error('服务连接超时'));
})
]).then(res => {
if (res.status !== 200) {
reject(new Error(`Response Error: ${res.statusText}`));
}else {
return res.json();
}
}).then(data => {
if (data.code === "200") {
this.setState({
voiceRecords: data.data
});
} else {
Util.alert("查询失败" + data.msg);
}
}).catch(err => {
console.error(err);
Util.alert(err.message);
}).finally(Util.unloading);
如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行。
Generator
简单说就是一个状态机,封装了多个内部状态并提供一个遍历器对象。形式上,Generator 是一个带关键字 * 的函数,内部通过yield定义内部状态。调用 Generator 函数后将暂停执行,等待调用next方法一步一步的执行并返回相应状态。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
第一次调用next,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。(注意表达式已求值,基于此特性可完成异步操作) 第二次调用next ,从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。 第三次调用next ,继续执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。 第四次调用next ,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。通过next方法的参数向函数体内部注入值,从而调整函数行为。 上面代码中,第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。第三次运行Next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
如果向next方法提供参数,返回结果就完全不一样了。上面代码第一次调用b的next方法时,返回x+1的值6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5,y等于24,所以return语句的值等于42。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
由于生成器函数内部有遍历器对象,因此可以用for、while循环自动调用next执行,这个特性可以用来自动执行。
对比各种异步机制
// **异步回调**
const fs = require('fs');
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
// **Promise实现的异步**
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
// **生成器实现的异步**
// 好处是可以从外部读取yield值并且通过next传入参数
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
// **async/await语法糖实现的异步**
// 好处是简洁直观,异步操作像串行执行一样
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};