从一个简单的实例看 JavaScript 的异步编程进化历
发布于 6 年前 作者 guowenfh 2279 次浏览 来自 分享

回调地狱

例子如下:

我们有 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/

3 回复

例子十 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了

回到顶部