前端日拱一卒D11——ES6筆記之異步篇

前言

餘爲前端菜鳥,感姿式水平匱乏,難觀前端之大局。遂決定循前端知識之脈絡,以興趣爲引,輔以幾分堅持,望於己能解惑致知、於同道能助力一二,豈不美哉。前端

本系列代碼及文檔均在 此處node

繼續啃老本...讓人又愛又恨的異步c++

開始以前

  • 同步和異步git

    function sync(){
      const doA = '12'
      const doB = '34'
    }
    function async(){
      ajax('/api/doC1', (res) => {
        doC2(res)
      })
    }
    複製代碼

    同步很好理解,任務一個個執行,doA之後才能doB。github

    異步任務能夠理解爲分兩個階段,doC的前一階段是發出請求,後一階段是在請求結束後的將來時刻處理。web

    二者各有優劣,同步任務會致使阻塞,異步任務須要由有機制實現先後兩部分的分離,使得主線程可以在這間歇內繼續工做而不浪費時間等待。ajax

    以瀏覽器爲例大體過程:shell

    主線程調用web api,經過工做線程發起請求,而後主線程繼續處理別的任務(這是part1)。工做線程執行完了異步任務之後往事件隊列裏註冊回調,等待主線程空閒後去隊列中取出到主線程執行棧中執行(這是part2)。編程

  • 併發和並行api

    簡單描述:併發是交替作不一樣事情,並行是同時作不一樣事情。

    咱們能夠經過多線程去處理併發,但說到底CPU只是在快速切換上下文來實現快速的處理。而並行則是利用多核,同時處理多個任務。

  • 單線程和多線程

    咱們總說js是單線程的,node是單線程的,其實這樣的說法並不完美。所謂單線程指的是js引擎解釋和執行js代碼的線程是一個,也便是咱們常說的主線程。

    又好比對於咱們熟悉的node,I/O操做實際上都是經過線程池來完成的,js->調用c++函數->libuv方法->I/O操做執行->完畢後js線程繼續執行後續。

lesson1 Promise

callback

ajax('/a', (res) => {
  ajax('/b, (res) => {
    // ...
  })
})
複製代碼

醜陋的callback形式,再也不多說

你的名字

  • Promise 誕於社區,初爲異步編程之解決方案,後有ES6將其寫入語言標準,終成今人所言之 Promise 對象
  • Promise對象特色有二:狀態不受外界影響、一旦狀態改變後不會再次改變

基本用法

  • Promise爲構造函數,用於生成Promise實例
    // 接收以resolve和reject方法爲參數的函數
    const pr = new Promise((resolve, reject) => {
      // do sth
      resolve(1) // pending -> resolved
      reject(new Error()) // pending -> rejected
    })
    複製代碼
  • 使用then方法傳入狀態更改後的回調函數
    pr.then((value) => {
      // onresolved cb
    }, (err) => {
      // onrejected cb
    })
    複製代碼

我愚蠢的孩子們

  • Promise.prototype.then

    採用鏈式寫法,返回一個新的Promise,上一個回調的返回做爲參數傳遞到下一個回調

  • Promise.prototype.catch

    其實是.then(null, rejection)的別名

    一樣支持鏈式寫法,最後一個catch能夠catch到前面任一個Promise跑拋出的未catch的error

  • Promise.all

    參數需具備Iterator接口,返回爲多個Promise實例

    var p = Promise.all([p1, p2, p3]);
    複製代碼

    p1, p2, p3均resolve後p才resolve,任一個reject則p就reject。

    若內部有catch,則外部catch捕獲不到異常。

  • Promise.race

    // 若5秒未返回則拋錯
    const p = Promise.race([
      fetch('/resource-that-may-take-a-while'),
      new Promise(function (resolve, reject) {
        setTimeout(() => reject(new Error('request timeout')), 5000)
      })
    ]);
    p.then(response => console.log(response));
    p.catch(error => console.log(error));
    複製代碼

    第一個狀態改變的Promise會引發p狀態改變。

  • Promise.resolve/reject

    Promise.resolve('1')
    Promise.resolve({ then: function() {
      console.log(123)
    } })
    複製代碼
    • 不傳參數/傳非thenable對象,生成一個當即resolve的Promise
    • 傳thenable對象,當即執行then方法,而後根據狀態更改執行then(普通Promise行爲)
  • Promise.prototype.finally

    Promise.prototype.finally = function (callback) {
      let P = this.constructor;
      return this.then(
        value => P.resolve(callback()).then(() => value),
        reason => P.resolve(callback()).then(() => { throw reason })
      );
    };
    複製代碼

    不管如何都會執行最後的cb

