首頁 > 軟體

await-to-js原始碼深入理解處理非同步任務用法範例

2022-08-07 14:00:57

如何處理非同步任務?

我們先從一個老生常談的問題開始。

回撥函數

由於javascript是一門單執行緒的語言,所以我們早期來處理非同步場景的時候,大部分是通過回撥函數來進行處理的。

var fn = function(callback){
    setTimeout(function(){
        callback()
    },1000)
}
fn(function(){console.log('hello, pino')})

例如上面這個例子,fn函數是一個非同步函數,裡面執行的setTimeout將會在1s之後呼叫傳入的callback函數,列印出hello,pino這個結果。

但是當我們有多個非同步操作的時候,就需要有多個非同步函數進行巢狀,程式碼將會變得更加臃腫和難以維護。

setTimeout(function(){
    console.log('執行了')
    setTimeout(function(){
        console.log('再次執行了')
        //.....
    },2000)
},1000)

同樣的,還有一個例子: 假設我們有fn1,fn2,fn3三個非同步函數:

let fn1 = function(){
    setTimeout(function(){
        console.log('pino')
    },1000)
}
let fn2 = function(){
    setTimeout(function(){
        console.log('愛吃')
    },3000)
}
let fn3 = function(){
    setTimeout(function(){
        console.log('瓜')
    },2000)
}

我們想順序對三個函數的結果進行順序列印,那麼使用傳統的回撥函數來實現的話,我們可以這樣寫:

var makefn = function(text,callback,timer){
    setTimeout(function(){
        console.log(text)
        callback()
    },timer)
}
makefn('pino',function(){
    makefn('愛吃',function(){
        makefn('瓜',function(){
            console.log('結束了~')
        },2000)
    },3000)
},1000)

可以看到當回撥任務過多的時候,我們的程式碼將會變的非常臃腫,尤其是多個非同步函數之間層層巢狀,這就形成了回撥地獄。

使用回撥函數的方式來處理非同步任務,當回撥函數過多時,對開發者的心智負擔是非常重的。

Promise

promise物件的出現其實是對js處理非同步任務邁出的一大步,它提供了非常多的針對非同步的處理方法,錯誤捕獲鏈式呼叫...

Promise物件是一個建構函式,用來生成Promise範例。

Promise建構函式接受一個函數作為引數,該函數的兩個引數分別是resolvereject

resolve函數: 將Promise物件的狀態從“未完成”變為“成功”(即從 pending 變為 resolved),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;

reject函數: 將Promise物件的狀態從“未完成”變為“失敗”(即從 pending 變為 rejected),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。

const person = new Promise((resolve,reject) => {
    let num = 6;
    if(num>5){
        resolve()
    }else{
        reject()
    }
})

Promise範例生成以後,可以用then方法分別指定resolved狀態和rejected狀態的回撥函數。

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

then方法可以接受兩個回撥函數作為引數。第一個回撥函數是Promise物件的狀態變為resolved時呼叫,第二個回撥函數是Promise物件的狀態變為rejected時呼叫。其中,第二個函數是可選的,這兩個函數都接受Promise物件傳出的值作為引數。

例如我們將上面的順序列印三個非同步函數進行改造:

makefn('pino',function(){
    makefn('愛吃',function(){
        makefn('瓜',function(){
            console.log('結束了~')
        },2000)
    },3000)
},1000)
//改造後
fn('pino',1000).then(function(){
    return fn('愛吃',3000)
})
.then(function(){
    return fn('瓜',2000)
})
.then(function(){
    console.log('結束了~')
})

可以看到改造完成後的程式碼變得非常具有可讀性和條理性。 由於本文的主角不是Promise物件,所以想要深入瞭解請移步:es6.ruanyifeng.com/#docs/promi…

async

ES2017 標準引入了 async 函數,使得非同步操作變得更加方便。

async函數返回一個 Promise 物件,可以使用then方法新增回撥函數。當函數執行的時候,一旦遇到await就會先返回,等到非同步操作完成,再接著執行函數體內後面的語句。

// 函數前面加入async關鍵字
async function getAllData(name) {
  // 遇到await會暫停,並返回值
  const data = await getData(name);
  const options = await getSelect(name);
  return options;
}
getAllData('pino').then(function (result) {
  console.log(result);
});

下面繼續使用async的方式來改造一下文章開頭的例子:

async function makeFn() {
  let fn1 = await fn1()
  let fn2 = await fn2()
  let fn3 = await fn3()
}

async函數的出現幾乎將非同步函數完全變為了同步的寫法,使非同步任務更容易維護。

Generator

形式上, Generator 函數是一個普通函數,但是有兩個特徵。

一是,function關鍵字與函數名之間有一個星號;

二是,函數體內部使用yield表示式,定義不同的內部狀態(yield在英語裡的意思就是“產出”)。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var p1 = helloWorldGenerator();

