1. 程式人生 > >「前端進階」完全吃透async/await,深入JavaScript非同步

「前端進階」完全吃透async/await,深入JavaScript非同步

完全吃透async/await

導論:

  • 首先,必須瞭解Promise

  • 主要研究基本語法

  • 對比Promise與Async

  • 異常處理

參考:

0. 前言

ES7 提出的async 函式,終於讓 JavaScript 對於非同步操作有了終極解決方案。No more callback hell。
async 函式是 Generator 函式的語法糖。使用 關鍵字 async

來表示,在函式內部使用 await 來表示非同步。
想較於 Generator,Async 函式的改進在於下面四點:

  • 內建執行器。Generator 函式的執行必須依靠執行器,而 Aysnc 函式自帶執行器,呼叫方式跟普通函式的呼叫一樣
  • 更好的語義asyncawait 相較於 *yield 更加語義化
  • 更廣的適用性co 模組約定,yield 命令後面只能是 Thunk 函式或 Promise物件。而 async 函式的 await 命令後面則可以是 Promise 或者 原始型別的值(Number,string,boolean,但這時等同於同步操作)
  • 返回值是 Promise
    async 函式返回值是 Promise 物件,比 Generator 函式返回的 Iterator 物件方便,可以直接使用 then() 方法進行呼叫

1. 基本語法

基本語法是 方法頭 新增關鍵字async,在非同步前 新增await

1. API

核心 API 就async 與 await,具體 直接將MDN中解釋拿來用

  1. async function 宣告將定義一個返回 AsyncFunction 物件的非同步函式。非同步函式是指通過事件迴圈非同步執行的函式,它會通過一個隱式的 Promise 返回其結果。但是如果你的程式碼使用了非同步函式,它的語法和結構會更像是標準的同步函式。

    MDN

    白話:async 返回一個 Promise,也就是 最後return是不是 Promise 最終都會被包裝成promise

  2. await 操作符用於等待一個Promise 物件。它只能在非同步函式 async function 中使用。MDN

    //語法
    [return_value] = await expression;   //注意,返回並不是一個Promise物件,而是結果
    

    表示式:
    一個 Promise 物件或者任何要等待的值。
    返回值:

    ​ (注意,返回並不是一個Promise物件,而是結果)
    返回 Promise 物件的處理結果。
    如果等待的不是 Promise 物件,則返回該值本身。

    描述:

    ​ await 表示式會暫停當前 async function 的執行,等待 Promise 處理完成。若 Promise 正常處理(fulfilled),其回撥的resolve函式引數作為 await 表示式的值,繼續執行 async function

    ​ 若 Promise 處理異常(rejected),await 表示式會把 Promise 的異常原因丟擲。

    ​ 另外,如果 await 操作符後的表示式的值不是一個 Promise,那麼該值將被轉換為一個已正常處理的 Promise。

2. 實踐—簡單用法

這會使 async 函式暫停執行,等待表示式中的 Promise 解析完成後繼續執行 async 函式並返回解決結果。

//用法1
/*
	async 返回一個 Promise
	1. return 值(value),則返回 Promise.resolve(value)
	2. 異常,則是 Promise.reject(err);
  */
async function testAsync() {
    return "hello async";
}

const result = testAsync();
console.log(result);//返回一個promise物件
//用法2
//async 函式中可能會有 await 表示式,這會使 async 函式暫停執行,等待表示式中的 Promise 解析完成後繼續執行 async 函式並返回解決結果。
function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
  return value;   //類似 return Promise.resolve(value)
}
//async 返回一個promise
asyncPrint('hello world', 50).then(function(d){
   console.log('then',d);
});
/** 列印
hello world
then hello world
*/
//await 必須的在 async方法內,否則會報錯
function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

 function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
  return value;   //類似 return Promise.resolve(value)
}
//async 返回一個promise
asyncPrint('hello world', 50).then(function(d){
   console.log('then',d);
});
//Uncaught SyntaxError: await is only valid in async function

