| JavaScript对话面试官系列 | - 异步解决方案

813 阅读8分钟

起因

当我输入了大量来自优质个人博客文章,经典书籍,名牌讲师的 JavaScript 系统知识之后,我反而变得有些困惑。我明明可以流利的书写八大继承,流利的书写 Underscore 库中的防抖节流,深拷贝……,我也知晓什么是闭包,什么是迭代器,生成器……。但是问题来了,当我试图将一个 JavaScript 核心概念,比如闭包……,介绍给同学,讲解给自己,对话面试官的时候。原本自认为可以脱口而出的语言,到嘴边却显得吞吞吐吐。只能用只言片语,或者东拼西凑的知识点来表达给对方。这无疑让我有了一种,虽然花了大量时间但是却从未拥有过 JavaScript 的感觉。

目的

所以这个系列要尝试解决的问题就是当别人询问或者考察我JavaScript 核心概念的时候,我可以尽可能流畅的,清晰的表达给他人。

期望

希望掘金优秀的前端技术人员和前辈们可以在百忙之中多多补充这篇文章,多多审查这篇文章,多多提问我。我期望可以通过这个系列来解决我当前的问题

你说一下你对 JavaScript 中异步的理解,以及 JavaScript 异步的解决方案

首先回答你对 JavaScript 异步是怎样理解的

首先想和面试官聊一聊我对于异步的认识,异步是计算机科学的一个基本概念,其中一种含义是计算机多线程的异步处理 ,Java 语言当中可以通过线程池来实现异步,但是对于我们 JavaScript 这种单线程语言(一次只能完成一个任务)。我们是通过 JavaScript 的事件循环模型结合浏览器多线程管理回调函数来实现协程作为异步解决方案。也就是将 DOM 操作,HTTP 请求,定时器等的回调函数都交给浏览器的对应线程来管理,有了操作结果之后就将回调函数加入 JavaScript 事件循环模型的消息队列当中,来等待 JavaScript 主线程的执行。所以异步行为就是在等待其他操作完成的同时,也可以运行其他指令,这样可以优化计算量大,时间长的操作。(而同步行为对应内存中顺序执行的处理器指令,同步行为类似于汇编指令的执行过程,汇编指令存放在CS:IP寄存器当中,等待着被 CPU 逐条顺序读取并执行)

其次回答 JavaScript 异步解决方案 - 回调函数处理异步回调

其次想和面试官聊一聊 JavaScript 针对异步回调函数的解决方案,在早期 JavaScript 中,只支持回调函数来处理异步操作。之后出现的 PromiseAsync 提供了更加优雅,简明的方式来处理 JavaScript 异步回调函数。本质上任然是在处理和表达回调函数。

早期回调函数处理异步回调的缺陷十分明显。

  • 第一个是线性理解能力的缺失,我们期望的异步方式是有计划的,线性的。比如:我通过 HTTP 请求,获取数据之后在操作数据,但是回调在表达异步的时候是非顺序的,非线性的。比如:我先定义好拿到数据之后的操作,再去 HTTP 请求。回调使得我们必须在初始化异步操作之前来定义回调。
  • 第二个是嵌套回调异步,如果异步返回值又依赖另一个异步返回值,回调的情况还会进一步复杂,最致命的是如果试图向其中添加新特性,他就立马会变得毛里毛躁,很难以拓展。
  • 第三个是信任缺失,因为没有一种统一的,可以复用的方案来解决异步的信任问题。我们交给第三方库的回调函数,控制权在第三方库手里。 所以我们需要一种更好的方案来优雅的处理异步回调函数。

之后回答 JavaScript 异步解决方案 - Promise 处理异步回调

再来想和面试官聊一聊 Promise 处理异步回调,通过手写Promise过后,我发现Promise在处理回调的核心是resovle一个异步回调函数,这个异步回调函数获取到 then 同步传递的函数之后调用它。

之后想和面试官从三个方面简单介绍一下 Promise。Promise 特性,Promise 静态方法,Promise 实例方法。

  • Promise 是一个有三种状态的对象,pending(待定), fufilled(兑现), rejected(拒绝)。无论落定为哪种状态都是不可逆的。Promise 的状态转换为兑现,就会有一个兑现的值 value,转换为拒绝,就会有拒绝的原因 reason。reson 和 value 会作为参数传递给 Promise 执行器函数当中的 resolve 和 reject 函数参数。
  • Promise 静态方法 :Promise.resove(), Promise.reject(), Promise.all(), Promise.race(),Promise.allSettled(), Promise.any()
  • Promise 实例方法 : Promise.prototype.then(), Promise.prototype.catch(), Promise.prototype.finally()