Promise爲咱們提供了優於callback嵌套的異步選擇,但實際上仍是基於回調來實現的。

實現

簡單的Promise實現代碼能夠看這裏 github

lesson2 Generator

初探

  • 基本概念

    function * gen() {
      const a = yield 1;
      return 2
    }
    const m = gen() // gen{<suspended>}
    m.next() // {value: 1, done: false}
    m.next() // {value: 2, done: true}
    m.next() // {value: undefined, done: true}
    m // gen {<closed>}
    複製代碼
    • Generator一個遍歷器生成函數,一個狀態機
    • 執行返回一個遍歷器,表明Generator函數的內部指針(此時yield後的表達式不會求值)
    • 每次調用遍歷器的next方法會執行下一個yield前的語句而且返回一個{ value, done }對象。
    • 其中value屬性表示當前的內部狀態的值,是yield表達式後面那個表達式的值,done屬性是一個布爾值,表示是否遍歷結束
    • 若沒有yield了,next執行到函數結束,並將return結果做爲value返回,若無return則爲undefined。
    • 這以後調用next將返回{ value: undefined, done: true },Generator的內部屬性[[GeneratorStatus]]變爲closed狀態
  • yield

    • 調用next方法時,將yield後的表達式的值做爲value返回,只有下次再調用next纔會執行這以後的語句,達到了暫停執行的效果,至關於具有了一個惰性求值的功能
    • 沒有yield時,Generator函數爲一個單純的暫緩執行函數(須要調用next執行)
    • yield只能用於Generator函數

方法

  • Generator.prototype.next()

    經過傳入參數爲Generator函數內部注入不一樣的值來調整函數接下來的行爲

    // 這裏利用參數實現了重置
    function* f() {
      for(var i = 0; true; i++) {
        var reset = yield i;
        if(reset) { i = -1; }
      }
    }
    var g = f();
    g.next() // { value: 0, done: false }
    g.next() // { value: 1, done: false }
    // 傳遞的參數會被賦值給i(yield後的表達式的值(i))
    // 而後執行var reset = i賦值給reset
    g.next(true) // { value: 0, done: false }
    複製代碼
  • Generator.prototype.throw()

    • Generator函數返回的對象都具備throw方法,用於在函數體外拋出錯誤,在函數體內能夠捕獲(只能catch一次)
    • 參數能夠爲Error對象
    • 若是函數體內沒有部署try...catch代碼塊,那麼throw拋出的錯會被外部try...catch代碼塊捕獲,若是外部也沒有,則程序報錯,中斷執行
    • throw方法被內部catch之後附帶執行一次next
    • 函數內部的error能夠被外部catch
    • 若是Generator執行過程當中內部拋錯,且沒被內部catch,則不會再執行下去了,下次調用next會視爲該Generator已運行結束
  • Generator.prototype.return()

    • try ... finally存在時,return會在finally執行完後執行,最後的返回結果是return方法的參數,這以後Generator運行結束,下次訪問會獲得{value: undefined, done: true}
    • try ... finally不存在時,直接執行return,後續和上一條一致

以上三種方法都是讓Generator恢復執行,並用語句替換yield表達式

