Promise對象、Generator 函數、async函數

一、Promise對象的含義、特點

含義: 異步編程的一種解決方案,傳統解決方案通過回調函數和時間,而Promise相當於一個容器,裏面保存着未來纔會結束的事件(通常是一個異步操作)的結果,從語法上說,Promise是一個對象,可以獲取異步操作的消息,提供統一的API,各種異步操作都可以以同樣的方法進行處理
特點:(1)對象的狀態不受外界影響,有三種狀態:pending(進行中)、fulfilled(已成功)、rejected(已失敗),只有異步操作的結果可以改變當前的狀態,其他任何操作都無法改變這個狀態。
(2)狀態一旦改變就不會在變了,任何時候都可以得到這個結果Promise對象的狀態改變只有兩種可能,從pendingfulfilled,從pendingrejected,只要狀態發生改變,就會一直保持這個結果,就算再對對象Promise添加回調函數,也會立即得到這個結果,與事件Event不同,事件的特帶你是如果錯過了它,再去監聽,是得不到結果的
(3)Promise一旦新建就會立即執行,無法途中取消,如果設置回調函數,Promise內部拋出錯誤,不會反應到外部,不會影響整體函數的執行,處於pending狀態時,無法得知當前進展哪一個階段

基本用法

在這裏插入圖片描述
ES6 規定,Promise對象是一個構造函數,用來生成Promise實例。

        let promise=new Promise(function (resolve,reject){
            setTimeout(resolve,100,1);
        });

Promise構造函數接受一個函數作爲參數,該函數的兩個參數也是一個函數,分別是resolvedrejectedresolvedPromise對象的狀態從未完成變爲成功,成功時的回調函數,會將異步操作的結果,作爲參數傳遞出去rejectedPromise對象的狀態由未完成變爲失敗,失敗時調用的函數,也會將異步操作報出的錯誤,作爲參數傳遞出去

注:setTimeout(resolve,100,1);,第三個參數時第一個參數函數的的形參

        promise.then(function (res){
            console.log(res);//1
        },function (arr){
            console.log(arr);
        }).catch(function (err){
            console.log(err);
        })

then方法接受兩個回調函數作爲參數,第一個回調函數是Promise對象的狀態變爲resolved時調用,第二個回調函數Promise對象變爲rejected時調用,這個函數是可選的,不一定要提供,這兩個函數都接受Promise對象傳出的值作爲參數Promise對象的狀態發生改變時就會觸發then方法綁定的回調函數
then()方法的作用是Promise實例添加解決(fulfillment)和拒絕(rejection)狀態的回調函數then()方法會返回一個新的Promise實例所以then()方法後面可以繼續跟另一個then()方法進行鏈式調用

        //promise執行的先後順序
        let promise = new Promise(function (resolve, reject) {
            console.log("start");
            setTimeout(resolve, 1000, "shuju");
        });
        console.log("外部");
        promise.then(function (res) {
            console.log(res);
            //對於res的操作需要在此代碼塊寫,因爲外部獲取不到
        });
        console.log("wan");

在這裏插入圖片描述
圖中的數據是其他代碼的結果,忽略掉,從結果看就是,Promise一旦新建就立即執行,輸入start,與此同時輸入同步結果外部、完,當setTimeout(resolve, 1000, "shuju");執行完後,即Promise狀態改變後,resolve函數的結果作爲形參傳入then(),並輸出結果!
異步加載圖片

        let promise = new Promise(function (resolve, reject) {
            let img = new Image();
            img.onload = function () {
                resolve(img);
            };
            img.onerror = function () {
                reject(new Error("圖片加載失敗" + url));
            }
        });
        promise.then(function (res) {
            console.log(res);
        }).catch(function (err) {
            console.log(err);
        })

Promise對象實現的 Ajax 操作

const getJSON = function(url) {
  const promise = new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

  });

  return promise;
};

getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('出錯了', error);
});

