想搞懂async?先從單向鏈表講起

async怎麼用

async函數返回一個Promise對象,可使用then方法添加回調函數。當函數執行的時候,一旦遇到await就會先返回,等到異步操做完成,再接着執行函數體內後面的語句。html

舉一個栗子:git

let timeout = ms => {
    console.log('a')
    return new Promise(resolve => {
        console.log('b')
        setTimeout(resolve, ms)
    })
}

let asyncPrint = async (value, ms) => {
    await timeout(ms)
    console.log(value)
    return `${value} world`
}
asyncPrint('hello', 500).then(_ => {
    console.log(_)
})//先依次輸出 a b 而後500毫秒後輸出hello 和 hello world
複製代碼

async 函數有多種形式es6

// 函數聲明
async function foo() {}

// 函數表達式
const foo = async function () {};

// 對象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 箭頭函數
const foo = async () => {};
複製代碼

注意⚠️:github

  • async函數返回的是promise對象,因此能夠用then接受函數內部return的數據
  • await命令後面通常跟着Promise對象,會等着返回Promise的結果,而後執行後面的。若是不是Promise對象,好比是123,這時等於 return 123。
  • await命令目前只容許存在與async函數裏面

好了,若是你只是想了解async函數和await怎麼用的,到這裏就能夠了。shell


那麼json

async究竟是什麼

一句話,它就是Generator函數的語法糖。promise

而Generator的實現不得不依賴於Iterator接口。bash

而Iterator的實現思想又來源於單向鏈表數據結構

因此咱們先來講說單向鏈表多線程

單向鏈表

wiki:鏈表(Linked list)是一種常見的基礎數據結構,是一種線性表,可是並不會按線性的順序儲存數據,而是在每個節點裏存到下一個節點的指針(Pointer)。因爲沒必要須按順序儲存,鏈表在插入的時候能夠達到 o(1)的複雜度,比另外一種線性表順序錶快得多,可是查找一個節點或者訪問特定編號的節點則須要 o(n)的時間,而順序表響應的時間複雜度分別是 o(logn)和 o(1)。

因此單向鏈表的優勢就是:

  • 無需預先分配內存
  • 插入/刪除節點不影響其餘節點,效率高(典型的栗子:dom操做)

簡單來講,單向鏈表是鏈表中最簡單的一種,它包含兩個域,一個信息域,一個指針域。這個連接指向列表中的下一個節點,而最後一個節點則指向null

image

單向鏈表的實現能夠看這篇文章: es6實現單向鏈表

Iterator(遍歷器)

Iterator是一種接口,爲各類不一樣的數據結構(原生支持Array Object Map Set)提供統一的訪問機制。任何部署了Iterator接口的數據結構均可以完成遍歷操做。

Iterator的做用:

  • 爲各類數據結構提供一個統一的、簡便的訪問接口
  • 使數據結構的成員可以按某種次序排列
  • 依靠Iterator,能夠實現for...of循環

Iterator的遍歷過程

  • 建立一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。
  • 第一次調用指針對象的next方法,能夠將指針指向數據結構的第一個成員。
  • 第二次調用指針對象的next方法,指針就指向數據結構的第二個成員。
  • 不斷調用指針對象的next方法,直到它指向數據結構的結束位置。

每一次調用next方法,都會返回數據結構的當前成員的信息。具體來講,就是返回一個包含value和done兩個屬性的對象。其中,value屬性是當前成員的值,done屬性是一個布爾值,表示遍歷是否結束。

根據以上的規則,咱們來簡單實現一個next方法

let makeIterator = array => {
    let nextIndex = 0;
    return {
        next: _ => nextIndex < array.length ?
        { value: array[nextIndex++], done: false }:
        { value: undefined, done: true }
    }//返回的是一個指針對象
}

let it = makeIterator(['a','b','c','d'])
it.next()
it.next()
it.next()
it.next()
it.next()
it.next()
//next方法用來移動指針,第一次調用會指向a,而且返回一個對象,表示當前數據成員的信息,這個對象具備value和done兩個屬性,第二次調用指向b。。。
複製代碼

若是使用 TypeScript 的寫法,遍歷器接口(Iterable)、指針對象(Iterator)和next方法返回值的規格能夠描述以下。

interface Iterable {
  [Symbol.iterator]() : Iterator,
}

interface Iterator {
  next(value?: any) : IterationResult,
}

interface IterationResult {
  value: any,
  done: boolean,
}
複製代碼

接口部署

ES6規定,默認的Iterator接口部署在數據結構的Symbol.iterator屬性上(symbol是什麼),就是說,一個數據結構只要具備Symbol.iterator屬性,就能夠認爲是可遍歷的(Iterable)。

再舉個栗子:

let arr = ['a','b','c']
let iter = arr[Symbol.iterator]()
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
複製代碼

那麼原生有哪些具有Iterator接口的數據結構呢

  • Array
  • Map
  • Set
  • String
  • arguments對象
  • NodeList 對象

調用Iterator接口的場合

  • 解構賦值
  • 擴張運算符
  • Array.form()
  • Map(), Set()(好比new Map([['a',1],['b',2]]))
  • Promise.all()
  • Promise.race()
  • ==for...of==
const arr = ['red', 'green', 'blue'];

for(let v of arr) {
  console.log(v); // red green blue
}

const obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);

for(let v of obj) {
  console.log(v); // red green blue
} 
複製代碼
  • ==Generator函數==

==========================================我是分割線

什麼是Generator函數