yield*

  • 在一個Generator內部直接調用另外一個Generator是沒用的,若是須要在一個Generator內部yield另外一個Generator對象的成員,則須要使用yield*

    function* inner() {
      yield 'a'
      // yield outer() // 返回一個遍歷器對象
      yield* outer() // 返回一個遍歷器對象的內部值
      yield 'd'
    }
    function* outer() {
      yield 'b'
      yield 'c'
    }
    let s = inner()
    for (let i of s) {
      console.log(i)
    } // a b c d
    複製代碼
  • yield*後跟一個遍歷器對象(全部實現了iterator的數據結構實際上均可以被yield*遍歷)

  • 被代理的Generator函數若是有return,return的值會被for...of忽略,因此next不會返回,可是實際上能夠向外部Generetor內部返回一個值,以下:

    function *foo() {
      yield 2;
      yield 3;
      return "foo";
    }
    function *bar() {
      yield 1;
      var v = yield *foo();
      console.log( "v: " + v );
      yield 4;
    }
    var it = bar();
    it.next()
    // {value: 1, done: false}
    it.next()
    // {value: 2, done: false}
    it.next()
    // {value: 3, done: false}
    it.next();
    // "v: foo"
    // {value: 4, done: false}
    it.next()
    // {value: undefined, done: true}
    複製代碼
  • 舉個🌰

    // 處理嵌套數組
    function* Tree(tree){
      if(Array.isArray(tree)){
        for(let i=0;i<tree.length;i++) {
          yield* Tree(tree[i])
        }
      } else {
        yield tree
      }
    }
    let ss = [[1,2],[3,4,5],6,[7]]
    for (let i of Tree(ss)) {
      console.log(i)
    } // 1 2 3 4 5 6 7
    // 理解for ...of 其實是一個while循環
    var it = iterateJobs(jobs);
    var res = it.next();
    while (!res.done){
      var result = res.value;
      // ...
      res = it.next();
    }
    複製代碼

Extra

  • 做爲對象的屬性的Generator函數

    寫法很清奇

    let obj = {
      * sss() {
        // ...
      }
    }
    let obj = ={
      sss: function* () {
        // ...
      }
    }
    複製代碼
  • Generator函數的this

    Generator函數返回的是遍歷器對象,會繼承prototype的方法,可是因爲返回的不是this,因此會出現:

    function* ss () {
      this.a = 1
    }
    let f = ss()
    f.a // undefined
    複製代碼

    想要在內部的this綁定遍歷器對象?

    function * ss() {
      this.a = 1
      yield this.b = 2;
      yield this.c = 3;
    }
    let f = ss.call(ss.prototype)
    // f.__proto__ === ss.prototype
    f.next()
    f.next()
    f.a // 1
    f.b // 2
    f.c // 3
    複製代碼

應用

  • 舉個🌰

    // 利用暫停狀態的特性
    let clock = function* () {
      while(true) {
        console.log('tick')
        yield
        console.log('tock')
        yield
      }
    }
    複製代碼
  • 異步操做的同步化表達

    // Generator函數
    function* main() {
      var result = yield request("http://some.url");
      var resp = JSON.parse(result);
        console.log(resp.value);
    }
    // ajax請求函數,回調函數中要將response傳給next方法
    function request(url) {
      makeAjaxCall(url, function(response){
        it.next(response);
      });
    }
    // 須要第一次執行next方法,返回yield後的表達式,觸發異步請求,跳到request函數中執行
    var it = main();
    it.next();
    複製代碼
  • 控制流管理

    // 同步steps
    let steps = [step1Func, step2Func, step3Func];
    function *iterateSteps(steps){
      for (var i=0; i< steps.length; i++){
        var step = steps[i];
        yield step();
      }
    }
    // 異步後續討論
    複製代碼

實現

TO BE CONTINUED

lesson3 Generator的異步應用

回到最初提到的異步:將異步任務看作兩個階段,第一階段如今執行,第二階段在將來執行,這裏就須要將任務 暫停。而前面說到的Generator彷佛剛好提供了這麼一個當口,暫停結束後第二階段開啓不就對應下一個next調用嘛!

想像我有一個異步操做,我能夠經過Generator的next方法傳入操做須要的參數,第二階段執行完後返回值的value又能夠向外輸出,maybe Generator真的能夠做爲異步操做的容器?

before it

協程coroutine

協程A執行->協程A暫停,執行權轉交給協程B->一段時間後執行權交還A->A恢復執行

// yield是異步兩個階段的分割線
function* asyncJob() {
  // ...其餘代碼
  var f = yield readFile(fileA);
  // ...其餘代碼
}
複製代碼

Thunk函數

  • 參數的求值策略

    • 傳名調用和傳值調用之爭
    • 後者更簡單,可是可能會有須要大量計算求值卻沒有用到這個參數的狀況,形成性能損失
  • js中的Thunk函數

    • 傳統的Thunk函數是傳名調用的一種實現,即將參數做爲一個臨時函數的返回值,在須要用到參數的地方對臨時函數進行求值
    • js中的Thunk函數略有不一樣 js中的Thunk函數是將多參數函數替換爲單參數函數(這個參數爲回調函數)
      const Thunk = function(fn) {
        return function (...args) {
          return function (callback) {
            return fn.call(this, ...args, callback);
          }
        };
      };
      複製代碼
      看起來只是換了個樣子,好像並無什麼用

