JavaScript 異步編程

異步編程是每個使用 JavaScript 編程的人都會遇到的問題,無論是前端的 ajax 請求,或是 node 的各種異步 API。本文就來總結(jié)一下常見的四種處理異步編程的方法。

一、回調(diào)函數(shù)

使用回調(diào)函數(shù)是最常見的一種形式,下面來舉幾個例子:

// jQuery ajax
$.get('test.html', data => {
  $('#result').html(data)
})
// node 異步讀取文件
const fs = require('fs')
fs.readFile('/etc/passwd', (err, data) => {
  if (err) {
    throw err
  }
  console.log(data)
})

回調(diào)函數(shù)非常容易理解,就是定義函數(shù)的時候?qū)⒘硪粋€函數(shù)(回調(diào)函數(shù))作為參數(shù)傳入定義的函數(shù)當(dāng)中,當(dāng)異步操作執(zhí)行完畢后在執(zhí)行該回調(diào)函數(shù),從而可以確保接下來的操作在異步操作之后執(zhí)行。

回調(diào)函數(shù)的缺點在于當(dāng)需要執(zhí)行多個異步操作的時候會將多個回調(diào)函數(shù)嵌套在一起,組成代碼結(jié)構(gòu)上混亂,被稱為回調(diào)地獄(callback hell)。

func1(data0, data1 => {
  func2(data2, data3 => {
    func3(data3, data4 => data4)
  })
})

二、Promise

Promise 利用一種鏈式調(diào)用的方法來組織異步代碼,可以將原來以回調(diào)函數(shù)形式調(diào)用的代碼改為鏈式調(diào)用。

// jQuery ajax promise 方式
$.get('test.html')
  .then(data => $(data))
  .then($data => $data.find('#link').val('href'))
  .then(href => console.log(href))

resolve 和 reject

自己定義一個 Promise 形式的函數(shù)在 ES6 當(dāng)中也非常簡單:

