例子如下:
我们有 A, B, C, D 四个请求获取数据的函数(函数自己实现), C 依赖 B 的结果,D 依赖 ABC 的结果,最终输出 D 的结果。
版本一
// 伪代码
function A(callbak) {
ajax(url, function(res) {
callbak(res);
});
}
function B(callbak) {
ajax(url, function(res) {
callbak(res);
});
}
function C(data, callback) {
ajax(url, data, function(res) {
callbak(res);
});
}
function D(data1, data2, data3, callback) {
ajax(url, { data1, data2, data3 }, function(res) {
callbak(res);
});
}
A(function(resa) {
B(function(resb) {
C(resb, function(resc) {
D(resa, resb, resc, function(resd) {
console.log("this is D result:", resd);
});
});
});
});
emm…代码还是能运行,但是写法丑陋,回调地狱,如果还有请求依赖,得继续回调嵌套 性能太差,没有考虑 A 和 B 实际上是可以并发的。
例子二
函数基础实现如同例子一,但是考虑 A,B 可以并发的。
// 伪代码
let resa = null;
let timer = null;
A(res => {
resa = res;
});
B(resb => {
C(resb, resc => {
timer = setInterval(() => {
if (resa) {
D(resa, resb, resc, resd => {
console.log("this is D result:", resd);
timer && clearInterval(timer);
});
}
}, 100);
});
});
考虑了 A,B 的并发,使用 setInterval 轮询实现,并不一定实时。性能太差。
例子三
// 伪代码
let count = 2;
let resa = null;
let resb = null;
let resc = null;
function done() {
count--;
if (count === 0) {
D(resa, resb, resc, resd => {
console.log("this is D result:", resd);
});
}
}
A(res => {
resa = res;
done();
});
B(datab => {
C(datab, datac => {
resb = datab;
resc = datac;
done();
});
});
使用 计数器实现。性能没什么问题,但是 封装太差,写法恶心。
例子四
// 实现并发
function parallel(tasks, callback) {
let count = tasks.length;
let all = [];
tasks.forEach((fn, index) => {
fn(res => {
all[index] = res;
count--;
if (count === 0) {
callback(all);
}
});
});
}
// 实现串行
function waterfall(tasks, callback) {
let count = tasks.length;
function loop(...args) {
let task = tasks.shift();
task.apply(
null,
args.concat([
(...res) => {
count--;
if (count === 0) {
return callback(res);
}
loop(...res);
}
])
);
}
loop();
}
function A(cb = () => {}) {
setTimeout(() => {
cb("a");
}, 2000);
}
function B(cb = () => {}) {
setTimeout(() => {
cb("b");
}, 1000);
}
function C(datab, cb = () => {}) {
setTimeout(() => {
cb(datab, "c");
}, 1000);
}
function D(data, datab, datac, cb = () => {}) {
cb("d");
}
parallel(
[
A,
cb => {
waterfall([B, C], (datab, datac) => {
cb(datab, datac);
});
}
],
data => {
const [resa, [resb, resc]] = data;
D(resa, resb, resc, resd => {
console.log("this is D result:", resd);
});
}
);
模仿 async.js 提炼出来了 waterfall,parallel,两个流程控制函数。还不错。 但是写法还是麻烦,对于 A,B,C 的实现有要求。得自己考虑好每一次 callback 的值。
async.js 是我认为在目前 JavaScript callback 的终极解决方案了(没用过 fib.js…
推荐查看 github async.js 源码。
waterfall 可以考虑使用函数式的形式实现:
function pipe(...fnList) {
return function(...args) {
const fn = fnList.reduceRight(function(a, b) {
return function(...subArgs) {
return b.apply(this, [].concat(subArgs, a));
};
});
return fn.apply(this, args);
};
}
例子五
function A() {
return fetch("http://google.com");
}
function B() {}
function C() {}
function D() {}
Promise.all[(A(), B().then(b => C(b)))]
.then(([resa,{resb,resc}) => {
return D(resa,resb,resc);
})
.then(resd => {
console.log("this is D result:", resd);
});
使用 Promise 来代替 之前的 callback。好评。 用 Promise.all 来控制并发,使用 .then 串行请求,整体看起来非常舒服了,脱离了回调地狱。
例子六
function A(cb) {
setTimeout(() => {
cb("a");
}, 2000);
}
function B(cb) {
setTimeout(() => {
cb("b");
}, 1000);
}
function C(datab, cb) {
setTimeout(() => {
cb("c");
}, 1000);
}
function D(dataa, datab, datac, cb) {
setTimeout(() => {
cb("d");
}, 1000);
}
function thunk(fn) {
return function(...args) {
return function(callback) {
fn.call(this, ...args, callback);
};
};
}
function scheduler(fn) {
var gen = fn();
function next(data) {
var result = gen.next(data);
if (result.done) return;
// 如果没结束就继续执行
result.value(next);
}
next();
}
// generator 实际代码
function* generatorTask() {
const resa = yield thunk(A)();
const resb = yield thunk(B)();
const resc = yield thunk(C)(resb);
const resd = yield thunk(D)(resa, resb, resc);
console.log("this is D result:", resd);
return null;
}
scheduler(generatorTask);
使用 generator + callback 来控制流程顺序,还是同步写法,看起来还是挺牛逼的。 但是 generator 不会自动执行,需要自己手动写一个执行器,并且依赖于 thunk 函数。麻烦! 等等。。又全变成了串行?垃圾
例子七
function A() {
return new Promise(r =>
setTimeout(() => {
r("a");
}, 2000)
);
}
function B() {
return new Promise(r =>
setTimeout(() => {
r("b");
}, 1000)
);
}
function C(datab) {
return new Promise(r =>
setTimeout(() => {
r("c");
}, 1000)
);
}
function D(dataa, datab, datac) {
return new Promise(r =>
setTimeout(() => {
r("d");
}, 1000)
);
}
function scheduler(fn) {
var gen = fn();
function next(data) {
var result = gen.next(data);
if (result.done) return;
// 如果没结束就继续执行
result.value.then(next);
}
next();
}
// generator 实际代码
function* generatorTask() {
const [resa, { resb, resc }] = yield Promise.all([
A(),
B().then(resb => C(resb).then(resc => ({ resb, resc })))
]);
const resd = yield D(resa, resb, resc);
console.log("this is D result:", resa, resb, resc, resd);
return resd;
}
scheduler(generatorTask);
抛弃了 thunk 函数,修改了一下 A,B,C,D。的实现以及 generator 执行函数 scheduler。 结合了 Promise 重新实现了并发和串行。 再等等??好麻烦啊。。然后并发好像和 generator 没什么关系吧。果然还是 Promise 大法好。
关于 generator 的自动执行建议直接看 github tj/co 的源码。
例子八
function A() {
return fetch("http://google.com");
}
// ...B,C,D
async function asyncTask() {
const resa = await A();
const resb = await B();
const resc = await C(resb);
const resd = await D(resa, resb, resc);
return resd;
}
asyncTask().then(resd => {
console.log("this is D result:", resd);
});
使用 Promise 结合 async/await 的形式 ,看起来非常简洁。也不用自己写执行器了,舒服。 但是和上面有几个版本出现了一样的问题,没有考虑并发的情况,导致性能下降。
例子九,终极方案?
// ...B,C,D
async function asyncBC() {
const resb = await B();
const resc = await c(resb);
return { resb, resc };
}
async function asyncTask() {
// const [resa,{resb,resc}] = await Promise.all([A(), B().then(resb=>C(resb)]);
const [resa, { resb, resc }] = await Promise.all([A(), asyncBC()]);
const resd = await D(resa, resb, resc);
return resd;
}
asyncTask().then(resd => {
console.log("this is D result:", resd);
});
使用 Promise.all 结合 async/await 的形式,考虑了并发和串行,写法简洁。 应该算是目前的终极方案了。 async/await 作为 generator 语法糖还是非常的甜的。
结语:
从上面几个例子我们可以窥探到 JavaScript 对于异步编程体验的一个非常大的进步。
但是同时我们其实可以看到不论是 generator 还是 async/await。其实更多的是基于 Promise 之上的一些语法简化。 没有从 callback 过渡到 Promise 的时候那种真正心灵上的愉悦。
博客原文地址:http://guowenfh.github.io/2018/09/03/2018/javascript-async/
例子十 rxjs终极 数据流动更清晰
import { defer } from 'rxjs'
import { forkJoin, mergeMap, map } from 'rxjs/operators'
// fnA, fnB, fnC, fnD 函数必须返回 Observable
const A$ = defer(() => fetch(...).json())
const BC$ = fnB().pipe(
mergeMap(resB => {
return fnC(resB).pipe(
map(resC => [resB, resC]),
)
}),
)
forkJoin(A$, BC$).pipe(
mergeMap(arr => fnD(arr[0], arr[1][0], arr[1][1])), // 这儿数据展开方式不一定正确
).subscribe(
res => {
console.info(res) // <------- fnD 返回的结果
}
)
@waitingsong 很棒,这个更多得是一种思想上的转变。
@guowenfh rxjs 不仅是处理异步流的一种(略繁琐但极其犀利的)方案,更是一种让你感到行云流水的编程思想。 微软太牛x了