博客 / 詳情

返回

JavaScript中異步與同步,回調函數,Promise ,async與await之滿漢全席

一、異步與同步

理解異步與同步

1、在生活中,按照字面量來理解,異步指的是,比如我先吃完蘋果再看電視,這是我們生活中理解的異步。同步就是我邊吃蘋果邊看電視。

2、然而,對我們的電腦程序來説,同步與異步的概念恰恰跟我們在生活中的理解完全相反。同步指的是我先吃完蘋果,然後再看電視(執行完一個事件再去執行另一個事件,若上一個事件沒執行完,下一個事件就無法執行);異步指的是我邊吃蘋果邊看電視(多個事件可以同時執行,不會發生阻塞)。理解js中的異步和同步,如果對這兩個概念混淆了,你只要想到跟我們生活中異步與同步的理解相反就行了。

談談JavaScript的單線程機制

單線程是JavaScript中一個比較重要的特性。這就表示在同一個時間只能做一件事情。

那為什麼JavaScript不弄成多線程的呢?還能更加充分利用CPU呢。

這要從JavaScript的使用場景説起,JavaScript作為瀏覽器語言,主要用途是通過操作DOM,與用户進行交互。

我們設想一下,如果一個線程去更新某個DOM元素,而另一個線程去刪除這個DOM元素,那麼瀏覽器該執行哪個操作呢?這就出現衝突了。

因此,為了避免複雜的多線陳機制,JavaScript從設計之初就選擇了單線程標準。

Event Loop 事件循環

單線程那就表示事件只能一個一個輪着執行,前面沒執行完後面就無法執行(只能乾等着,這樣也太低效了)。比如,我用ajax向服務端請求一個數據(假設數據量很大,花費時間很久),這時候就會卡在那裏,因為後面的只能等前一個事件把數據成功請求回來再執行接下的代碼,這樣對用户的體驗就太不友好了。這時候 任務隊列 就出場了。JS中將所有任務分為同步任務異步任務

目前個人的理解是,只有所有的同步任務執行完才會去執行異步任務。常見的異步任務有setTimeout函數,http請求,數據庫查詢等。

(1)所有任務都在主線程上執行,形成一個執行棧(execution context stack)。
(2)主線程之外,還存在一個"任務隊列"(task queue)。系統把異步任務放到"任務隊 列"之中,然後繼續執行後續的任務。
(3)一旦"執行棧"(事件循環隊列)中的所有任務執行完畢,系統就會讀取"任務隊列"。如果這個時候,異步任務已經結束了等待狀態,就會從"任務隊列"進入執行棧,恢復執行。
(4)主線程不斷重複上面的第三步。

詳情查看此文章

 console.log('這裏是調試1');
 setTimeout(() => {
     console.log('這裏是調試2');
 },0)
 console.log('這裏是調試3');
 console.log('這裏是調試4');
 console.log('這裏是調試5');
 // 輸出
 這裏是調試1
 這裏是調試3
 這裏是調試4
 這裏是調試5
 這裏是調試2 // setTimeout中的最後面才輸出來

異步任務之微任務&宏任務

異步任務分為宏任務(macrotasks)和微任務(microtasks)。常見的宏任務有(setTimeoutsetIntervalsetImmediate),常見的微任務有(Promise.then)。在一個事件循環中,當執行完所有的同步任務後,會在任務隊列中取出異步任務,這時候會優先執行微任務,接着再執行宏任務。換句話説,微任務就是為了插隊而存在的。看一下下面這個例子:

setTimeout(() => console.log(1));
new Promise((resolve) => {
   resolve();
   console.log(2);
}).then(() => {
   console.log(3);
})
console.log(4);

輸入結果為:2 - 4 - 3 - 1

setTimeout(...)相關

1、在setTimeout(...)中可以用async(...)await(...),但是隻能當前setTimeout(...)執行棧中生效;
2、同一層級的多個setTimeout(...)相互獨立,就算setTimeout(...)中有await(...)也不會影響到其他setTimeout(...)的執行;

阮一峯 JavaScript運行機制詳解
樸靈 標註
mdn

