Async/Await 如何经过同步的方式实现异步

做为前端人员要回答这个问题,须要了解这三个知识点:javascript

  • 同步
  • 异步
  • Async/Await

首先,js 是单线程的(重复三遍),所谓单线程, 通俗的讲就是,一根筋(比喻有点过度,哈哈)执行代码是一行一行的往下走(即所谓的同步), 若是上面的没执行完,就痴痴的等着(是否是很像恋爱中在路边等她/他的你,伪装 new 了个对象,啊哈哈哈,调皮一下很开心), 仍是举个 🌰 吧:前端

// chrome 75
function test() {
  let d = Date.now();
  for (let i = 0; i < 1e8; i++) {}
  console.log(Date.now() - d); // 62ms左右
}
function test1() {
  let d = Date.now();

  console.log(Date.now() - d); // 0
}
test();
test1();
复制代码

上面仅仅是一个 for 循环,而在实际应用中,会有大量的网络请求,它的响应时间是不肯定的,这种状况下也要痴痴的等么?显然是不行的,于是 js 设计了异步,即 发起网络请求(诸如 IO 操做,定时器),因为须要等服务器响应,就先不理会,而是去作其余的事儿,等请求返回告终果的时候再说(即异步)。 那么如何实现异步呢?其实咱们平时已经在大量使用了,那就是 callback,例如:java

$.ajax({
  url: 'http://xxx',
  success: function(res) {
    console.log(res);
  },
});
复制代码

success 做为函数传递过去并不会当即执行,而是等请求成功了才执行,即回调函数(callback)git

const fs = require('fs');
fs.rename('旧文件.txt', '新文件.txt', err => {
  if (err) throw err;
  console.log('重命名完成');
});
复制代码

和网络请求相似,等到 IO 操做有告终果(不管成功与否)才会执行第三个参数:(err)=>{}es6

从上面咱们就能够看出,实现异步的核心就是回调钩子,将 cb 做为参数传递给异步执行函数,当有告终果后在触发 cb。想了解更多,去看看 event-loop 机制吧。github

至于 async/await 是如何出现的呢,在 es6 以前,大多 js 数项目中会有相似这样的代码:面试

ajax1(url, () => {
  ajax2(url, () => {
    ajax3(url, () => {
      // do something
    });
  });
});
复制代码

这种函数嵌套,大量的回调函数,使代码阅读起来晦涩难懂,不直观,形象的称之为回调地狱(callback hell),因此为了在写法上能更通俗一点,es6+陆续出现了 PromiseGeneratorAsync/await,力求在写法上简洁明了,可读性强。ajax

=========================我是分割线==========================chrome

以上只是铺垫,下面在进入正题 👇,开始说道说道主角:async/await编程

=========================我是分割线==========================

async/await 是参照 Generator 封装的一套异步处理方案,能够理解为 Generator 的语法糖,

因此了解 async/await 就不得不讲一讲 Generator,

Generator 又依赖于迭代器Iterator

因此就得先讲一讲 Iterator,

Iterator 的思想呢又来源于单向链表,

终于找到源头了:单向链表

1. 单向链表

wiki:链表(Linked list)是一种常见的基础数据结构,是一种线性表,可是并不会按线性的顺序储存数据,而是在每个节点里存到下一个节点的指针(Pointer)。因为没必要须按顺序储存,链表在插入的时候能够达到 o(1)的复杂度,比另外一种线性表顺序表快得多,可是查找一个节点或者访问特定编号的节点则须要 o(n)的时间,而顺序表响应的时间复杂度分别是 o(logn)和 o(1)。