自執行

Generator看起來很美妙,可是next調用方式看起來很麻煩,如何實現自執行呢?

Thunk函數實現Generator函數自動執行

  • Generator函數自動執行

    function* gen() {
      yield a // 表達式a
      yield 2
    }
    let g = gen()
    let res = g.next()
    while(!res.done) {
      console.log(res.value)
      res = g.next() // 表達式b
    }
    複製代碼

    可是,這不適合異步操做。若是必須保證前一步執行完,才能執行後一步,上面的自動執行就不可行。

    next方法是同步的,執行時必須馬上返回值,yield後是同步操做固然沒問題,是異步操做時就不能夠了。處理方式就是返回一個Thunk函數或者Promise對象。此時value值爲該函數/對象,done值仍是按規矩辦事。

    var g = gen();
    var r1 = g.next();
    // 重複傳入一個回調函數
    r1.value(function (err, data) {
      if (err) throw err;
      var r2 = g.next(data);
      r2.value(function (err, data) {
        if (err) throw err;
        g.next(data);
      });
    });
    複製代碼
  • Thunk函數的自動流程管理

    • 思路:

      Generator函數中yield 異步Thunk函數,經過yield將控制權轉交給Thunk函數,而後在Thunk函數的回調函數中調用Generator的next方法,將控制權交回給Generator。此時,異步操做確保完成,開啓下一個任務。

      Generator是一個異步操做的容器,實現自動執行須要一個機制,這個機制的關鍵是控制權的交替,在異步操做有告終果之後自動交回控制權,而回調函數執行正是這麼個時間點。

      // Generator函數的執行器
      function run(fn) {
        let gen = fn()
        // 傳給Thunk函數的回調函數
        function cb(err, data) {
          // 控制權交給Generator,獲取下一個yield表達式(異步任務)
          let result = gen.next(data)
          // 沒任務了,返回
          if (result.done) return
          // 控制權交給Thunk函數,傳入回調
          result.value(cb)
        }
        cb()
      }
      // Generator函數
      function* g() {
        let f1 = yield readFileThunk('/a')
        let f2 = yield readFileThunk('/b')
        let f3 = yield readFileThunk('/c')
      }
      // Thunk函數readFileThunk
      const Thunk = function(fn) {
        return function (...args) {
          return function (callback) {
            return fn.call(this, ...args, callback);
          }
        };
      };
      var readFileThunk = Thunk(fs.readFile);
      readFileThunk(fileA)(callback);
      // 自動執行
      run(g)
      複製代碼

大名鼎鼎的co

  • 說明

    • 不用手寫上述的執行器,co模塊其實就是將基於Thunk函數和Promise對象的兩種自動Generator執行器包裝成一個模塊
    • 使用條件:yield後只能爲Thunk函數或Promise對象或Promise對象數組
  • 基於Promise的執行器

    function run(fn) {
      let gen = fn()
      function cb(data) {
        // 將上一個任務返回的data做爲參數傳給next方法,控制權交回到Generator
        // 這裏將result變量引用{value, done}對象
        // 不要和Generator中的`let result = yield xxx`搞混
        let result = gen.next(data)
        if (result.done) return result.value
        result.value.then(function(data){
          // resolved以後會執行cb(data)
          // 開啓下一次循環,實現自動執行
          cb(data)
        })
      }
      cb()
    }
    複製代碼
  • 源碼分析

    其實和上面的實現相似

    function co(gen) {
      var ctx = this;
      var args = slice.call(arguments, 1) // 除第一個參數外的全部參數
      // 返回一個Promise對象
      return new Promise(function(resolve, reject) {
        // 若是是Generator函數,執行獲取遍歷器對象gen
        if (typeof gen === 'function') gen = gen.apply(ctx, args);
        if (!gen || typeof gen.next !== 'function') return resolve(gen);
        // 第一次執行遍歷器對象gen的next方法獲取第一個任務
        onFulfilled();
        // 每次異步任務執行完,resolved之後會調用,控制權又交還給Generator
        function onFulfilled(res) {
          var ret;
          try {
            ret = gen.next(res); // 獲取{value,done}對象,控制權在這裏暫時交給異步任務,執行yield後的異步任務
          } catch (e) {
            return reject(e);
          }
          next(ret); // 進入next方法
        }
        // 同理可得
        function onRejected(err) {
          var ret;
          try {
            ret = gen.throw(err);
          } catch (e) {
            return reject(e);
          }
          next(ret);
        }
        // 關鍵
        function next(ret) {
          // 遍歷執行完異步任務後,置爲resolved,並將最後value值返回
          if (ret.done) return resolve(ret.value);
          // 獲取下一個異步任務,並轉爲Promise對象
          var value = toPromise.call(ctx, ret.value);
          // 異步任務結束後會調用onFulfilled方法(在這裏爲yield後的異步任務設置then的回調參數)
          if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
          return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
            + 'but the following object was passed: "' + String(ret.value) + '"'));
        }
      })
    }
    複製代碼

    其實仍是同樣,爲Promise對象then方法指定回調函數,在異步任務完成後觸發回調函數,在回調函數中執行Generator的next方法,進入下一個異步任務,實現自動執行。

    舉個🌰

    'use strict';
    const fs = require('fs');
    const co =require('co');
    function read(filename) {
      return new Promise(function(resolve, reject) {
        fs.readFile(filename, 'utf8', function(err, res) {
          if (err) {
            return reject(err);
          }
          return resolve(res);
        });
      });
    }
    co(function *() {
      return yield read('./a.js');
    }).then(function(res){
      console.log(res);
    });
    複製代碼

