前端-JavaScript非同步程式設計async函式

程式設計碼農發表於2021-10-21

基本概念

傳統JavaScript非同步程式設計的形式大體分以下幾種。

  • 回撥函式
  • 事件監聽
  • 釋出/訂閱
  • Promise 物件

非同步

一個任務連續的執行就叫做同步。如果將任務為分兩步執行,執行完第一步,轉而執行其它任務,等做好了準備,再回過頭執行第二步,這種不連續的執行就叫做非同步。

回撥函式

回撥函式就是把第二步執行的任務單獨寫在一個函式裡面,等到重新執行這個任務的時候,就直接呼叫這個函式。回撥函式的英語叫callback,直譯過來就是"重新呼叫"。

loadData(url, function (data) {
  console.log(data);
});

注意:任務第一步執行完後,所在的上下文環境就已經結束了,所以我們一般會使用var that = this 將第一步執行時的this 指向進行儲存,以便回撥時使用。

function Api(url) {
    this.url = url;
    this.request = function () {
          var that = this
        setTimeout(function () {
            console.log('url', that.url)
        }, 1000)
    }
}

var api = new Api('http://127.0.0.1')
api.request() // url http://127.0.0.1

Generator函式

非同步程式設計解決方案中, ES6還提供了Generator函式。它其實是一個普通函式,獨有特徵

  1. function關鍵字與函式名之間有一個星號;
  2. 函式體內部使用yield表示式,定義不同的內部狀態。

function* statusGenerator() {
  yield 'pending';
  yield 'running';
  return 'end';
}

var st = statusGenerator();

上面程式碼 statusGenerator 函式返回一個迭代器物件,函式內定義了三個狀態,呼叫迭代器next方法指向下一個狀態。

st.next() // { value: 'pending', done: false }
st.next() // { value: 'running', done: false }
st.next() // { value: 'end', done: false }

yield 表示式

yield表示式就是暫停標誌。迭代器執行next時。

  1. 遇到yield表示式,就暫停執行後面的操作,並將yield後面的那個表示式的值作為返回的物件的value屬性值。
  2. 下一次呼叫next方法時,再繼續往下執行,直到遇到下一個yield表示式。
  3. 如果沒有再遇到新的yield表示式,就一直執行到函式結束,直到return語句為止,並將return語句後面的表示式的值,作為返回的物件的value屬性值。
  4. 如果該函式沒有return語句,則返回的物件的value屬性值為undefined

for...of 迴圈

我們也可以使用 for...of進行遍歷。

function* statusGenerator() {
  yield 'pending';
  yield 'running';
  return 'end';
}

var st = statusGenerator();
for(let v of st){
  console.log(v)// pending running
}

Generator 的應用

協程

協程的意思是多個執行緒互相協作,完成非同步任務。它是一些程式語言的非同步程式設計方案,比如go中的協程實現goroutine。協程式執行的大致流程如下:

  1. 協程A開始執行。
  2. 協程A執行到一半,進入暫停,執行權轉移到協程B
  3. (一段時間後)協程B交還執行權。
  4. 協程A恢復執行。

JavaScript中的協程實現Generator 函式,它可以在指定的地方(yield)交出函式的執行權(即暫停執行),然後等待執行權交還繼續執行。

比如:我們實現一個倒數計時函式,任務就緒後等待倒數計時,一起執行。

function* countdown(num, running) {
    do {
        yield num--
    } while (num > 0)
    running()
}

const tasks = []
const ct = countdown(3, function () {
    console.log('start run task')
    for (let task of tasks) {
        task()
    }
})

for (let i = 0; i < 3; i++) {
    tasks.push(function () {
        console.log('task '+ i)
    })
    ct.next()
}

ct.next()

一個非同步請求封裝

var fetch = require('node-fetch');

function* request(){
  var url = 'xxxx';
  var user = yield fetch(url); // 返回promise物件,data: {'user':'xxxx'}
  console.log(user);
}

var req = request();
var result = req.next();

result.value.then(function(data){
  return data.user
}).then(function(user){
  req.next(user);                        // 將 user資訊傳到 request()函式,被user變數接收。
});

async函式

ES2017 引入了 asyncawait 關鍵字,使用這對關鍵字,可以用更簡潔的方式寫出基於Promise的非同步行為,而無需刻意地鏈式呼叫promise

async宣告的函式一般稱為async函式。可以把 async 看作是 Generator 的語法糖,因為它們本質的作用一樣。

Generator 寫法

const loadData = function (url) {
    return new Promise(function (resolve, reject) {
        resolve(data);
    });
};

const request = function* () {
    const user = yield loadData('https://user');
    const goods = yield loadData('https://goods');
    console.log(user, goods);
};

async 寫法

const loadData = function (url) {
    return new Promise(function (resolve, reject) {
        resolve(data);
    });
};

const request = async function () {
    const user = await loadData('https://user');
    const goods = await loadData('https://goods');
    console.log(user, goods);
};

基本用法

async函式會返回一個 Promise 物件。當函式執行的時候,一旦遇到await就會先返回,等到非同步操作完成,再接著執行函式體內後面的語句。

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello', 50);

async函式內部return語句返回的值,會成為then方法回撥函式的引數。

async function hello() {
  return 'hello';
}

hello().then(v => console.log(v))
// "hello"

async函式內部丟擲錯誤,會導致返回的 Promise 物件變為reject狀態。丟擲的錯誤物件會被catch方法回撥函式接收到。

async function hello() {
  throw new Error('Error');
}

hello().then(
  v => console.log(v),
  e => console.log( e)
) // //Error: Error

await 命令

一般情況下,await後面都是一個 Promise 物件,返回該物件的結果。如果不是 Promise 物件,就直接返回對應的值。

async function hello() {
  return await 'hello'
}
hello().then(v => console.log(v)) // hello

async function hello() {
  return await Promise.resolve('hello');
}
hello().then(v => console.log(v)) // hello

錯誤處理

如果await後面的非同步操作出錯,那麼等同於async函式返回的 Promise 物件被reject

async function hello() {
  await new Promise(function (resolve, reject) {
    throw new Error('error');
  });
}

hello()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:error

所以最好把 await命令放在try...catch程式碼塊中。

async function hello() {
    try {
        await new Promise(function (resolve, reject) {
            throw new Error('error');
        });
    } catch(e) {
        console.log('err:', e) // error
    }
    return await('hello');
}

const  h = hello();
h.then((v) => {console.log(v)}) // hello

小結

本文記錄了JavaScript非同步程式設計中的一些方式,Generator函式和 asyncawait 語法,歡迎留言交流。

相關文章