var promise = new Promise(function(resolve, reject) {
  if (/* 異步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // success
}, function(value) {
  // failure
});

Promise 構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù),該函數(shù)的兩個參數(shù)分別是 resolve 方法和 reject 方法。

如果異步操作成功,則用 resolve 方法將 Promise 對象的狀態(tài),從「未完成」變?yōu)椤赋晒Α梗磸?pending 變?yōu)?resolved);

如果異步操作失敗,則用 reject 方法將 Promise 對象的狀態(tài),從「未完成」變?yōu)椤甘 梗磸?pending 變?yōu)?rejected)。

then 和 catch

在多個任務(wù)的情況下 <code>then</code> 方法可以用一個清晰的方式完成。<code>then</code>可以使用鏈式調(diào)用的寫法原因在于,每一次執(zhí)行該方法時總是會返回一個 Promise 對象。另外,在<code>then</code> onFulfilled的函數(shù)當(dāng)中的返回值,可以作為后續(xù)操作的參數(shù)。

function printHello (ready) {
    return new Promise(function (resolve, reject) {
        if (ready) {
            resolve("Hello");
        } else {
            reject("Good bye!");
        }
    });
}

printHello(true).then(function (message) {
    return message;
}).then(function (message) {
    return message  + ' World';
}).then(function (message) {
    return message + '!';
}).then(function (message) {
    alert(message);
}).catch(function(reason){ // error
    console.log(reason);
});

Promise.all 和 Promise.race

<code>Promise.all</code> 可以接收一個元素為<code>Promise</code> 對象的數(shù)組作為參數(shù),當(dāng)這個數(shù)組里面所有的 Promise 對象都變?yōu)?resolve 時,該方法才會返回。

var p1 = new Promise(function (resolve) {
    setTimeout(function () {
        resolve("Hello");
    }, 3000);
});
var p2 = new Promise(function (resolve) {
    setTimeout(function () {
        resolve("World");
    }, 1000);
});
Promise.all([p1, p2]).then(function (result) {
    console.log(result); // ["Hello", "World"]
});

還有一個和 <code>Promise.all</code> 相類似的方法 <code>Promise.race</code> ,它同樣接收一個數(shù)組,不同的是只要該數(shù)組中的 Promise 對象的狀態(tài)發(fā)生變化(無論是 resolve 還是 reject)該方法都會返回。

兼容性

關(guān)于 Promise 對象的兼容性問題。

image.png

在瀏覽器端,一些主流的瀏覽器都已經(jīng)可以使用 Promise 對象進行開發(fā),在 Node.js 配合 babel 也可以很方便地使用。

如果要兼容舊的瀏覽器,建議可以尋找一些第三方的解決方案,例如 jQuery 的 <code>$.Deferred</code>。

util.promisify

在 node 8.0 以上的版本還可以利用 util.promisify 方法將回調(diào)形式的函數(shù)變?yōu)?Promise 形式。

const util = require('util')
const fs = require('fs')
const readPromise = util.promisify(fs.readFile)
readPromise('test.txt').then(data => console.log(data.toString()))

三、Generators

什么是生成器?

我們從一個示例開始:

function* quips(name) {
  yield "你好 " + name + "!";
  yield "希望你能喜歡這篇介紹ES6的譯文";
  if (name.startsWith("X")) {
    yield "你的名字 " + name + "  首字母是X,這很酷!";
  }
  yield "我們下次再見!";
}

這段代碼看起來很像一個函數(shù),我們稱之為生成器函數(shù),它與普通函數(shù)有很多共同點,但是二者有如下區(qū)別:

  • 普通函數(shù)使用function聲明,而生成器函數(shù)使用function*聲明。
  • 在生成器函數(shù)內(nèi)部,有一種類似return的語法:關(guān)鍵字yield。二者的區(qū)別是,普通函數(shù)只可以return一次,而生成器函數(shù)可以yield多次(當(dāng)然也可以只yield一次)。在生成器的執(zhí)行過程中,遇到y(tǒng)ield表達式立即暫停,后續(xù)可恢復(fù)執(zhí)行狀態(tài)。

這就是普通函數(shù)和生成器函數(shù)之間最大的區(qū)別,普通函數(shù)不能自暫停,生成器函數(shù)可以。

生成器做了什么?

當(dāng)你調(diào)用quips()生成器函數(shù)時發(fā)生了什么?

> var iter = quips("jorendorff");
  [object Generator]
> iter.next()
  { value: "你好 jorendorff!", done: false }
> iter.next()
  { value: "希望你能喜歡這篇介紹ES6的譯文", done: false }
> iter.next()
  { value: "我們下次再見!", done: false }
> iter.next()
  { value: undefined, done: true }

生成器是迭代器

我們學(xué)習(xí)了ES6的迭代器,它是ES6中獨立的內(nèi)建類,同時也是語言的一個擴展點,通過實現(xiàn)<code>Symbol.iterator()</code>和<code>.next()</code>兩個方法你就可以創(chuàng)建自定義迭代器。

class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

  [Symbol.iterator]() { return this; }

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    } else {
      return {done: true, value: undefined};
    }
  }
}

// 返回一個新的迭代器,可以從start到stop計數(shù)。
function range(start, stop) {
  return new RangeIterator(start, stop);
}

四、Async/Await

node7.6 以上的版本引入了一個 ES7 的新特性 Async/Await 是專門用于控制異步代碼。先看一個例子:

const util = require('util')
const fs = require('fs')
const readFile = util.promisify(fs.readFile)
async function readFiles () {
  const txt = await readFile('file1.txt', 'utf8')
  console.log(txt)
  const txt2 = await readFile('file2.txt', 'utf8')
  console.log(txt2)
})

首先要使用<code>async</code> 關(guān)鍵字定義一個包含異步代碼的函數(shù),在 Promise 形式的異步函數(shù)前面使用 <code>await</code> 關(guān)鍵字就可以將異步寫成同步操作的形式。

看上去與 Generators 控制方式相差不大,但是 Async/Await 是原生用于控制異步,所以是比較推薦使用的。

五、錯誤處理

最后來探討下四種異步控制方法的錯誤處理。

回調(diào)函數(shù)

回調(diào)函數(shù)錯誤處理非常簡單,就是在回調(diào)函數(shù)中同時回傳錯誤信息:

const fs = require('fs')
fs.readFile('file.txt', (err, data) => {
  if (err) {
    throw err
  }
  console.log(data)
})

Promise

Promise 在 then 方法之后使用一個 catch 方案來捕捉錯誤信息:

const fs = require('fs')
const readFile = util.promisify(fs.readFile)
readFile('file.txt')
  .then(data => console.log(data))
  .catch(err => console.log(err))

Generators 和 Async/Await

Generators 和 Async/Await 比較類似,可以有兩種方式,第一種使用 Promise 的 catch 方法,第二種用 try catch 關(guān)鍵字。

Promise catch

const fs = require('fs')
const co = require('co')
const readFile = util.promisify(fs.readFile)
co(function* () {
  const data = yield readFile('file.txt').catch(err => console.log(err))
})
const fs = require('fs')
const co = require('co')
const readFile = util.promisify(fs.readFile)
async function testRead() {
  const data = await readFile('file.txt').catch(err => console.log(err))
}

try/catch

const fs = require('fs')
const co = require('co')
const readFile = util.promisify(fs.readFile)
co(function* () {
  try {
    const data = yield readFile('file.txt')
  } catch(err) {
    console.log(err)
  }
})
const fs = require('fs')
const readFile = util.promisify(fs.readFile)
async function testRead() {
  try {
    const data = await readFile('file.txt')
  } catch(err) {
    console.log(data)
  }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容