選擇異步還是同步?

1、在JS中默認的方式是同步的,個別是異步的(http請求,setTimeout)。假設現在有一個場景,我們需要從後端接口獲取數據(異步任務),然後對該數據進行處理,這時候我們就需要將獲取數據這一異步任務阻塞,也就是將其轉化為同步任務。

2、常見的處理異步(將異步變為同步)的方式按照出現時間排序有:回調函數Promiseasync與await

二、閉包與回調函數

1、無論通過何種手段將內部函數傳遞到這個函數定義時的詞法作用域(定義時的詞法作用域指的是函數定義時的那個位置)以外執行,內部函數都會持有對原始定義作用域的引用,無論在何處執行這個函數都會使用閉包。
2、無論函數在哪裏被調用,也無論它如何被調用,它的詞法作用域都只由函數被聲明時所處的位置決定。
3、閉包使得函數可以繼續訪問定義時的詞法作用域。
4、只要使用了回調函數,實際上就是在使用閉包。
---《你不知道的JavaScript》

1、一般來説,block(塊級)作用域被執行完後,裏面的數據就被回收了。

2、作用: 將某個塊級作用域裏面的變量可以被外部使用。

3、詞法作用域:靜態作用域,查找作用域的順序是按照函數定義時的位置來決定的。

4、全局作用域是在V8啓動過程中就創建了,且一直保存在內存中不會被銷燬的,直到V8退出。而函數作用域是在執行該函數時創建的,當函數執行結束之後,函數作用域就隨之被銷燬了。

我們來看一個閉包的案例:

function foo() {
    let a = 2;
    function bar() {
        console.log(a);
        a++;
    }
    return bar;
}
let baz = foo();
baz(); // 2
baz(); // 3

函數foo(...)中返回了一個名為bar的函數,接着 let baz = foo();就將bar(...)這個函數賦值給了baz,所以在一定程度上baz相當於bar(...),接着我們就執行baz(在bar定義時的詞法作用域以外執行了)(詞法作用域: 函數在哪裏定義,他的詞法作用域就在哪裏);這也就印證了我們前面説的,將一個內部函數傳遞到這個函數所在的詞法作用域以外執行,所以就產生了閉包,因為我們在外面獲取到了foo函數內部的變量a,同時這也是閉包產生的效果。

3、總結:

  • 什麼時候產生閉包: 將一個函數傳遞到這個函數所在的詞法作用域以外執行;
  • 閉包的作用: 引用某個塊級作用域裏面變量(持續引用)

4、只要使用了回調函數,實際上就是在使用閉包。下面我們來看一個案例。

function wait(message) {
    setTimeout(function timer() {
      console.log(message);
    }, 1000);
}
wait('Hello, World');

將一個內部函數(名為timer)傳遞給setTimeout(...)。timer具有涵蓋wait(...)作用域的閉包,因此還保有對變量message的引用。wait(...)執行1000毫秒後,它的內部作用域並不會消失,timer函數依然保有wait(...)作用域的閉包。因為它可以引用到message這個變量。

5、下面我們來看一個稍微複雜點的案例(利用回調函數進行同步傳值):

// 模擬獲取ajax請求的數據(異步)
function getAjaxData(cb) {
    // 用setTimeout實現異步請求
    setTimeout(function() {
      // 假設data是我們請求得到的數據 我們需要將數據發送給別人
      const data = "請求得到的數據";
      cb(data);
    }, 1000)
}
// 獲取ajax請求的響應數據並對數據進行處理
getAjaxData(function handleData(tempData) {
    tempData = tempData + '666';
    console.log(tempData); // 請求得到的數據666
});

handleData(...)作為參數傳進getAjaxData(...)中,因此cb(data)中的data就作為參數傳進了handleData(...)中,這樣也就達到了傳值的作用了。

6、回調函數存在的問題:信任問題。以上面的例子進行改進。

