非同步程式設計小結

jiangtao_發表於2019-04-30

在javascript單執行緒的世界裡,沒有非同步寸步難行。本章節介紹非同步程式設計的發展,從callback,Eventspromise,generator,async/await.

為什麼要使用非同步程式設計

在(javascript)單執行緒的世界裡,如果有多個任務,就必須排隊,前面一個完成,再繼續後面的任務。就像一個ATM排隊取錢似的,前面一個不取完,就不讓後面的取。 為了這個問題,javascript提供了2種方式: 同步和非同步。 非同步就像銀行取錢填了單子約了號,等著叫到號,再去做取錢,等待的時間裡還可以乾點其他的事兒~

非同步回撥帶來的問題

回撥地獄 (Callbacks Hell)

舉個例子:通過api拿到資料,資料裡面有圖片,圖片載入成功渲染,那麼程式碼如下:

// 虛擬碼
request(url, (data) => {
    if(data){
        loadImg(data.src, () => {
            render();
        })
    }
})
複製程式碼

如果有在業務邏輯比較複雜或者NodeJS I/O操作比較頻繁的時候,就成了下面這個樣子:

doSth1((...args, callback) => {
    doSth2((...args, callback) => {
        doSth3((...args, callback) => {
            doSth4((...args, callback) => {
                doSth5((...args, callback) => {

                })
            })
        })
    })
})
複製程式碼

這樣的維護性可讀性,整個人瞬間感覺不好了~

異常處理

try {
    setTimeout(() => {
        throw new Error('unexpected error');
    }, 100);
} catch (e) {
    console.log('error2', e.message);
}
複製程式碼

以上程式碼執行丟擲異常,但try catch不能得到未來時間段的異常。

流程控制不方便

流程控制只能通過維護各種狀態來處理,不利於管理

非同步程式設計現有解決方案對比

事件機制

不管瀏覽器還是NodeJS,提供了大量內建事件API來處理非同步。

事件監聽

瀏覽器中如: websocket, ajax, canvas, imgFileReader等 NodeJS如: stream, http

自定義事件(本質上是一種釋出訂閱模式)

  • NodeJS中的EventEmitter事件模型
  • 瀏覽器中:如DOM可使用addEventListener,此外瀏覽器也提供一些自定義事件的API,但相容性不好,具體可以這篇文章;也可以用Node中的EventEmitter;jquery中也對此做了封裝,on,bind等方法支援自定義事件。

事件小結

事件一定程度上解決了解耦和提升了程式碼可維護性;對於異常處理,只有部分支援類似error事件才能處理。若想實現異常處理機制,只有自己模擬error事件,比較繁瑣。

Promise

Promise嚴格來說不是一種新技術,它只是一種機制,一種程式碼結構和流程,用於管理非同步回撥。為了統一規範產生一個Promise/A+規範,點選檢視Promise/A+中文版,cnode的William17實現了Promise/A+規範,有興趣的可以點這裡檢視

  • promise狀態由內部控制,外部不可變
  • 狀態只能從pendingresovled, rejected,一旦進行完成不可逆
  • 每次then/catch操作返回一個promise例項,可以進行鏈式操作

promise狀態
部分程式碼如下:

readFile(path1).then(function (data) {
    console.log(data.toString());
    return readFile(path2);
}).then(function (data) {
    console.log(data.toString());
    return readFile(errorPath);
}).then(function (data) {
    console.log(data.toString());
}).catch(function (e) {
    console.log('error', e);
    return readFile(path1);
}).then(function (data) {
    console.log(data.toString());
});
複製程式碼

Promise的缺陷:

  • 內部丟擲錯誤只能通過promise.catch才能才能接收到
  • 語義不明確

Generator

generator介紹

Generator是ES6提供的方法,是生成器,最大的特點:可以暫停執行和恢復執行(可以交出函式的執行權),返回的是指標物件.

  • 需要自執行函式才能持續執行,否則需要手工呼叫流程
  • 自執行函式借助promise, 獲取異常和最終結果

generator 和 promise自執行實現

const run = function (generator) {
  var g = generator()
  var perform = function (result) {
    if (result.done === true) {
      return result.value
    }
    if (isPromise(result.value)) {
      return result.value.then(function (v) {
        return perform(g.next(v))
      }).catch(function (e) {
        return perform(g.throw(e))
      })
    } else {
      return perform(g.next(result.value))
    }

  }
  return perform(g.next())
}

const isPromise = f => f.constructor === Promise

function* g() {
  var a = yield sleep(1000, _ => 1)
  var b = yield sleep(1000, _ => 2)
  return a + b
}
function sleep(d, fn) {
  return new Promise((resolve, reject) => {
    setTimeout(_ => resolve(fn()), d)
  })
}
複製程式碼

由於以上問題,於是一個叫 co庫誕生,支援thunkPromise. 關於thunk可以檢視阮一峰老師的thunk介紹和應用

Async/Await

ES7提供了async函式,使得非同步操作變得更加方便。

  • 內建執行器
  • 更好的語義
  • 更多的實用場景,co引數Generator函式中yield只能是promisethunk

例項程式碼:

async function asyncReadFile() {   
    var p1 = readFile(path.join(__dirname, '../data/file1.txt'));
    var p2 = readFile(path.join(__dirname, '../data/file2.txt'));
    var [f1, f2] = await Promise.all([p1, p2]);
    return `${f1.toString()}\n${f2.toString()}`;
}
(async function () {
    try {
        console.log(await asyncReadFile());
    } catch (e) {
        console.log(e.message)
    }
})();
複製程式碼

Node8 async/await 支援

Node8.0釋出,全面支援 async/await, 推薦使用 async/await, 低版本node可以使用 babel來編譯處理。 而 為了方便 介面設計時 返回 promise 更方面使用者. 當然依然使用 callback , 通過 promisify做轉換, Node8.0已經內建 util.promisify方法。

參考資料

小結

非同步程式設計在javascript中扮演者重要的角色,雖然現在需要通過babel,typescript等編譯或轉換程式碼,跟著規範標準走,就沒有跑偏。

好久之前在github部落格上的文章了。

如需轉載,請備註出處。

相關文章