总结一下链表优势:

  • 无需预先分配内存
  • 插入/删除节点不影响其余节点,效率高(典型的例子:git commit、dom 操做

单向链表:是链表中最简单的一种,它包含两个域,一个信息域和一个指针域。这个连接指向列表中的下一个节点,而最后一个节点则指向一个空值。

image
一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的连接

单链特色:节点的连接方向是单向的;相对于数组来讲,单链表的的随机访问速度较慢,可是单链表删除/添加数据的效率很高。

理解 js 原型链/做用域链的话,理解这个很容易,他们是相通的。编程语言中,数组的长度时固定的,因此数组中的增长和删除比较麻烦,须要频繁的移动数组中的其余元素,而 js 做为一门动态语言,数组本质是一个相似数组的对象,是动态的,不须要预先分配内存

那么如何设计一个单向链表呢?这个取决于咱们须要哪些操做,一般有:

  • append(element):追加节点
  • insert(element,index):在索引位置插入节点
  • remove(element):删除第一个匹配到的节点
  • removeAt(index):删除指定索引节点
  • removeAll(element):删除全部匹配的节点
  • get(index):获取指定索引的节点信息
  • set(element,index):修改指定索引的节点值
  • indexOf(element):获取某节点的索引位置
  • clear():清除全部节点
  • length():返回节点长度
  • printf():打印节点信息

看到这些方法是否是有些许熟悉,当你用原生 js 或 jq 时常会用上面相似的方法,如今根据上面列出的方法进行实现一个单向链:

// 节点模型
class LinkNode {
  constructor(element, next) {
    this.element = element;
    this.next = next;
  }
}

class LinkedList {
  constructor() {
    this._head = null;
    this._size = 0;
    this._errorBoundary = this._errorBoundary.bind(this);
    this._getNodeByIndex = this._getNodeByIndex.bind(this);
    this.append = this.append.bind(this);
    this.insert = this.insert.bind(this);
    this.remove = this.remove.bind(this);
    this.removeAt = this.removeAt.bind(this);
    this.removeAll = this.removeAll.bind(this);
    this.getElement = this.getElement.bind(this);
    this.setIndex = this.setIndex.bind(this);
    this.indexOf = this.indexOf.bind(this);
    this.clear = this.clear.bind(this);
    this.length = this.length.bind(this);
    this.printf = this.printf.bind(this);
  }

  // 边界检验
  _errorBoundary(index) {
    if (index < 0 || index >= this._size) {
      throw `超出边界(${0}~${this._size}),目标位置${index}不存在!`;
    }
  }
  // 根据索引获取目标对象
  _getNodeByIndex(index) {
    this._errorBoundary(index);
    let obj = this._head;
    for (let i = 0; i < index; i++) {
      obj = obj.next;
    }
    return obj;
  }
  // 追加节点
  append(element) {
    if (this._size === 0) {
      this._head = new LinkNode(element, null);
    } else {
      let obj = this._getNodeByIndex(this._size - 1);
      obj.next = new LinkNode(element, null);
    }
    this._size++;
  }
  // 在索引位置插入节点
  insert(element, index) {
    if (index === 0) {
      this._head = new LinkNode(element, this._head);
    } else {
      let obj = this._getNodeByIndex(index - 1);
      obj.next = new LinkNode(element, obj.next);
    }
    this._size++;
  }
  // 删除第一个匹配到的节点
  remove(element) {
    if (this._size < 1) return null;

    if (this._head.element == element) {
      this._head.element = this._head.next;
      this._size--;
      return element;
    } else {
      let temp = this._head;
      while (temp.next) {
        if (temp.next.element == element) {
          temp.next = temp.next.next;
          this._size--;
          return element;
        } else {
          temp = temp.next;
        }
      }
    }
    return null;
  }
  // 删除指定索引节点
  removeAt(index) {
    this._errorBoundary(index);
    let element = null;
    if (index === 0) {
      element = this._head.element;
      this._head = this._head.next;
    } else {
      let prev = this._getNodeByIndex(index - 1);
      element = prev.next.element;
      prev.next = prev.next.next;
    }
    this._size--;
    return element;
  }
  // 删除全部匹配的节点
  removeAll(element) {
    // 建立虚拟头节点,
    let v_head = new LinkNode(null, this._head);
    let tempNode = v_head;
    // let tempEle = null;
    while (tempNode.next) {
      if (tempNode.next.element == element) {
        tempNode.next = tempNode.next.next;
        this._size--;
        // tempEle = element;
      } else {
        tempNode = tempNode.next;
      }
    }
    this._head = v_head.next;
  }
  // 获取指定索引的节点信息
  getElement(index) {
    return this._getNodeByIndex(index).element;
  }
  // 修改指定索引的节点值
  setIndex(element, index) {
    this._errorBoundary(index);
    let obj = this._getNodeByIndex(index);
    obj.element = element;
  }
  // 获取某节点的索引位置
  indexOf(element) {
    let obj = this._head;
    let index = -1;
    for (let i = 0; i < this._size; i++) {
      if (obj.element == element) {
        index = i;
        break;
      }
      obj = obj.next;
    }
    return index;
  }
  // 清除全部节点
  clear() {
    this._head = null;
    this._size = 0;
  }
  // 返回节点长度
  length() {
    return this._size;
  }
  // 打印节点信息
  printf() {
    let obj = this._head;
    const arr = [];
    while (obj != null) {
      arr.push(obj.element);
      obj = obj.next;
    }
    const str = arr.join('->');
    return str || null;
  }
}

const obj = new LinkedList();
obj.append(0);
obj.append(1);
obj.append(2);
obj.printf();
// "0->1->2"

obj.insert(3, 3);
obj.printf();
// "0->1->2->3"

obj.remove(3);
obj.printf();
// "0->1->2"

obj.removeAt(0);
obj.printf();
// "1->2"

obj.setIndex(0, 0);
obj.printf();
// "0->2"

obj.indexOf(2);
// 1

obj.length();
// 2

obj.clear();
obj.printf();
// null
复制代码

查看源码

经过以上,我伪装你明白什么是单向链表,而且可以用代码实现一个单向链表了,下一步开始说一说迭代器 Iterator

2. Iterator

Iterator 翻译过来就是**迭代器(遍历器)**让咱们先来看看它的遍历过程(相似于单向链表):

  • 建立一个指针对象,指向当前数据结构的起始位置
  • 第一次调用指针对象的 next 方法,将指针指向数据结构的第一个成员
  • 第二次调用指针对象的 next 方法,将指针指向数据结构的第二个成员
  • 不断的调用指针对象的 next 方法,直到它指向数据结构的结束位置

一个对象要变成可迭代的,必须实现 @@iterator 方法,即对象(或它原型链上的某个对象)必须有一个名字是 Symbol.iterator 的属性(原生具备该属性的有:字符串、数组、类数组的对象、Set 和 Map):

属性
[Symbol.iterator]: 返回一个对象的无参函数,被返回对象符合迭代器协议

当一个对象须要被迭代的时候(好比开始用于一个 for..of 循环中),它的 @@iterator 方法被调用而且无参数,而后返回一个用于在迭代中得到值的迭代器

迭代器协议:产生一个有限或无限序列的值,而且当全部的值都已经被迭代后,就会有一个默认的返回值

当一个对象只有知足下述条件才会被认为是一个迭代器:

它实现了一个 next() 的方法,该方法必须返回一个对象,对象有两个必要的属性:

  • done(bool)
    • true:迭代器已经超过了可迭代次数。这种状况下,value 的值能够被省略
    • 若是迭代器能够产生序列中的下一个值,则为 false。这等效于没有指定 done 这个属性
  • value 迭代器返回的任何 JavaScript 值。done 为 true 时可省略

根据上面的规则,我们来自定义一个简单的迭代器:

const makeIterator = arr => {
  let nextIndex = 0;
  return {
    next: () =>
      nextIndex < arr.length
        ? { value: arr[nextIndex++], done: false }
        : { value: undefined, done: true },
  };
};
const it = makeIterator(['人月', '神话']);
console.log(it.next()); // { value: "人月", done: false }
console.log(it.next()); // { value: "神话", done: false }
console.log(it.next()); // {value: undefined, done: true }
复制代码

咱们还能够自定义一个可迭代对象:

const myIterable = {};
myIterable[Symbol.iterator] = function*() {
  yield 1;
  yield 2;
  yield 3;
};

for (let value of myIterable) {
  console.log(value);
}
// 1
// 2
// 3

//or

console.log([...myIterable]); // [1, 2, 3]
复制代码

了解了迭代器,下面能够进一步了解生成器了

3. Generator

Generator:生成器对象是生成器函数(GeneratorFunction)返回的,它符合可迭代协议迭代器协议,既是迭代器也是可迭代对象,能够调用 next 方法,但它不是函数,更不是构造函数

生成器函数(GeneratorFunction):

function* name([param[, param[, ... param]]]) { statements }

  • name:函数名
  • param:参数
  • statements:js 语句

调用一个生成器函数并不会立刻执行它里面的语句,而是返回一个这个生成器的迭代器对象,当这个迭代器的 next() 方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现 yield 的位置为止(让执行处于暂停状),yield 后紧跟迭代器要返回的值。或者若是用的是 yield*(多了个星号),则表示将执行权移交给另外一个生成器函数(当前生成器暂停执行),调用 next() (再启动)方法时,若是传入了参数,那么这个参数会做为上一条执行的 yield 语句的返回值,例如:

function* another() {
  yield '人月神话';
}
function* gen() {
  yield* another(); // 移交执行权
  const a = yield 'hello';
  const b = yield a; // a='world' 是 next('world') 传参赋值给了上一个 yidle 'hello' 的左值
  yield b; // b=! 是 next('!') 传参赋值给了上一个 yidle a 的左值
}
const g = gen();
g.next(); // {value: "人月神话", done: false}
g.next(); // {value: "hello", done: false}
g.next('world'); // {value: "world", done: false} 将 'world' 赋给上一条 yield 'hello' 的左值,即执行 a='world',
g.next('!'); // {value: "!", done: false} 将 '!' 赋给上一条 yield a 的左值,即执行 b='!',返回 b
g.next(); // {value: undefined, done: false}
复制代码

看到这里,你可能会问,Generatorcallback 有啥关系,如何处理异步呢?其实两者没有任何关系,咱们只是经过一些方式强行的它们产生了关系,才会有 Generator 处理异步

咱们来总结一下 Generator 的本质,暂停,它会让程序执行到指定位置先暂停(yield),而后再启动(next),再暂停(yield),再启动(next),而这个暂停就很容易让它和异步操做产生联系,由于咱们在处理异步时:开始异步处理(网络求情、IO 操做),而后暂停一下,等处理完了,再该干吗干吗。不过值得注意的是,js 是单线程的(又重复了三遍),异步仍是异步,callback 仍是 callback,不会由于 Generator 而有任何改变

下面来看看,用 Generator 实现异步:

const promisify = require('util').promisify;
const path = require('path');
const fs = require('fs');
const readFile = promisify(fs.readFile);

const gen = function*() {
  const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
  console.log(res1);
  const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
  console.log(res2);
};

const g = gen();

const g1 = g.next();
console.log('g1:', g1);

g1.value
  .then(res1 => {
    console.log('res1:', res1);
    const g2 = g.next(res1);
    console.log('g2:', g2);
    g2.value
      .then(res2 => {
        console.log('res2:', res2);
        g.next(res2);
      })
      .catch(err2 => {
        console.log(err2);
      });
  })
  .catch(err1 => {
    console.log(err1);
  });
// g1: { value: Promise { <pending> }, done: false }
// res1: {
// "a": 1
// }

// {
// "a": 1
// }

// g2: { value: Promise { <pending> }, done: false }
// res2: {
// "b": 2
// }

// {
// "b": 2
// }
复制代码

以上代码是 Generatorcallback 结合实现的异步,能够看到,仍然须要手动执行 .then 层层添加回调,但因为 next() 方法返回对象 {value: xxx,done: true/false} 因此咱们能够简化它,写一个自动执行器:

const promisify = require('util').promisify;
const path = require('path');
const fs = require('fs');
const readFile = promisify(fs.readFile);

function run(gen) {
  const g = gen();
  function next(data) {
    const res = g.next(data);
    // 深度递归,只要 `Generator` 函数还没执行到最后一步,`next` 函数就调用自身
    if (res.done) return res.value;
    res.value.then(function(data) {
      next(data);
    });
  }
  next();
}
run(function*() {
  const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
  console.log(res1);
  // {
  // "a": 1
  // }
  const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
  console.log(res2);
  // {
  // "b": 2
  // }
});
复制代码

说了这么多,怎么尚未到 async/await,客官别急,立刻来了(其实我已经漏了一些内容没说:Promise 和 callback 的关系,thunk 函数,co 库,感兴趣的能够去 google 一下,yuanyifeng 老师讲的es6 入门很是棒,我时不时的都会去看一看)

4. Async/Await

首先,async/awaitGenerator 的语法糖,上面我是分割线下的第一句已经讲过,先来看一下两者的对比:

// Generator
run(function*() {
  const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
  console.log(res1);
  const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
  console.log(res2);
});

// async/await
const readFile = async ()=>{
  const res1 = await readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' });
  console.log(res1);
  const res2 = await readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' });
  console.log(res2);
  return 'done';
}
const res = readFile();
复制代码

能够看到,async function 代替了 function*await 代替了 yield,同时也无需本身手写一个自动执行器 run

如今再来看看async/await 的特色:

  • await 后面跟的是 Promise 对象时,才会异步执行,其它类型的数据会同步执行
  • 执行 const res = readFile(); 返回的仍然是个 Promise 对象,上面代码中的 return 'done'; 会直接被下面 then 函数接收到
res.then(data => {
  console.log(data); // done
});
复制代码

啊,终于完了,一个 async-await 连带出来这么多知识点,之后面试被问到它的原理时,但愿可以帮助到你

【参考】:

  1. developer.mozilla.org/zh-CN/docs/…
  2. es6.ruanyifeng.com/#docs/itera…
  3. es6.ruanyifeng.com/#docs/async

===🧐🧐 文中不足,欢迎指正 🤪🤪===

相关文章
相关标签/搜索