function getAjaxData (cb) {
  // 用setTimeout實現異步請求
  setTimeout(function () {
    // 假設data是我們請求得到的數據 我們需要將數據發送給別人
    const data = "請求得到的數據";
    cb(data);
    cb(data);
  }, 1000)
}
// 獲取ajax請求的響應數據並對數據進行處理
getAjaxData(function handleData (tempData) {
  tempData = tempData + '666';
  console.log(tempData); // 請求得到的數據666
});

假設getAjaxData(...)這個方法是由第三方庫引進來的,我們並不清楚裏面的代碼邏輯細節,這樣的話handleData(...)的執行就存在不確定性,比如上面我增加了一個cb(data),這handleData(...)就會執行兩次,當然這不是我們想要的效果,因此回調的處理就不可控了。

回調最大的問題是控制反轉,它會導致信任鏈的完全斷裂。
因為回調函數內部的調用情況是不確定的,可能不調用,也可能被調用了多次。
---《你不知道的JavaScript》

Promise的出現正是為了解決這個問題。

7、備註: 關於js中內存回收的問題

三、Promise

Promise一旦決議(resolve),一直保持其決議結果不變
解決回調調用過早的問題
解決回調調用過晚的問題
解決回調未調用的問題
解決調用次數過少或過多

Promise API概述

1、new Promise(...)構造器

2、Promise.resolve(...)和Promise.reject(...)

3、then(...)和catch(...)

4、Promise.all([...])和Promise.race([...])

Promise源碼解讀

1、new Promise 時,需要傳遞一個executor執行器,執行器立刻執行;

2、executor 接受兩個參數,分別是resolve和reject;

3、promise 只能從 pending 到 rejected,或者從 pending 到 fulfilled;

4、promise 的狀態一旦確認,就不會再改變;

5、promise都有then方法,then接受兩個參數,分別是promise成功的回調 onFulfilled,和promise失敗的回調onRejected;

6、如果調用then時,promise已經成功,則執行onFulfilled,並將promise的值作為參數傳遞進去。如果promise已經失敗,那麼執行onRejected,並將promise失敗的原因作為參數傳遞進去。如果promise的狀態是pending,需要將onFulfilled和onRejected函數存放起來,等待狀態確定後,再依次將對應的函數執行;

7、then的參數onFulfilled和onRejected可以缺省;

8、promise可以then多次,promise的then方法返回一個promise;

9、如果then返回的是一個結果,那麼就會把這個結果作為參數,傳遞給下一個then的成功的回調(onFulfilled);

10、如果then中拋出異常,那麼就會把這個異常作為參數,傳遞給下一個then的失敗的回調(onRejected);

11、如果then返回的是一個promise,那麼就會等這個promise執行完,promise如果成功,就走下一個then的成功,如果失敗,就走下一個then的失敗;

流程

1、當promise中有異步任務:如果promise中有異步任務的話,那他的status為pedding,之後所有的then都會放到待辦任務數組裏面。
2、鏈式調用的實現:then方法返回一個promise。
3、當then中有異步任務:兩種情況:

  • 僅僅是一個異步任務:按照正常情況來,會再最後面輸出。
  • 異步任務在一個promise中,並且該promise有return:後面的所有then會放進待辦任務數組中,這種情況會等到return的promise resolve(...)之後,將狀態改變之後才會再執行後面的(遍歷待辦任務數組),也就是相當於將then(...)中的異步任務阻塞了。

源碼實現&解析

1、實現promise(...)方法,該方法有一個回調函數exectue參數 ;

當我們new promise((resolve, reject) => {});時,將會執行該回調函數 ;

並且resolve對應到promise(...)中的res(...)

reject對應到promise(...)中的rej(...)

當我們執行到resolve(...)時,才會執行res(...)

function promise(exectue) {
  const res = (value) => { }
  const rej = (reason) => { }
  exectue(res, rej);
}

// resolve對應的是promise(...)中的res(...)
// reject對應的是promise(...)中的rej(...)
// 因此只有執行了resolve(...)或reject(...) 才改變status的值
const test = new promise((resolve, reject) => {
  console.log('這裏是調試1');
  setTimeout(() => {
    console.log('這裏是調試2');
    resolve();
  }, 3000)
})

// 這裏是調試1
// 這裏是調試2
// 執行了res

