异步
在 JavaScript
界,异步是获取数据的主旋律,大概有以下几种方式:
callback
回调Promise
event
事件
简单说说以上 3
种模式:
回调
将需要执行的函数传入异步调用内,在异步调用结束后,主动执行传入的函数。
function doSomeAsync(callback) {
let data = {/* 数据 */};
setTimeout(() => {
callback(data);
});
}
doSomeAsync((data) => {
// do some thing
doSomeAsync((data) => {
// do some thing
});
});
但试想一下,如果说回调之中还有回调,那么将会形成一个类似金字塔的结构(回调地狱),代码将变难以维护。
Promise
出现了回调地狱,我们得解决,这就出现了 Promise
,将上面回调的例子改写成 Promise
的形式。
let promise = () => {
return new Promise((resolve, reject) => {
let data = {/* 数据 */}
setTimeout(() => {
resolve();
})
})
};
promise().then(res => {
// do some thing
return promise()
}).then(res => {
// do some thing
return promise()
})
虽然这样写代码上看起来和回调的形式差不多,但如果在一个异步操作之后还要继续进行异步操作的话,在调用一次 then
方法即可。
事件
事件是 发布/订阅
模式的实现,具体的过程如下:
- 在异步操作结束前,定义操作结束后的事件名以及对应的事件处理函数。
- 异步操作结束时,触发相应事件。
关于事件类该如何实现,可以参考我自己实现的一个事件类 Event。
ok 把上面的例子改一改:
let event = new Event();
event.$on('asyncEnd', () => {
// do some thing
})
setTimeout(() => {
// do some thing
event.$emit('asyncEnd');
})
ok 主流的异步解决方式都已经说完,我们来看看 Generator
函数在异步中的应用。
Generator 的异步使用
先来个 Generator
函数
function* gen(x) {
let y = yield x + 2;
console.log(y);
let z = yield y + 3;
console.log(z);
return z
}
然后调用
let g = gen(1)
gen.next() // {value: 3, done: false}
gen.next(3) // {value: 6, done: false}
gen.next(6) // {value: 6, done: true}
注意:next
中传的参数即为上一次调用 next
返回值中的 value
也就是 yield
表达式的返回值。如果对于 next
参数的使用有疑问,请参考 Generator 中的 next。
接下来,我们来写一个函数让 next
方法自动执行。
function autoGen(gen){
let genReturn = {
value: undefined,
done: false
};
do {
genReturn = gen.next(genReturn.value);
} while(!genReturn.done)
}
autoGen(gen(1))
// 3
// 6
这是同步模式下 Generator
函数自动化执行的实现,那我们现在来改一改,将 yield
后的表达式改为一个异步调用( Promise
)。
function createPromise(num){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num);
})
});
};
function* genPromise(x) {
let y = yield createPromise(x + 2);
console.log(y);
let z = yield createPromise(y + 3);
console.log(z);
return z;
}
按照之前的调用形式,得到的 value
将会是一个 Promise
,而且对于变量 y
和 z
来说,更想要的应该是异步结束后的结果,那我们调整一下自动执行函数的内部逻辑,使得 next
方法中的参数是异步结束后的结果。
function autoGenForPromise(gen){
// 获得第一个 promise
let promiseChain = gen.next().value;
function next(){
// promise.then 能拿到异步的结果
// 所以将 next 方法放在 then 方法内执行
promiseChain.then(res => {
let innerReturn = gen.next(res);
// 判断 Generator 函数是否已经执行完毕
if(!innerReturn.done){
// 获取下一个 promise
promiseChain = innerReturn.value;
// 递归调用
next();
}
})
};
next();
}
autoGenForPromise(genPromise(1));
// 3
// 6
同样的只是在不断的调用 next
方法,区别只是同步模式下用循环,异步模式下是递归而已。
我们实现了基于 Promise
的 Generator
函数的自动化,那么该考虑考虑,如何进行基于回调(callback
)的自动化了。
具体如何实现的过程不做过多的解读,主要看代码
// 参数和回调分两个步骤传入异步中
function createCallback(num){
return function(callback){
setTimeout(() => {
callback(num);
})
}
}
function* genCallback(x) {
let y = yield createCallback(x + 2);
console.log(y);
let z = yield createCallback(y + 3);
console.log(z);
return z;
}
不同于 Promise
的代码,genCallback
生成的 Generator
函数调用 next
返回的 value
是一个需要执行的函数,函数执行后会进行真正的异步调用。
接着我们来让它实现异步自动化执行
function autoGenForCallback(gen){
// 获取第一个 callback
let nextResutl = gen.next();
function next(){
// 返回的 value 允许我们传入一个函数
nextResutl.value((res) => {
nextResutl = gen.next(res);
// 判断是否结束
if(!nextResutl.done){
next();
}
})
}
next();
}
autoGenForCallback(genCallback(1));
// 3
// 6
至此,我们基本上实现了对异步形式的控制,不管是如何实现自动化的,其 Generator
函数大致是不会变的,都是如下的格式:
function* gen(x) {
let y = yield; // 一个表达式
// 对于上一个 yield 的返回值进行的操作
console.log(y);
let z = yield; // 一个表达式
// 对于上一个 yield 的返回值进行的操作
console.log(z);
return z;
}
而我们之所以将不同的形式拆成不同的方法,其实也是为了方便理解,想象一下,如果我们在自动化函数中做了对不同形式的兼容,不就成了一个完美的 Generator
函数异步自动执行的方案了?
至于这个方案的具体代码,可以查看 npm
上一个叫 co 的模块,感兴趣的可以去了解下。
总结
有了 Generator
函数加上一个良好的自动化执行函数后,我们就可以像写同步代码那样写异步代码了,大体的结构就是上面说的那个样子。
function* gen(x) {
let y = yield; // 一个表达式
// 对于上一个 yield 的返回值进行的操作
console.log(y)
let z = yield // 一个表达式
// 对于上一个 yield 的返回值进行的操作
console.log(z)
return z
}
autoGen(gen(/* initValue */))
其实这段代码像极了 ES7
中的 async/await
,只不过这里是 */yield/autoGen
。其实在 ES7
中 async/await
仅仅不过是一层语法糖罢了,其本质是 Promise
的使用,在 JavaScript
中,是不可能实现正真的同步,设计如此,而这些语法的实现,不得不说思维的伟大。