天天看點

ES6--Promise、Generator及async

版權聲明:本文為部落客原創文章,遵循 CC 4.0 by-sa 版權協定,轉載請附上原文出處連結和本聲明。

本文連結:https://ligang.blog.csdn.net/article/details/70843713

ES6誕生以前,異步程式設計的方法,大概有如下四種:回調函數、事件監聽、釋出/訂閱、Promise對象;ES6中,引入了Generator函數;ES7中,async更是将異步程式設計帶入了一個全新的階段。

十四、Promise對象

​ Promise,就是一個對象,用來傳遞異步操作的消息,避免了層層嵌套的回調函數。它代表了某個未來才會知道結果的事件(通常是一個異步操作),并且這個事件提供統一的API,可供進一步處理。

(1)對象的狀态不受外界影響。有三種狀态:Pending(進行中)、Resolved(已完成,又稱Fulfilled)和Rejected(已失敗)。

(2)一旦狀态改變,就不會再變,任何時候都可以得到這個結果。Promise對象的狀态改變,隻有兩種可能:從Pending變為Resolved和從Pending變為Rejected。隻要這兩種情況發生,狀态就凝固了,不會再變了,會一直保持這個結果。

缺點:

  • 無法取消Promise,一旦建立它就會執行,無法中途取消
  • 如果不設定回調函數,Promise内部抛出的錯誤,不會反應到外部
  • 當處于Pending狀态時,無法得知目前進展到哪一個階段(剛剛開始還是即将完成)。

更多Promise請參考:http://blog.csdn.net/ligang2585116/article/details/51417334

十五、Generator函數

​ 從計算機角度看,生成器是一種類協程或半協程,它提供了一種可以通過特定語句或方法使其執行對象暫停的功能。

​ Generator函數,傳回一個部署了Iterator接口的周遊器對象,用來操作内部指針。以後,每次調用周遊器對象的next方法,就會傳回一個有着value和done兩個屬性的對象。value屬性表示目前的内部狀态的值,是yield語句後面那個表達式的值;done屬性是一個布爾值,表示是否周遊結束。

yield [[expression]]

​ yield 關鍵字使生成器函數暫停執行,并傳回跟在它後面的表達式的目前值。可以把它想成是return關鍵字的一個基于生成器的版本,但其并非退出函數體,而是切出目前函數的運作時,與此同時可以将一個值帶到主線程中。yield語句是暫停執行的标記,而next方法可以恢複執行。

function* gen(){
  yield 'li';
  yield 'gang'; // 有誤!!!
  return '!';
}
var g = gen();
g.next(); // {value: 'li', done: false}
g.next(); // {value: 'gang', done: false}
g.next(); // {value: '!', done: true}           

複制

(1)遇到yield語句,就暫停執行後面的操作,并将緊跟在yield後面的那個表達式的值,作為傳回的對象的value屬性值;

(2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield語句;

(3)如果沒有再遇到新的yield語句,就一直運作到函數結束,直到return語句為止,并将return語句後面的表達式的值,作為傳回的對象的value屬性值;

(4)如果該函數沒有return語句,則傳回的對象的value屬性值為undefined。

需要注意的是,yield語句後面的表達式,隻有當調用next方法、内部指針指向該語句時才會執行,是以等于為JavaScript提供了手動的“惰性求值”(Lazy Evaluation)的文法功能。

function* gen() {
  yield  123 + 456;
}           

複制

​ 上述示例中,yield後面的表達式123 + 456,不會立即求值,隻會在next方法将指針移到這一句時,才會求值。Generator函數也可以不用yield語句,這時就變成了一個單純的暫緩執行函數。

function* f() {
  console.log('執行了!')
}
let gen = f();
setTimeout(function () {
  gen.next()
}, 2000);           

複制

next方法的參數

​ 注意: yield句本身沒有傳回值(傳回undefined)。next方法可以帶一個參數,該參數就會被當作上一個yield語句的傳回值。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next();  // Object{value:6, done:false}
a.next();  // Object{value:NaN, done:false}
a.next();  // Object{value:NaN, done:true}

var b = foo(5);
b.next();   // { value:6, done:false }
b.next(12); // { value:8, done:false }
b.next(13); // { value:42, done:true }           

複制

​ next方法不帶參數,導緻y的值等于

2 * undefined

(即NaN),除以3以後還是NaN;next方法提供參數,第一次調用b的next方法時,傳回x+1的值6;第二次調用next方法,将上一次yield語句的值設為12,是以y等于24,傳回y/3的值8。

​ 注意:這個功能有很重要的文法意義。Generator函數從暫停狀态到恢複運作,它的上下文狀态(context)是不變的。通過next方法的參數,就有辦法在Generator函數開始運作之後,繼續向函數體内部注入值。也就是說,可以在Generator函數運作的不同階段,從外部向内部注入不同的值,進而調整函數行為。

function* f() {
  for(let i=0; true; i++) {
    let reset = yield i;
    if(reset) { i = -1; }
  }
}

let g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }           

複制

for…of循環

for...of

循環可以自動周遊Generator函數時生成的Iterator對象,且此時不再需要調用next方法。

function *foo() {
  yield 1;
  yield 2;
  return 3;
}
for (let v of foo()) {
  console.log(v);
}           

複制

利用Generator函數和

for...of

循環,實作斐波那契數列

function* fibonacci() {
  let [prev, curr] = [0, 1];
  while (true) {
    [prev, curr] = [curr, prev + curr];
    yield curr;
  }
}
for (let n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}           

