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

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

相關文章
相關標籤/搜索