多個Promise連用

        const promise1=new Promise(function(resolve,reject){
           setTimeout(resolve,3000,"a");
        })
        const promise2=new Promise(function(resolve,reject){
            setTimeout(resolve,1000,promise1);
        })
        promise2.then(function(res){
            console.log(res);
        })

在這裏插入圖片描述
promise1promise2都是 Promise 的實例,但是promise2resolve方法將promise1作爲參數,即一個異步操作的結果是返回另一個異步操作,promise1的狀態就會傳遞給promise2promise2的回調函數就會等待promise1執行完,狀態改變後再執行
再來一個例子

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail

上面代碼中,p1是一個 Promise,3 秒之後變爲rejectedp2的狀態在 1 秒之後改變,resolve方法返回的是p1。由於p2返回的是另一個 Promise,導致p2自己的狀態無效了,由p1的狀態決定p2的狀態。所以,後面的then語句都變成針對後者(p1)。又過了 2 秒,p1變爲rejected,導致觸發catch方法指定的回調函數。

多個then()鏈式調用

var p2 = new Promise(function (resolve) {
    resolve(100);
});
p2.then(function (value) {
    return value * 2;
}).then(function (value) {
    return value * 2;
}).then(function (value) {
    console.log("finally: " + value);
});

調用then()方法之後都會新建一個promise對象,這裏的return語句相當於Promise語句中的resolve()或者rejected(),他們的結果,作爲then方法函數的參數傳入

多個Promise合併
Promise.all()

Promise.all()方法用於將多個 Promise 實例,包裝成一個新的 Promise 實例。

const p = Promise.all([p1, p2, p3]);

上面代碼中,Promise.all()方法接受一個數組作爲參數,p1p2p3都是 Promise 實例,如果不是,就會先調用下面講到的Promise.resolve方法,將參數轉爲 Promise 實例,再進一步處理。另外,Promise.all()方法的參數可以不是數組,但必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例。

p的狀態由p1p2p3決定,分成兩種情況。

(1)只有p1p2p3的狀態都變成fulfilled,p的狀態纔會變成fulfilled,此時p1p2p3返回值組成一個數組,傳遞給p的回調函數。

(2)只要p1p2p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值,會傳遞給p的回調函數。

如果作爲參數的 Promise 實例,自己定義了catch方法,那麼它一旦被rejected,並不會觸發Promise.all()catch方法

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result)
.catch(e => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error('報錯了');
})
.then(result => result)
.catch(e => e);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 報錯了

上面代碼中,p1resolvedp2首先會rejected,但是p2有自己的catch方法,該方法返回的是一個新的 Promise 實例,p2指向的實際上是這個實例。該實例執行完catch方法後,也會變成resolved,導致Promise.all()方法參數裏面的兩個實例都會resolved,因此會調用then方法指定的回調函數,而不會調用catch方法指定的回調函數。

如果p2沒有自己的catch方法,就會調用Promise.all()catch方法。

const p1 = new Promise((resolve, reject) => {
  resolve('hello');
})
.then(result => result);

const p2 = new Promise((resolve, reject) => {
  throw new Error('報錯了');
})
.then(result => result);

Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 報錯了

Promise.race()

Promise.race()方法同樣是將多個 Promise 實例,包裝成一個新的 Promise 實例。

const p = Promise.race([p1, p2, p3]);

上面代碼中,只要p1p2p3之中有一個實例率先改變狀態,p的狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數。

二、Generator 函數的語法

Generator 函數是 ES6 提供的一種異步編程解決方案,語法行爲與傳統函數完全不同
Generator 函數是一個狀態機,封裝了多個內部狀態。
執行 Generator 函數會返回一個遍歷器對象。Generator 函數除了狀態機,還是一個遍歷器對象生成函數。返回的遍歷器對象,可以依次遍歷 Generator 函數內部的每一個狀態

Generator 函數有兩個特徵。一是,function關鍵字與函數名之間有一個星號;二是,函數體內部使用yield表達式,定義不同的內部狀態,星號*的位置以下幾個都是合法的