複制

yield* [[expression]]

yield*

一個可疊代對象,就相當于把這個可疊代對象的所有疊代值分次 yield 出去。表達式本身的值就是目前可疊代對象疊代完畢(當done為true時)時的傳回值。

function* gen(){
  yield [1, 2];
  yield* [3, 4];
}
var g = gen();
g.next(); // {value: Array[2], done: false}
g.next(); // {value: 3, done: false}
g.next(); // {value: 4, done: false}
g.next(); // {value: undefined, done: true}           

複制

判斷是否為Generator函數

function isGenerator(fn){
  // 生成器示例必帶@@toStringTag屬性
  if(Symbol && Symbol.toStringTag) {
    return fn[Symbol.toStringTag] === 'GeneratorFunction';
  }
}           

複制

十六、async函數

​ async函數可以了解為Generator函數的文法糖,使用async内置了執行器,無需調用next方法進行逐漸調用。且其傳回值為Promise。

基本用法

async

函數傳回一個 Promise 對象,可以使用

then

方法添加回調函數。當函數執行的時候,一旦遇到

await

就會先傳回,等到異步操作完成,再接着執行函數體内後面的語句。

async function gen(x){
    var y = await x + 2;
    var z = await y + 2;
    return z;
}
gen(1).then(result => console.log(result), error => console.log(error));
gen(1).then(result => console.log(result)).catch(error => console.log(error));           

複制

5秒以後,輸出

hello world

/*回調方式*/
function test(callback) {
  setTimeout(() => callback('hello world'), 5000);
}
var res = test((value) => console.log(value));
console.log(res);

/*Promise方式*/
function test() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve('hello world');
    }, 5000)
  });
}
test().then((value) => console.log(value));

/*async方式*/
async function test() {
  return await new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve('hello world');
    }, 5000)
  });
}
test().then((value) => console.log(value));           

複制

await指令

正常情況下,await指令後是一個Promise對象。如果不是,會被轉成一個立即resolve的Promise對象。

/*成功情況*/
async function f() {
  return await 123;
}
f().then(value => console.log(value));  // 123
/*失敗情況*/
async function f() {
  return Promise.reject('error');
}
f().catch(e => console.error(e));   // error           

複制

錯誤處理

async function f() {
  await new Promise((resolve, reject) => {
    throw new Error('出錯了');
  })
}
f().then(v => console.log(v))
  .catch(e => console.error(e));  // Error: 出錯了           

複制

注意點

await隻能用在async函數中,不能用在普通函數中

/* 錯誤處理 */
function f(db) {
  let docs = [1, 2, 3];
  for(let doc of docs) {
    await db.push(doc);
  }
  return db; // Uncaught SyntaxError: Unexpected identifier
}

/* 正确處理(順序執行) */
async function f(db) {
  let docs = [1, 2, 3];
  for(let doc of docs) {
    await db.push(doc);
  }
  return db;
}
let ary = [];
f(ary);
console.log(ary); // [1, 2, 3]

/* 正确處理(并發執行) */
async function f(db) {
  let docs = [1, 2, 3];
  let promises = docs.map(doc => db.push(doc));
  db = await Promise.all(promises);
  return db;
}
let ary = [];
f(ary);
console.log(ary); // [1, 2, 3]           

複制

await後面可能存在reject,需要進行try…catch代碼塊中

async function f() {
  try {
    await Promise.reject('出錯了');
  } catch(e) {
    console.error(e);
  }
  return Promise.resolve('hello');
}
f().then(v => console.log(v));   // 出錯了 hello           

複制

多個異步操作,如果沒有繼承關系,最好同時觸發

async function f() {
  let [foo, bar] = await Promise.all([getFoo(), getBar()]);
  return [foo, bar];
}           

複制

十七、Promise和async使用場景

​ 下述描述的使用場景,隻是自己在開發中經常遇到的,并不一定完全符合所有人的使用,隻是這樣用起來會很便利和實作一些比較難處理的情況。

一個過程中同時存在異步、同步情況,請使用Promise

/*正常方式,錯誤!不能實作*/
function test(bool) {
  // bool為true,直接傳回"hello"
  // bool為false,進行異步請求,這裡使用setTimeout代替異步過程
  if(bool) {
    return "hello";
  } else {
    setTimeout(() => {
      return "world";
    }, 5000); 
  }
}
test(true);  // "hello"
test(false); // 無任何輸出内容

/*Promise正确方式*/
function test(bool) {
  // bool為true,直接傳回"hello"
  // bool為false,進行異步請求,這裡使用setTimeout代替異步過程
  return new Promise((resolve, reject) => {
    if(bool) {
        return resolve("hello");
    } else {
        setTimeout(() => {
          return resolve("world");
        }, 5000); 
    }
  });
}
test(true).then(v => console.log(v));   // 'hello'
test(false).then(v => console.log(v));  // 大約5s後輸出 'world'           

複制

包裹本身不支援async的函數,且hold住目前請求

import fs from "fs";

async function readFile(filepath) {
    return await new Promise((resolve, reject) => {
        fs.stat(filepath, (error) => {
            if(error) {
                return reject("檔案不存在!");
            }
            let content = fs.readFileSync(filepath, "utf8");
            return resolve(content);
        })
    })
}
// 測試
readFile(__filename).then((content) => {
    console.log(content)
}).catch((error) => {
    console.error(error);
});           

複制