2、promise(...)方法中維護幾個變量,用於存儲執行節點的狀態&數據:

  • fulfilled:標誌任務是否已完成狀態;
  • pedding:標誌任務是否正在進行中狀態(未完成狀態);
  • status:標誌當前執行節點的狀態,節點的初始狀態為pedding
  • value:存儲promise狀態成功時的值;
  • reason:存儲promise狀態失敗時的值;
  • onFulfilledCallbacks :數組,存儲成功的回調任務;
  • onRejectedCallbacks:數組,存儲失敗的回調任務;
function promise(exectue) {
  this.fulfilled = 'fulfilled';
  this.pedding = 'pedding';
  this.rejected = 'rejected';
  this.status = this.pedding;
  this.value;
  this.reason;
  this.onFulfilledCallbacks = [];
  this.onRejectedCallbacks = [];
  const res = (value) => {
    if(this.status === this.pedding) {
      // 執行了resolve就要改變status為fulfilled
      this.status = this.fulfilled;
      this.value = value;
      // 遍歷執行放在待辦任務數組中的事件
      this.onFulfilledCallbacks.forEach(fn => fn());
    }
  }
  const rej = (reason) => {
    if(this.status === this.pedding) {
      // 執行了reject就要改變status為rejected
      this.status = this.rejected;
      this.reason = reason;
      this.onRejectedCallbacks.forEach(fn => fn());
    }
  }
  exectue(res, rej);
}

3、實現then(...),為符合鏈式調用,then(...)方法必須返回一個promise(...)

promise.prototype.then = function (onFulfilled, onRejected) {
  // this指向promise(...)對象
  const that = this;
  const promise2 = new promise((resolve, reject) => {
    if(that.status === that.fulfilled) {
      onFulfilled();
      resolve();
    }
    if(that.status === that.pedding) { }
    if(that.status === that.rejected) { }
  })
  return promise2;
}
// 調用
const test = new promise((resolve, reject) => {
  console.log('這裏是調試1');
  resolve();
})
test.then(() => {
  console.log('這裏是調試2');
}).then(() => {
  console.log('這裏是調試3');
})

// 這裏是調試1
// 這裏是調試2
// 這裏是調試3

4、到這裏,我們已實現了基本的鏈式調用的功能了,那如果在promise(...)中是一個異步事件時,並且我們需要阻塞這個異步任務(將異步轉化為同步),預計的效果是隻有執行了resolve(...)才能執行then(...)中的代碼,可以怎麼實現呢?

promise(...)中是需要阻塞的異步任務時,那麼當執行到then(...)時,此時的statuspedding,需要將鏈式調用的執行節點根據FulfilledRejected兩種情況分別添加到onFulfilledCallbacksonRejectedCallbacks這兩個變量中,等到異步任務執行完,resolve(...)之後才輪到鏈式調用節點的執行,改進代碼:

promise.prototype.then = function (onFulfilled, onRejected) {
  // this指向promise(...)對象
  const that = this;
  const promise2 = new promise((resolve, reject) => {
    if(that.status === that.fulfilled) {
      onFulfilled();
      resolve();
    }
    if(that.status === that.pedding) {
      console.log('這裏是調試4');
      this.onFulfilledCallbacks.push(() => {
        onFulfilled();
        resolve();
      })
    }
    if(that.status === that.rejected) {
      this.onRejectedCallbacks.push(() => {
        onRejected();
        reject();
      })
    }
  })
  return promise2;
}

// 調用
const test = new promise((resolve, reject) => {
  setTimeout(() => {
    console.log('這裏是調試1');
    resolve();
  }, 1000)
})
test.then(() => {
  console.log('這裏是調試2');
}).then(() => {
  console.log('這裏是調試3');
})

// 這裏是調試4
// 這裏是調試4
// 這裏是調試1
// 這裏是調試2
// 這裏是調試3

5、接下來,這裏又有一個場景,當我們在then(...)方法中有異步任務,我們同樣想要讓該異步任務阻塞,又該怎麼弄呢?按照我們上面的邏輯是阻塞不了then(...)方法中的異步任務的,大家可以嘗試一下。