//*的位置
//建議使用這一個
function* sum(x){
    yield x+(yield x*2);
    yield x+3;
}

function * sum(x){
    yield x+(yield x*2);
    yield x+3;
}

function  *sum(x){
    yield x+(yield x*2);
    yield x+3;
}

調用 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象,也就是遍歷器對象。

function* c(){
   yield 'hello';
   yield 'world';
   return 'over'; 
}
console.log(c);/*ƒ* c(){
   yield 'hello';
   yield 'world';
   return 'over'; 
}*/
console.log(c());

在這裏插入圖片描述

總結:調用 Generator 函數,返回一個遍歷器對象,代表 Generator 函數的內部指針。以後,每次調用遍歷器對象的next方法,就會返回一個有着valuedone兩個屬性的對象。value屬性表示當前的內部狀態的值,是yield表達式後面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。yield表達式是暫停執行的標記,而next方法可以恢復執行

function* c(){
   yield 'hello';
   yield 'world';
   return 'over'; 
}
console.log(c);/*ƒ* c(){
   yield 'hello';
   yield 'world';
   return 'over'; 
}*/
console.log(c());
var d=c();
console.log(d.next());//{value: "hello", done: false}
console.log(d.next());//{value: "world", done: false}
console.log(d.next());//{value: "over", done: true}

//遍歷結束的時候,即done爲true時,以後的next()  value值都爲undefined
console.log(d.next());//{value: undefined, done: true}
console.log(d.next());//{value: undefined, done: true}

遍歷器對象的next方法的運行邏輯如下

(1)遇到yield表達式,就暫停執行後面的操作,並將緊跟在yield後面的那個表達式的值,作爲返回的對象的value屬性值。

(2)下一次調用next方法時,再繼續往下執行,直到遇到下一個yield表達式。

(3)如果沒有再遇到新的yield表達式,就一直運行到函數結束,直到return語句爲止,並將return語句後面的表達式的值,作爲返回的對象的value屬性值。

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

yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行,因此等於爲 JavaScript 提供了手動的“惰性求值”的語法功能

function *sum1(){
    yield 12+34;
}
console.log(sum1().next());//46

yield表達式與return語句既有相似之處,也有區別
相似之處在於,都能返回緊跟在語句後面的那個表達式的值。
區別在於每次遇到yield,函數暫停執行,下一次再從該位置繼續向後執行,而return語句不具備位置記憶的功能。一個函數裏面,只能執行一次(或者說一個)return語句,但是可以執行多次(或者說多個)yield表達式。正常函數只能返回一個值,因爲只能執行一次return

複雜一點的例子

function * sum(x){
    yield x+(yield x*2);
}
var sums=sum(3);
//第一種
console.log(sums.next());//6 false
console.log(sums.next());//NAN false
console.log(sums.next());//undefined true

//第二種
console.log(sums.next());//6 false
console.log(sums.next(90));//93 false
console.log(sums.next());//undefined true



世紀大難題!!!被自己寫的函數難住了!!!
這裏需要注意的是

yield表達式如果用在另一個表達式之中,必須放在圓括號裏面。
yield表達式用作函數參數或放在賦值表達式的右邊,可以不加括號。
yield表達式只能用在 Generator 函數裏面,用在其他地方都會報錯。

for…of循環
for...of循環可以自動遍歷 Generator 函數運行時生成的Iterator對象,且此時不再需要調用next方法

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5

一旦next方法的返回對象的done屬性爲truefor...of循環就會中止,且不包含該返回對象,所以上面代碼的return語句返回的6,不包括在for...of循環之中。