lesson4 async函數

語法糖

  • 比較

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

    看起來只是寫法的替換,實際上有這樣的區別

    • async函數內置執行器,不須要手動執行next方法,不須要引入co模塊
    • async適用更廣,co模塊對yield後的內容嚴格限制爲Thunk函數或Promise對象,而await後能夠是Promise對象或原始類型值
    • 返回Promise,這點和co比較像
  • 用法

    • async標識該函數內部有異步操做
    • 因爲async函數返回的是Promise,因此能夠將async函數做爲await命令的參數
    • async函數可使用在函數、方法適用的許多場景

語法

  • 返回的Promise

    • async函數只有在全部await後的Promise執行完之後纔會改變返回的Promise對象的狀態(return或者拋錯除外)即只有在內部操做完成之後纔會執行then方法
    • async函數內部return的值會做爲返回的Promise的then方法回調函數的參數
    • async函數內部拋出的錯誤會使得返回的Promise變成rejected狀態,同時錯誤會被catch捕獲
  • async命令及其後的Promise

    • async命令後若是不是一個Promise對象,則會被轉成一個resolved的Promise
    • async命令後的Promise若是拋錯了變成rejected狀態或者直接rejected了,都會使得async函數的執行中斷,錯誤能夠被then方法的回調函數catch到
    • 若是但願async的一個await Promise不影響到其餘的await Promise,能夠將這個await Promise放到一個try...catch代碼塊中,這樣後面的依然會正常執行,也能夠將多個await Promise放在一個try...catch代碼塊中,此外還能夠加上錯誤重試

使用注意

  • 相互獨立的異步任務能夠改造下讓其併發執行(Promise.all)

    let [foo, bar] = await Promise.all([getFoo(), getBar()]);
    複製代碼
  • await 與 for ... of

    應該還在提案階段吧

    for await (const item of list) {
      console.log(item)
    }
    複製代碼

實現

  • 其實就是將執行器和Generator函數封裝在一塊兒,詳見上一課

舉舉🌰

  • 併發請求,順序輸出
    async function logInOrder(urls) {
      // 併發讀取遠程URL
      const textPromises = urls.map(async url => {
        const response = await fetch(url);
        return response.text();
      });
      // 按次序輸出
      for (const textPromise of textPromises) {
        console.log(await textPromise);
      }
    }
    複製代碼

目前瞭解到的異步解決方案大概就這樣,Promise是主流,Generator做爲容器,配合async await語法糖提供了看起來彷佛更加優雅的寫法,但實際上由於一切都是Promise,同步任務也會被包裝成異步任務執行,我的感受仍是有不足之處的。

雖發表於此,卻畢竟爲一人之言,又是每日學有所得之筆記,內容未必詳實,看官老爺們還望海涵。

相關文章
相關標籤/搜索