2. Async對比Promise優勢

1. 解決then 多層回撥

參考:理解 JavaScript 的 async/await

假設:假設一個業務,分多個步驟完成,每個步驟都是非同步的,而且依賴於上一個步驟的結果。我們仍然用 setTimeout 來模擬非同步操作:

/**
 * 傳入引數 n,表示這個函式執行的時間(毫秒)
 * 執行的結果是 n + 200,這個值將用於下一步驟
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}
//Promise方案
function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
//async 寫法
//對比 promise寫法,
async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}
doIt();

2. 帶catch

//promise 版本
function getProcessedData(url) {
  return downloadData(url) // returns a promise
            .catch(e => {
                return downloadFallbackData(url)  // 返回一個 promise 物件
                        .then(v => {
                            return processDataInWorker(v); // 返回一個 promise 物件
                        });
            })
            .then(v => {
                return processDataInWorker(v); // 返回一個 promise 物件
            });
}
//Async 版本
async function getProcessedData(url) {
  let v;
  try {
    v = await downloadData(url);
  } catch (e) {
    v = await downloadFallbackData(url);
  }
  return processDataInWorker(v);
}
//注意,在上述示例中,return 語句中沒有 await 操作符,因為 async function 的返回值將隱式傳遞給 Promise.resolve。

3.Async並行

0. 背景

對比 promise 並行處理

前面解決都是 一個promise執行完後,再執行新的promise;而下面討論是,兩個Promise如何並行

1. 基本並行處理

// 方法 1
let [res1, res2] = await Promise.all([func1(), func2()])

// 方法 2
let func1Promise = func1()
let func2Promise = func2()
let res1 = await func1Promise
let res2 = await func2Promise

2. 深入理解並行

上文基本的並行,並不是 正在的並行

參考:MDN 下面程式碼參考自 MDN

var resolveAfter2Seconds = function() {
  console.log("starting slow promise");
  return new Promise(resolve => {
    setTimeout(function() {
      resolve(20);
      console.log("slow promise is done");
    }, 2000);
  });
};

var resolveAfter1Second = function() {
  console.log("starting fast promise");
  return new Promise(resolve => {
    setTimeout(function() {
      resolve(10);
      console.log("fast promise is done");
    }, 1000);
  });
};

var sequentialStart = async function() {
  console.log('==SEQUENTIAL START==');

  // 如果 await 操作符後的表示式不是一個 Promise 物件, 則它會被轉換成一個 resolved 狀態的 Promise 物件
  const slow = await resolveAfter2Seconds();

  const fast = await resolveAfter1Second();
  console.log(slow);
  console.log(fast);
}

var concurrentStart = async function() {
  console.log('==CONCURRENT START with await==');
  const slow = resolveAfter2Seconds(); // 立即啟動計時器
  const fast = resolveAfter1Second();

  console.log(await slow);
  console.log(await fast); // 等待 slow 完成, fast 也已經完成。
}

var stillSerial = function() {
  console.log('==CONCURRENT START with Promise.all==');
  Promise.all([resolveAfter2Seconds(), resolveAfter1Second()]).then(([slow, fast]) => {
    console.log(slow);
    console.log(fast);
  });
}

var parallel = function() {
  console.log('==PARALLEL with Promise.then==');
  resolveAfter2Seconds().then((message)=>console.log(message)); // in this case could be simply written as console.log(resolveAfter2Seconds());
  resolveAfter1Second().then((message)=>console.log(message));
}

sequentialStart(); // sequentialStart 總共花了 2+1 秒
// 等到 sequentialStart() 完成
setTimeout(concurrentStart, 4000); // concurrentStart 總共花了 2 秒
// 等到 setTimeout(concurrentStart, 4000) 完成
setTimeout(stillSerial, 7000); // stillSerial 總共花了 2 秒
// 等到 setTimeout(stillSerial, 7000) 完成
setTimeout(parallel, 10000); // 真正的並行執行

上面程式碼是4中,不同處理promise並行方式。但核心是不管怎樣 await執行都會有順序,會等待執行。

4. 並行——迴圈

1. for-of

//不推薦,因為是序列解決
function fetch(d){
	return new Promise((resolve)=>{
        	console.log('start:',d);
			setTimeout(()=>{resolve(d)},Math.random()*1000);
		})
}
var args = [1,2,3,4,5];
async function test(args){
	for(const arg of args){
		const res = await fetch(arg);
		console.log('end:',res);
    }
}
test(args);
/**
   	start: 1
    23:00:11.331 bundle.9303569f0937a02f1c80.js:4 end: 1
    23:00:11.332 bundle.9303569f0937a02f1c80.js:4 start: 2
    23:00:12.009 bundle.9303569f0937a02f1c80.js:4 end: 2
    23:00:12.009 bundle.9303569f0937a02f1c80.js:4 start: 3
    23:00:12.248 bundle.9303569f0937a02f1c80.js:4 end: 3
    23:00:12.248 bundle.9303569f0937a02f1c80.js:4 start: 4
    23:00:12.984 bundle.9303569f0937a02f1c80.js:4 end: 4
    23:00:12.984 bundle.9303569f0937a02f1c80.js:4 start: 5
    23:00:13.184 bundle.9303569f0937a02f1c80.js:4 end: 5
*/