上面程式碼定義了一個 Generator 函數helloWorldGenerator,它內部有兩個yield表示式(hello和world

即該函數有三個狀態:hello,worldreturn 語句(結束執行)。

然後, Generator 函數的呼叫方法與普通函數一樣,也是在函數名後面加上一對圓括號。

不同的是,呼叫 Generator 函數後,該函數並不執行,返回的也不是函數執行結果,而是一個指向內部狀態的指標物件,也就是上一章介紹的遍歷器物件(Iterator Object)。 下一步,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。

也就是說,每次呼叫next方法,內部指標就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止。

換言之, Generator 函數是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。

p1.next()
// { value: 'hello', done: false }
p1.next()
// { value: 'world', done: false }
p1.next()
// { value: 'ending', done: true }
p1.next()
// { value: undefined, done: true }

上面程式碼一共呼叫了四次next方法。

Generator 函數也可以進行非同步任務的處理,上面的async函數就是Generator 函數的語法糖,而兩者之間最大的區別就是async函數內建了自執行器,也就是說無需手動呼叫next()方法,async函數就會幫我們繼續向下執行,而Generator 函數不會自動呼叫next()方法,只能進行手動呼叫,下面實現一個簡易執行器:

// 接受一個Generator函數
function run(gen){
  var g = gen();
  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    // 只要返回的dong不為true,沒有執行完畢,就繼續呼叫next函數,繼續執行
    result.value.then(function(data){
      next(data);
    });
  }
  next();
}
run(gen);

使用Generator 函數來該寫一下之前的案例,其實只需要將await更換為yield

function* makeFn() {
  let fn1 = yield fn1()
  let fn2 = yield fn2()
  let fn3 = yield fn3()
}

本文只是簡略的講解了Generator 函數和async函數,如果像深入學習,請移步:

es6.ruanyifeng.com/#docs/async

es6.ruanyifeng.com/#docs/gener…

什麼是await-to-js?

說了這麼多,今天的主角await-to-js到底是幹啥的?解決了什麼問題❓

先來看一下作者的定義:

Async await wrapper for easy error handling without try-catch。

非同步等待封裝器,便於錯誤處理,不需要try-catch

先來看一下如何使用:

安裝

npm i await-to-js --save

對比一下使用await-to-js後,我們在程式碼中處理錯誤捕獲有什麼不同,這裡使用async函數進行處理:

// async的處理方式
function async getData() {
    try {
      const data1 = await fn1()
    } catch(error) {
      return new Error(error)
    }
    try {
      const data2 = await fn2()
    } catch(error) {
      return new Error(error)
    }
    try {
      const data3 = await fn3()
    } catch(error) {
      return new Error(error)
    }
}
// 使用await-to-js後
import to from './to.js';
function async getData() {
   const [err, data1]  = await to(promise)
   if(err) throw new (error);
   const [err, data2]  = await to(promise)
   if(err) throw new (error);
   const [err, data3]  = await to(promise)
   if(err) throw new (error);
}

可以看到,使用await-to-js後我們的程式碼變得精簡了許多,在使用async函數時,需要手動使用try...catch來進行錯誤捕獲,而await-to-js直接就可以將錯誤返回給使用者。

所以根據上面的例子,可以得出結論,await-to-js的作用就是封裝了錯誤捕獲的處理常式,使非同步的操作更加的方便。

那麼await-to-js是如何實現的呢?

原始碼解析

其實await-to-js的原始碼非常短,只有15行,可以直接看一下原始碼中是如何實現的(為了檢視原始碼更加的直觀,下面的原始碼已經去除了typescript語法):

export function to(
  promise,
  errorExt
){
  return promise
    .then((data) => [null, data])
    .catch((err) => {
      if (errorExt) {
        const parsedError = Object.assign({}, err, errorExt);
        return [parsedError, undefined];
      }
      return [err, undefined];
    });
}

可以看到await-to-js中直接返回了to函數,他接受兩個引數,promiseerrorExt,其中promise引數接受一個Promis物件,而errorExt引數是可選的,先來看一下如果不傳入errorExt引數是什麼樣子的:

export function to(promise, errorExt){
  // 使用then和catch來執行和捕獲錯誤
  return promise
    .then((data) => [null, data])
    .catch((err) => {
      return [err, undefined];
    });
}

to函數直接返回了傳入的Promise物件,並定義了then函數和catch函數,無論成功還是失敗都返回一個陣列,陣列的第一項是錯誤結果,如果執行成功則返回null,執行失敗則返回錯誤資訊,陣列的第二項為執行結果,執行成功則返回響應成功的結果,如果執行失敗則返回undefined,這也是非常符合預期的。

那麼第二個引數是幹什麼的呢?第二個引數errorExt是可選的,他接收一個物件,主要用於接收使用者自定義的錯誤資訊,然後使用Object.assign將自定義資訊與錯誤資訊合併到一個物件,返回給使用者。

.catch((err) => {
  if (errorExt) {
    // 合併錯誤物件:預設錯誤資訊+使用者自定義錯誤資訊
    const parsedError = Object.assign({}, err, errorExt);
    // 返回錯誤結果
    return [parsedError, undefined];
  }
});

剛開始看原始碼的時候各種不適應,但是隻要沉下心去一步一步的偵錯,結合測試用例,有些東西真的沒有想象中那麼難,主要還是重在行動,想到了一個念頭和想法就趕緊去做,拒絕拖沓,只有真正的行動去學習,去獲取,去感知,才能真正的進步!


IT145.com E-mail:sddin#qq.com