yield*表達式

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同於
function* bar() {
  yield 'x';
  for (let v of foo()) {
    yield v;
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"
function* inner() {
  yield 'hello!';
}

function* outer1() {
  yield 'open';
  yield inner();
  yield 'close';
}

var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一個遍歷器對象
gen.next().value // "close"

function* outer2() {
  yield 'open'
  yield* inner()
  yield 'close'
}

var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
let delegatedIterator = (function* () {
  yield 'Hello!';
  yield 'Bye!';
}());

let delegatingIterator = (function* () {
  yield 'Greetings!';
  yield* delegatedIterator;
  yield 'Ok, bye.';
}());

for(let value of delegatingIterator) {
  console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."
//yield*後面的 Generator 函數(沒有return語句時),等同於在 Generator 函數內部,部署一個for...of循環。

function* concat(iter1, iter2) {
  yield* iter1;
  yield* iter2;
}

// 等同於

function* concat(iter1, iter2) {
  for (var value of iter1) {
    yield value;
  }
  for (var value of iter2) {
    yield value;
  }
}

三、async函數

async表示函數裏有異步操作,await表示緊跟在後面的表達式需要等待結果。
async函數的await命令後面,可以是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時會自動轉成立即 resolved 的 Promise 對象)。
async函數的返回值是 Promise 對象
async函數完全可以看作多個異步操作,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

例子:下面代碼指定 50 毫秒以後,輸出hello world

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

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

asyncPrint('hello world', 50);

async 函數有多種使用形式。

// 函數聲明
async function foo() {}

// 函數表達式
const foo = async function () {};

// 對象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then();

// 箭頭函數
const foo = async () => {};

返回Promise對象

async函數返回一個 Promise 對象。

async函數內部return語句返回的值,會成爲then方法回調函數的參數。
async函數返回的 Promise 對象,必須等到內部所有await命令後面的 Promise 對象執行完,纔會發生狀態改變,除非遇到return語句或者拋出錯誤,也就是說,只有async函數內部的異步操作執行完,纔會執行then方法指定的回調函數。
await命令後面是一個 Promise 對象,返回該對象的結果。如果不是 Promise 對象,就直接返回對應的值
await命令後面是一個thenable對象(即定義then方法的對象),那麼await會將其等同於 Promise 對象。
await命令後面的 Promise 對象如果變爲reject狀態,則reject的參數會被catch方法的回調函數接收到

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

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

任何一個await語句後面的 Promise 對象變爲reject狀態,那麼整個async函數都會中斷執行

async function f() {
  await Promise.reject('出錯了');
  await Promise.resolve('hello world'); // 不會執行
}

最好把await命令放在try...catch代碼塊中

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

// 另一種寫法

async function myFunction() {
  await somethingThatReturnsAPromise()
  .catch(function (err) {
    console.log(err);
  });
}

多個await命令後面的異步操作,如果不存在繼發關係,最好讓它們同時觸發。

let foo = await getFoo();
let bar = await getBar();

上面代碼中,getFoogetBar是兩個獨立的異步操作(即互不依賴),被寫成繼發關係。這樣比較耗時,因爲只有getFoo完成以後,纔會執行getBar,完全可以讓它們同時觸發。

// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

上面兩種寫法,getFoogetBar都是同時觸發,這樣就會縮短程序的執行時間。

await命令只能用在async函數之中,如果用在普通函數,就會報錯。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  // 報錯
  docs.forEach(function (doc) {
    await db.post(doc);
  });
}

上面代碼會報錯,因爲await用在普通函數之中了。但是,如果將forEach方法的參數改成async函數,也有問題。

unction dbFuc(db) { //這裏不需要 async
  let docs = [{}, {}, {}];

  // 可能得到錯誤結果
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}

上面代碼可能不會正常工作,原因是這時三個db.post操作將是併發執行,也就是同時執行,而不是繼發執行。正確的寫法是採用for循環。

async function dbFuc(db) {
  let docs = [{}, {}, {}];

  for (let doc of docs) {
    await db.post(doc);
  }
}

如果確實希望多個請求併發執行,可以使用Promise.all方法。當三個請求都會resolved時,下面兩種寫法效果相同。

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}

// 或者使用下面的寫法

async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章