const test = new promise((resolve, reject) => {
  setTimeout(() => {
    console.log('這裏是調試1');
    resolve();
  }, 1000)
})
test.then(() => {
  setTimeout(() => {
    console.log('這裏是調試2');
  }, 2000)
}).then(() => {
  console.log('這裏是調試3');
})

// 輸出
// 這裏是調試4
// 這裏是調試4
// 這裏是調試1
// 這裏是調試3
// 這裏是調試2

要想阻塞then(...)中的異步任務,這裏得分成兩種情況來討論:一種是要阻塞異步任務,需要在then(...)返回一個promise(...),另一種是不需要阻塞異步任務,那就不需要返回任何值。繼續改進代碼:

promise.prototype.then = function (onFulfilled, onRejected) {
  // this指向promise(...)對象
  const that = this;
  const promise2 = new promise((resolve, reject) => {
    if(that.status === that.fulfilled) {
        let x = onFulfilled(that.value); 
        if(x && typeof x === 'object' || typeof x === 'function') {
          let then = x.then;
          if(then) {
            then.call(x, resolve, reject);
          } else {
            resolve(x);
          }
    }
    if(that.status === that.pedding) {
      this.onFulfilledCallbacks.push(() => {
        // onFulfilled(...) 為then(...)方法中的回調函數
        let x = onFulfilled(that.value); // 執行了then(...)方法中的回調函數並將返回值賦給x
        // 判斷then(...)中回調函數返回值類型 是否為function
        if(x && typeof x === 'object' || typeof x === 'function') {
          let then = x.then;
          // 如果then存在 説明返回了一個promise
          if(then) {
            // 這裏採用的方法是將resolve放在返回的promise的then裏面執行 .
            // 這樣只有當then(...)方法中的異步事件執行完才resolve(...) 很牛皮
            // call(...)綁定this的指向
            then.call(x, resolve, reject);
          }
        } else {
          resolve(x);
        }
      })
    }
    if(that.status === that.rejected) {
      this.onRejectedCallbacks.push(() => {  
        let x = onRejected(that.reason);
        if(x && typeof x === 'object' || typeof x === 'function') {
          let then = x.then;
          if(then) {
            then.call(x, resolve, reject);
          } else {
            resolve(x);
          }
      })
    }
  })
  return promise2;
}


const test = new promise((resolve, reject) => {
  setTimeout(() => {
    console.log('這裏是調試1');
    resolve();
  }, 1000)
})
test.then(() => {
  // 若要阻塞then(...)中的異步任務需要return一個promise
  return new promise((resolve1, reject1) => {
    setTimeout(() => {
      console.log('這裏是調試2');
      resolve1();
    }, 2000)
  })
}).then(() => {
  console.log('這裏是調試3');
})

// 這裏是調試1
// 這裏是調試2
// 這裏是調試3

6、到這裏,關於promise核心的內容基本已經實現了,再看一下以下案例:

new promise(function(resolve){
    console.log('這裏是調試1');
    resolve();
}).then(function(){
    console.log('這裏是調試2');
});
console.log('這裏是調試3');

// 這裏是調試1
// 這裏是調試2
// 這裏是調試3
new Promise(function(resolve){
    console.log('這裏是調試1');
    resolve();
}).then(function(){
    console.log('這裏是調試2');
});
console.log('這裏是調試3');

// 這裏是調試1
// 這裏是調試3
// 這裏是調試2

可以發現,我們的promise和原生的Promise輸出結果不一樣。根據Promise規範,在.then(...)方法中執行的程序屬於微任務(異步任務分為微任務宏任務,一般的異步事件屬於宏任務,微任務比宏任務先執行,但我們自己實現的promise並不支持這個規則,原生的Promise才支持),因此需要將其轉化為異步任務,下面我們用setTimeout(...)來模擬實現一下異步任務(原生Promise 並不是用setTimeout(...)實現的)

promise.prototype.then = function (onFulfilled, onRejected) {
  const that = this;
  const promise2 = new promise((resolve, reject) => {
    if (that.status === that.fulfilled) {
      setTimeout(() => {
        let x = onFulfilled(that.value);
        if (x && typeof x === 'object' || typeof x === 'function') {
          let then = x.then;
          if (then) {
            then.call(x, resolve, reject);
          }
        } else {
          resolve(x);
        }
      })
    }
    if (that.status === that.pedding) {
      that.onFulfilledCallbacks.push(() => {
        setTimeout(() => {
          let x = onFulfilled(that.value);
          if (x && typeof x === 'object' || typeof x === 'function') {
            let then = x.then;
            if (then) {
              then.call(x, resolve, reject);
            }
          } else {
            resolve(x);
          }
        })
      })
      that.onRejectedCallbacks.push(() => {
        setTimeout(() => {
          let x = onRejected(that.reason);
          if (x && typeof x === 'object' || typeof x === 'function') {
            let then = x.then;
            if (then) {
              then.call(x, resolve, reject);
            }
          } else {
            resolve(x);
          }
        })
      })
    }
    if (that.status === that.rejected) {
      setTimeout(() => {
        let x = onRejected(that.reason);
        if (x && typeof x === 'object' || typeof x === 'function') {
          let then = x.then;
          if (then) {
            then.call(x, resolve, reject);
          }
        } else {
          resolve(x);
        }
      })
    }
  })
  return promise2;
}

// 這裏是調試1
// 這裏是調試3
// 這裏是調試2

7、再來看一個案例:

const test = new promise((res, rej) => {
  setTimeout(() => {
    console.log('這裏是調試1');
    res();
  }, 4000);
})
// 回調函數push進test.onFulfilledCallbacks數組中
test.then(() => {
  return new promise((res1) => {
    setTimeout(() => {
      console.log('這裏是調試2');
      res1();
    }, 3000);
  })
})
// 回調函數也是push進test.onFulfilledCallbacks數組中,因此兩個then(...)是相互獨立的,執行順序按照常規的來處理
test.then(() => {
  console.log('這裏是調試3');
})

// 輸出
// 這裏是調試1
// 這裏是調試3
// 這裏是調試2

源碼實現參考
宏任務微任務參考

四、async與await

  • async、await相對於promise多了等待的效果
  • 原理: Promise + 生成器 = async/await;
  • async函數一定會返回一個Promise對象。如果一個async函數的返回值看起來不是promise,那麼它將會被隱式得包裝在一個promise中;
  • 重要: 使用async與await時,await後面的函數必須返回決議的Promise,不然,程序將停止在await這一步;
  • async、await相對與promise多了等待的效果;具體是怎麼實現的?--- 使用了yield暫停函數,只有等前面的promise決議之後才執行next(...),程序才繼續往後面執行,表現出來的就是暫停程序的效果;

迭代器

1、一種特殊對象,用於為迭代過程設計的專有接口,簡單來説就類似與遍歷一個對象;

2、調用返回一個結果對象,對象中有next()方法;

3、next()方法返回一個對象: value: 表示下一個要返回的值;done:沒有值可以返回時為true,否則為false;

4、迭代器簡易內部實現:

function createIterator(items) {
  let i = 0;
  return {
    next: function() {
      let done = (i >= items.length);
      // items[i++] 相當於 items[i]; i++;
      let value = !done ? items[i++] : undefined;
      return {
        value: value,
        done: done // true表示後面沒有值了
      };
    }
  };
}
// 只創建了一個實例 因此iterator一直是同一個
let iterator = createIterator([1,2,3]);
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

生成器

1、一個返回迭代器的函數;實現了一種順序、看似同步的異步流程控制表達風格;
2、關鍵字yield:表示函數暫停運行;
3、過程:每當執行完一條yield語句後函數就暫停了,直到再次調用函數的next()方法才會繼續執行後面的語句;
4、使用yield關鍵字可以返回任何值或表達式( 表示要傳遞給下一個過程的值);
5、注意點:

  • yield關鍵字只能在生成器函數的直接內部使用(在生成器函數內部的函數中使用會報錯);
  • 不能用箭頭函數來創建生成器;

簡易使用示例:

function *createInterator() {
  yield 1;
  yield 2;
  yield 3;
}
// 生成器的調用方式與普通函數相同,只不過返回一個迭代器
let iterator = createInterator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

迭代消息傳遞:

function *foo(x) {
  const y = x * (yield);
  return y;
}
const it = foo(6);
// 啓動foo(...)
it.next(); 
const res = it.next(7); // next(...)的數量總是比yield的多一個
console.log(res); // 42

首先,傳入6作為參數。然後調用it.next(...),這會啓動*foo(...)

接着,在*foo(...)內部,開始執行語句const y = x ...,但隨後就遇到了一個yield表達式。它就會在這一點上暫停*foo(...)(在賦值語句中間),並非在本質上要求調用代碼為yield表達式提供一個結果值。接下來,調用it.next(7),這一句把值7傳回作為被暫停的yield表達式結果。

所以,這是賦值語句實際上就是const y = 6 * 7。現在,return y返回值42作為調用it.next(7)的結果。

注意,根據你的視角不同,yieldnext(...)調用有一個不匹配。一般來説,需要的next(...)調用要比yield語句多一個,前面的代碼片段就有一個yield和兩個next(...)調用。

因為第一個next(...)總是用來啓動生成器,並運行到第一個yield處。不過,是第二個next(...)調用完成第一個被暫停的yield表達式,第三個next(...)完成第二個yield,以此類推。

消息是雙向傳遞的:

yield(...)作為一個表達式,可以發出消息響應next(...)

next(...)也可以向暫停的yield表達式發送值。注意:前面我們説到,第一個next(...)總是用來啓動生成器的,此時沒有暫停的yield來接受這樣一個值,規範和瀏覽器都會默認丟棄傳遞給第一個next(...)的任何東西。因此,啓動生成器一定要用不帶參數的next(...);

function *foo(x) {
  const y = x * (yield "Hello");
  return y;
}
const it = foo(6);
let res = it.next(); // 第一個next(),並不傳入任何東西
console.log(res.value); // Hello
res = it.next(7); // 向等待的yield傳入7
console.log(res.value); // 42

源碼實現&解析

async&await是基於生成器+Promise封裝的語法糖,使用async修飾的function實際上會被轉化為一個生成器函數,並返回一個決議的Promise(...)await對應到的是yield,因此await只能在被async修飾的方法中使用

下面看一個例子:這是一個常見的使用async/await的案例

// 模擬多個異步情況
function foo1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(100); // 如果沒有決議(resolve)就不會運行下一個
    }, 3000)
  })
}
function foo2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(200);
    }, 2500)
  })
}
async function test() {
  const f1 = await foo1();
  const f2 = await foo2();
  console.log('這裏是調試1');
  console.log(f1);
  console.log(f2);
  return 'haha';
}
test().then((data) => {
  console.log('這裏是調試2');
  console.log(data);
});
// 這裏是調試1
// 100
// 200
// 這裏是調試2
// haha

接着我們來看一下如何將這個案例中的async/await生成器+Promise來實現:

test(...)轉化為生成器函數:

function *test() {
  const f1 = yield foo1();
  const f2 = yield foo2();
  console.log('這裏是調試1');
  console.log(f1);
  console.log(f2);
  return 'haha';
}

實現方法run(...),用於調用生成器*test(...)

function run(generatorFunc) {
  return function() {
    const gen = generatorFunc();
    // async 默認返回Promise
    return new Promise((resolve, reject) => {
      function step(key, arg) {
        let generatorResult;
        try {
          // 相當於執行gen.next(...)
          generatorResult = gen[key](arg);
        } catch (error) {
          return reject(error);
        }
        // gen.next() 得到的結果是一個 { value, done } 的結構
        const { value, done } = generatorResult;
        if (done) {
          return resolve(value);
        } else {
          return Promise.resolve(value).then(val => step('next', val), err => step('throw', err));
        }
      }
      step("next");
    })
  }
}

const haha = run(test);
haha().then((data) => {
  console.log('這裏是調試2');
  console.log(data);
});

// 這裏是調試1
// 100
// 200
// 這裏是調試2
// haha

async/await 參考

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.