最后回答 JavaScript 异步解决方案 - 异步函数 async/await

最后我想和面试官聊一聊异步函数 async 来处理异步回调,Promise 处理异步回调,想对于只使用回调来处理异步回调来说已经非常完美,但是如果我们想要使用 Promise 兑现值 value 的时候,只能将处理逻辑全部塞在期约处理函数then当中,当逻辑非常多的情况下就会有一大堆的 then 处理函数 ,所以 async/await 出现了,旨在解决 Promise 处理异步时的代码组织问题,async/await 使得同步书写的代码可以异步执行。

你说一下异步函数 async/await 的原理

首先,异步函数就是生成器函数的语法糖,异步函数 async 原理的核心实现实则是内置了自动化执行器,也就是不需要在像生成器函数表达异步一样,需要结合 co 模块或者thunk 函数来实现自动执行。

co 模块/thunk 函数通过回调函数 / Promise 对象来接收和交还程序的执行权。从而自动化执行生成器函数。co 模块/thunk交还和接收执行权的本质就是等待异步操作的协程有了结果之后,去递归调用传入的回调函数来归还异步函数协程的执行权。相比之下,异步函数比生成器函数有更好的语义 -> async 比 * 明确, await 比 yield 明确,更广的适用性 -> co 模块规定,yield 命令后面只能是 thunk 函数或者是 Promise 对象,await 后面可以是 Promise 对象或者是原始类型的值。

相关代码和概念

thunk 函数实现自动执行生成器函数

  • 在 JavaScript 语言中,Thunk 函数是将多参数函数替换成一个只接受回调函数作为参数的单参数函数。
function thunk(fn) {
  /* 接收参数的函数 */
  return function (...args) {
    /* 接收回调函数的函数(就是下文的next) */
    return function (callBack) {
      /* 执行函数,归还生成器函数执行权 */
      fn.all(this, ...args,callBack);
    };
  };
}
function run(fn) {
  const gen = fn();
  /* thunk函数归还执行权的回调函数 */
  function next(err, data) {
    /* 交出生成器函数的执行权  next参数传递的是上一个yield的返回值 */
    const result = gen.next(data);
    if (result.done) return;
    /* 调用接收回调函数的函数 */
    result.value(next);
  }
  next();
}
/* 示例 */
----
a.txt
Hello Async!
---
const readFileThunk = thunk(fs.readFile);
const gen = function* () {
  const r1 = yield readFileThunk("./a.txt");
  console.log(r1.toString());
  const r2 = yield readFileThunk("./a.txt");
  console.log(r2.toString());
};
run(gen);
// Hello Async!
// Hello Async!

co 模块实现自动执行生成器函数

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function (error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};
function co(gen) {
  const g = gen();
  /* thunk函数归还执行权的回调函数 */
  function next(data) {
    /* 交出生成器函数的执行权 */
    const result = g.next(data);
    if (result.done) return result.value;
    result.value.then(next);
  }
  next();
}
co(gen);
/* 示例 */
----
a.txt
Hello Async!
---
const gen = function* () {
  const f1 = yield readFile("./a.txt");
  const f2 = yield readFile("./a.txt");
  console.log(f1.toString());
  console.log(f2.toString());
};
// Hello Async!
// Hello Async!

thunk/co实现自动执行生成器函数的本质是,yield关键字交出执行权,co/thunk传入回调函数,回调函数调用归还执行权给生成器函数。

协程概念

协程的概念是多个线程互相协作,完成异步任务。

  • 协程(Coroutines)是一种比线程更加轻量级的存在 , 协程是一个特殊的函数,这个函数可以暂停,保留状态后暂时推出,之后可以重新在暂停处恢复运行。
  • 第一步,协程 A 开始执行。
  • 第二步,协程 A 执行到一半,进入暂停,执行权转移到协程 B。
  • 第三步,(一段时间后)协程 B 交还执行权。
  • 第四步,协程 A 恢复执行。 在 ES6 当中可以通过生成器函数可以交出函数的执行权的特性来实现协程。

参考

阮一峰 ECMAScript 6 (ES6)