形式上,Generator函數有兩個特徵。

  • function關鍵字與函數名之間有一個*號
  • 函數體內部使用yield表達式,定義不一樣的內部狀態
function* myGenerator() {
    yield 'hi'
    yield 'lueluelue'
    return 'byebye'
}
let myG = myGenerator()
myG.next() //{value: "hi", done: false}
myG.next() //{value: "lueluelue", done: false}
myG.next() //{value: "byebye", done: true}
myG.next() //{value: undefined, done: true}
複製代碼

經過上面的代碼,能夠得出,Generator函數是一個指針對象生成函數,返回的指針對象,能夠依次遍歷Generator函數內部的每個狀態。 與普通函數不一樣的是。調用Generator函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象。

Generator函數與Iterator接口的關係

根據以前

任意一個對象的Symbol.iterator方法,等於該對象的遍歷器生成函數,調用該函數會返回該對象的一個指針對象。

又由於Generator函數就是遍歷器生成函數,所以能夠把Generator函數賦值給對象的Symbol.iterator屬性,從而使該對象具備Iterator接口。 好比:

let myIterable = {}
myIterable[Symbol.iterator] = function* (){
	yield 1
	yield 2
	yield 3
}
[...myIterable] // 1,2,3
複製代碼

那麼aynsc和Generator又是什麼關係呢? 回答這個問題以前,咱們須要知道什麼是異步。 =============================================我是分割線

異步

咱們都知道,js是單線程的,通俗的講就是代碼一行行的執行唄。但是要是遇到了下面這種狀況呢?

image
上圖的綠色部分是程序的運行時間,紅色部分是等待時間。能夠看到,因爲I/O操做很慢,因此這個線程的大部分運行時間都在空等I/O操做的返回結果。

那爲何不用多線程呢?

若是採用多線程,同時運行多個任務,那極可能就是下面這樣。

image
能夠看到,多線程不只佔用多倍的系統資源,也閒置多倍的資源,這顯然不合理。

那麼要怎麼解決這個問題呢?這時候就用到了Event Loop。

Event Loop是一個程序結構,用於等待和發送消息和事件

簡單說,就是在程序中設置兩個線程:一個負責程序自己的運行,稱爲"主線程";另外一個負責主線程與其餘進程(主要是各類I/O操做)的通訊,被稱爲"Event Loop線程"(能夠譯爲"消息線程")。

image

上圖主線程的綠色部分,仍是表示運行時間,而橙色部分表示空閒時間。每當遇到I/O的時候,主線程就讓Event Loop線程去通知相應的I/O程序,而後接着日後運行,因此不存在紅色的等待時間。等到I/O程序完成操做,Event Loop線程再把結果返回主線程。主線程就調用事先設定的回調函數,完成整個任務。

這種運行方式稱爲"異步模式"

咱們怎麼實現異步呢?

比較傳統的幾種方法:

  • 回調函數
  • 事件監聽
  • 發佈/訂閱
  • Promise 對象

好比回調函數(callback)

fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
  if (err) throw err;
  console.log(data);
});
複製代碼

上面代碼中,readFile函數的第三個參數,就是回調函數,也就是任務的第二段。等到操做系統返回了/etc/passwd這個文件之後,回調函數纔會執行。

可是回調函數很容易出現一個問題

fs.readFile(fileA, 'utf-8', function (err, data) {
  //do something
  fs.readFile(fileB, 'utf-8', function (err, data) {
    //do something
    fs.readFile(fileC, 'utf-8', function (err, data) {
        // ...
      });
  });
});
複製代碼

這種時候就出現了多重嵌套,因爲多個文件造成了強耦合,只要有一個操做須要修改,它的上層回調函數和下層回調函數,可能都要跟着修改。這種狀況就稱爲"回調函數地獄"

Promise就很好的解決了這個問題

var fs = require('fs');

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) return reject(error);
      resolve(data);
    });
  });
};


readFile(fileA)
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileB);
})
.then(function (data) {
  console.log(data.toString());
})
.then(function () {
  return readFile(fileC);
})
.then(function (data) {
  console.log(data.toString());
})
.catch(function (err) {
  console.log(err);
});
複製代碼

能夠看到使用了Promise之後,異步任務看的比較的清楚了,但又有了一個問題,那就是代碼顯得很冗餘,一眼看去是一堆的then,語義變得不清楚。

那麼,有沒有更好的寫法呢?


Generator函數

var fs = require('fs');

var readFile = function (fileName){
  return new Promise(function (resolve, reject){
    fs.readFile(fileName, function(error, data){
      if (error) return reject(error);
      resolve(data);
    });
  });
};

var gen = function* (){
  var f1 = yield readFile('/etc/fstab');
  console.log(f1.toString());
  var f2 = yield readFile('/etc/shells');
  console.log(f2.toString());
};
複製代碼

而後,咱們能夠手動執行

var g = gen();

g.next().value.then(function(data){
  g.next(data).value.then(function(data){
    g.next(data);
  });
});

複製代碼

手動執行其實就是用then方法,層層添加回調函數。理解了這一點,就能夠寫出一個自動執行器。

function run(gen){
  var g = gen();

  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }

  next();
}

run(gen);
複製代碼

或者用co模塊

上面代碼中,只要 Generator 函數還沒執行到最後一步,next函數就調用自身,以此實現自動執行。


說了這麼多,開始切入正題

那麼到底什麼是 aynsc呢

// 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函數就是自帶了執行器 的Generator函數的語法糖,咱們只須要把'*'換成async,把yield換成await就能夠了。

相關文章
相關標籤/搜索