Skip to content

JS 的异步模式

Callback

当一个函数传入另外一个函数当作参数时,把传入的当作参数的函数叫做 Callback 函数,另外一个函数叫 Hight order function 高阶函数

Callback 存在 2 个问题:

  • 控制反转(inversion of control) 控制反转的意思是, Callback 函数 不能保证按照预期肯定会被执行。
  • 难以理解

回调地狱

经常被人误解

javascript
setTimeout(function () {
  output("one");
  setTimeout(function () {
    output("two");
    setTimeout(function () {
      output("three");
    }, 1000);
  }, 1000);
}, 1000);

可以改成下面没有嵌套,容易理解的

js
function output(msg) {
  console.log(msg);
}

function one(cb) {
  output("one");
  setTimeout(cb, 1000);
}

function two(cb) {
  output("two");
  setTimeout(cb, 1000);
}

function three() {
  output("three");
}

function start(cb) {
  setTimeout(cb, 1000);
}

start(function () {
  one(function () {
    two(three);
  });
});

说明回调地狱的问题不在于嵌套,问题在复杂的例子不太好写。

实战

假设我们有三个不同的文件 file1,file2,file3,我们希望并行请求这些文件,并按照顺序依次展示出文件内容。

普通的实现方式:

js
const arr = ["file1", "file2", "file3"];
const time = {
  file1: 3000,
  file2: 2000,
  file3: 1000
};
const objFiles = {
  // file1: 'file1_content',
  // file2: 'file2_content',
  // file3: 'file3_content',
};

function fakeAjax(file, callback) {
  setTimeout(() => {
    callback(file + "_content");
  }, time[file]);
}

function getFile(file) {
  fakeAjax(file, function (text) {
    fileReceived(file, text);
  });
}

function fileReceived(file, text) {
  objFiles[file] = text;
  for (let i = 0; i < arr.length; i++) {
    const item = arr[i];
    const text = objFiles[item];
    if (text) {
      console.log(text);
    } else {
      // 保证按顺序输出, 如果第一项内容没有值, 则直接 return
      return;
    }
  }
  console.log("done");
}

getFile(arr[0]);
getFile(arr[1]);
getFile(arr[2]);

thunk

什么是 thunk? 只要调用 thunk 函数,返回一个固定的值。

同步 thunk

直接调用就能返回你需要的值。

javascript
const add = (a, b) => {
  return a + b;
};

const thunk = () => {
  return add(1, 2);
};

thunk();

异步 thunk

需要传入一个回调函数, 回调函数的参数就是返回值。

js
const add = (a, b, cb) =>
  setTimeout(() => {
    cb(a + b);
  }, 1000);
const thunk = (cb) => add(1, 2, cb);
thunk((num) => console.log(num));

利用 thunk 来重写上面的例子:

javascript
const arr = ["file1", "file2", "file3"];

function fakeAjax(file, callback) {
  const time = {
    file1: 1000,
    file2: 2000,
    file3: 3000
  };
  setTimeout(() => {
    callback(file + "_content");
  }, time[file]);
}

function getFile(file) {
  var cnt;
  fakeAjax(file, function (text) {
    if (cnt) {
      // 如果提前执行了下面的 cnt = callback, 则直接执行回调函数
      cnt(text);
    } else {
      // 否则赋值给 cnt
      cnt = text;
    }
  });

  return function (callback) {
    if (cnt) {
      // 如果 fakeAjax 是同步执行, 已经赋值给了 cnt = text, 则直接执行回调
      callback(cnt);
    } else {
      // 否则把 cnt 赋值给 callback (PS: 通常是走到这里)
      cnt = callback;
    }
  };
}

var gF1 = getFile(arr[0]);
var gF2 = getFile(arr[1]);
var gF3 = getFile(arr[2]);
const outPut = (text) => console.log(text);

gF1((text) => {
  outPut(text);
  gF2((text) => {
    outPut(text);
    gF3((text) => {
      outPut(text);
      outPut("done");
    });
  });
});

thunk 的优点是代码已经好理解来, 缺点是:

  • 回调函数是受外界控制,会不会被调用不知道。
  • 会被调用几次也是不知道的,而 Promise 如果 resolve 多次的话,只有最开始的 1 次是有效的。

Promise

重写上面的例子

javascript
const arr = ["file1", "file2", "file3"];

function fakeAjax(file, callback) {
  const time = {
    file1: 1000,
    file2: 2000,
    file3: 3000
  };
  setTimeout(() => {
    callback(file + "_content");
  }, time[file]);
}

function getFile(file) {
  return new Promise((resolve, reject) => {
    fakeAjax(file, (text) => {
      resolve(text);
    });
  });
}

var getFile1 = getFile(arr[0]);
var getFile2 = getFile(arr[1]);
var getFile3 = getFile(arr[2]);

const output = (text) => console.log(text);

getFile1.then((text) => {
  output(text);
  getFile2.then((text) => {
    output(text);
    getFile3.then((text) => {
      output(text);
      output("done");
    });
  });
});

// 这里其实用下面的写法更好理解
gF1
  .then(output)
  .then(() => gF2)
  .then(output)
  .then(() => gF3)
  .then(output)
  .then(() => console.log("done"));

// 或者用函数式编程的方法
["file1", "file2", "file3"]
  .map((item) => getFile(item))
  .reduce((a, b) => {
    return a.then(() => b).then(output);
  }, Promise.resolve());

Promise 核心在于 then 后面的注册函数只会被调用 1 次,要么成功要么失败,控制权在自己手里。 缺点:

  • 一旦开始执行就不能中止
  • catch 里面的错误没办法捕获, 除非 catch 后面再写个 catch, 这样就变成无限循环了。

Generator

generator 函数特点:

  • 函数可暂停和继续
  • 可以返回值给外部
  • 也可以传入值进来

若用 Generator 重写上面的例子:

javascript
function runGenerator(ge) {
  let it = ge();
  const loop = (val) => {
    let res = it.next(val);
    if (res.done) {
      return;
    }
    if (res.value.then) {
      res.value.then(loop);
    } else {
      loop(res.value);
    }
  };
  loop();
}

function* main() {
  let f1 = getFile("file1");
  let f2 = getFile("file2");
  let f3 = getFile("file3");
  output(yield f1);
  output(yield f2);
  output(yield f3);
  console.log("done");
}

runGenerator(main);

上述代码中,我们用 Promise + generator 模拟了 async 函数。

Async await 函数

缺点

  • await 后面只能跟一个 Promise 或者 一个纯 js 基本类型(数组, 对象之类的)
  • 无法在外界取消一个正在运行中的 async 函数
javascript
async function main() {
  let f1 = getFile("file1");
  let f2 = getFile("file2");
  let f3 = getFile("file3");
  output(await f1);
  output(await f2);
  output(await f3);
  console.log("done");
}

main();