2. 使用map並行執行

function fetch(d){
	return new Promise((resolve)=>{
			console.log('start:',d);
			setTimeout(()=>{resolve(d)},Math.random()*1000);
		})
}
var args = [1,2,3,4,5];
async function test3(args){
	const promises = args.map(async arg=>{//map 執行 可以並行執行
		const re = await fetch(arg);
		return re;
	})
	for(const p of promises){
		p.then((d)=>{
			console.log('end',d);
		})
	}
}

test3(args);
/**
    start: 1
    22:56:44.421 VM6500:3 start: 2
    22:56:44.421 VM6500:3 start: 3
    22:56:44.422 VM6500:3 start: 4
    22:56:44.422 VM6500:3 start: 5
    22:56:44.436 Promise {<resolved>: undefined}
    22:56:44.462 VM6500:15 end 2
    22:56:44.552 VM6500:15 end 1
    22:56:44.569 VM6500:15 end 5
    22:56:44.974 VM6500:15 end 3
    22:56:44.993 VM6500:15 end 4
*/

Array.prototype.mapArray.prototype.forEach 執行promise陣列,是並行。

for-in for-of for都是序列的

5. 異常處理

0.異常分類

參考:Promise異常分類

異常簡單分為分為 執行異常和非同步異常(通過是否能try-catch捕獲來區分);

1. 基本套路

//套路1
async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error('出錯了');
  });
  await Promise.resolve('1')
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))

//套路2
async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出錯了');
    });
  } catch(e) {
    console.log(e)
  }
}
f()
//為什麼 Promise 無法使用try-catch捕獲異常,但 async中,可以捕獲?
//猜測可能是,await返回是一個值,執行上下文應該是同一個

2. 鏈式處理

//在基本套路1 基礎上處理 與promise 鏈式異常處理對比
async function f() {
  await new Promise(function (resolve, reject) {
    console.log('1')
    throw new Error('出錯了');
  });
  await new Promise(function(resolve, reject){
	console.log('2');//沒有列印
    resolve(2);
  })
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
/**
    1
    Error: 出錯了
        at <anonymous>:4:11
        at new Promise (<anonymous>)
        at f (<anonymous>:2:9)
        at <anonymous>:12:1
*/

重點: 第二個 await沒有執行,(‘2’沒有列印);也就證明,async遇到異常 就會中斷鏈,與Promise鏈式異常對比

//在基本套路2 基礎上處理 對比
async function f() {
  try {
    await new Promise(function (resolve, reject) {
          console.log('222');
          throw new Error('出錯了');
    });
    await new Promise(function