整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          Async是如何被 JavaScript 實現的

          Async是如何被 JavaScript 實現的

          無論是面試過程還是日常業務開發,相信大多數前端開發者可以熟練使用 Async/Await 作為異步任務的終極處理方案。

          但是對于 Async 函數的具體實現過程只是知其然不知所以然,僅僅了解它是基于 Promise 和 Generator 生成器函數的語法糖。

          提及 JavaScript 中 Async 函數的內部實現原理,大多數開發者并不清楚這一過程。甚至從來沒有思考過 Async 所謂語法糖是如何被 JavaScript 組合而來的。

          別擔心,文中會帶你分析 Async 語法在低版本瀏覽器下的 polyfill 實現,同時我也會手把手帶你基于 Promise 和 Generator 來實現所謂的 Async 語法。

          我們會從以下方面來逐步攻克 Async 函數背后的實現原理:

          • Promise 章節梳理,從入門到源碼帶你掌握 Promise 應用。
          • 什么是生成器函數?Generator 生成器函數基本特征梳理。
          • Generator 是如何被實現的,Babel 如何在低版本瀏覽器下實現 Generator 生成器函數。
          • 作為通用異步解決方案的 Generator 生成器函數是如何解決異步方案。
          • 開源 Co 庫的基本原理實現。
          • Async/Await 函數為什么會被稱為語法糖,它究竟是如何被實現的。

          相信讀完文章的你,對于 Async/Await 真正可以做到“知其然,知其所以然”。

          Promise

          所謂 Async/Await 語法我們提到本質上它是基于Promise 和 Generator 生成器函數的語法糖

          關于 Promise 這篇文章中我就不過于展開他的基礎和原理部分了,網絡中對于介紹 Promise 相關的文章目前已經非常優秀了。如果有興趣深入 Promise 的同學可以查看:

          • JavaScript Promise MDN Docs:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise

          Promise 基礎使用準則,MDN 上給出了詳盡的說明和實例,強烈建議對于 Promise 陌生的同學可以查閱 MDN 鞏固 Promise 基礎知識。

          • Promise A+ 規范:https://promisesaplus.com/

          Promise A+ 實現準則,不同瀏覽器/環境下對于 Promise 都有自己的實現,它們都會依照同一規范標準去實現 Promise 。

          我在 ?? 這個地址:https://github.com/19Qingfeng/notes/blob/master/promise/core/index.js 按照規范實現過一版完整的 Promise ,有興趣的通許可以自行查閱代碼進行 Promise 原理鞏固。

          • V8 Promise源碼全面解讀:https://juejin.cn/post/7055202073511460895

          關于 Promise 中各種邊界應用以及深層次 Promise 原理實現,筆者強烈建議有興趣更深層次的同學結合月夕的這篇文章去參照閱讀。

          生成器函數

          關于 Generator 生成器函數與 Iterator 迭代器,大多數開發者在日常應用中可能并不如 Promise 那么常見。

          所以針對于 Generator 我會稍微和大家從基礎開始講起。

          Generator 概念

          Generator 基礎入門

          所謂 Generator 函數它是協程在 ES6 的實現,最大特點就是可以交出函數的執行權(即擁有暫停函數執行的效果)。

          function* gen() {
            yield 1;
            yield 2;
            yield 3;
          }
          
          let g=gen();
          
          g.next(); // { value: 1, done: false }
          g.next(); // { value: 2, done: false }
          g.next(); // { value: 3, done: false }
          g.next(); // { value: undefined, done: true }
          g.next(); // { value: undefined, done: true }
          復制代碼
          

          上述的函數就是一個 Generator 生成器函數的例子,我們通過在函數聲明后添加一個 * 的語法創建一個名為 gen 的生成器函數。

          調用創建的生成器函數會返回一個 Generator { } 生成器實例對象。

          初次接觸生成器函數的同學,看到上面的例子可能稍微會有點懵。什么是 Generator 實例對象,函數中的 yield 關鍵字又是做什么的,我們應該如何使用它呢?

          別著急,接下來我們來一步一揭開這些迷惑。

          所謂返回的 g 生成器對象你可以簡單的將它理解成為類似這樣的一個對象結構:

          {
              next: function () {
                  return {
                      done:Boolean, // done表示生成器函數是否執行完畢 它是一個布爾值
                      value: VALUE, // value表示生成器函數本次調用返回的值
                  }
              }
          }
          復制代碼
          
          • 首先,我們通過 let g=gen() 調用生成器函數創建了一個生成器對象 g ,此時 g 擁有 next 上述結構的 next 方法。

          這一步,我們成為 g 為返回的生成器對象, gen 為生成器函數。通過調用生成器函數 gen 返回了生成器對象 g 。

          • 之后,生成器對象中的 next 方法每次調用會返回一次 { value:VALUE, done:boolean }的對象。

          每次調用生成器對象的 next 方法會返回一個上述類型的 object:

          其中 done 表示生成器函數是否執行完畢,而 value 表示生成器函數中本次 yield 對應的值。

          部分沒有接觸過的同學可能不太了解這一過程,我們來詳細拆開上述函數的執行過程來看看:

          • 首先調用 gen() 生成器函數返回 g 生成器對象。
          • 其次返回的 g 生成器對象中擁有一個 next 的方法。
          • 每當我們調用 g.next() 方法時,生成器函數緊跟著上一次進行執行,直到函數碰到 yield 關鍵值。
            • yield 關鍵字會停止函數執行并將 yield 后的值返回作為本次調用 next 函數的 value 進行返回。
            • 同時,如果本次調用 g.next() 導致生成器函數執行完畢,那么此時 done 會變成 true 表示該函數執行完畢,反之則為 false 。

          比如當我們調用 let g=gen() 時,會返回一個生成器函數,它擁有一個 next 方法。

          之后當第一次調用 g.next() 方法時,會執行生成器函數 gen 。函數會進行執行,直到碰到 yield 關鍵字會進行暫停,此時函數會暫停到 yield 1 語句執行完畢,將 1 賦給 value

          同時因為生成器函數 gen 并沒有執行完畢,所以此時 done 應該為 false 。所以此時首次調用 g.next() 函數返回的應該是 { value: 1, done: false }

          之后,我們第二次調用 g.next() 方法時,函數會從上一次的中斷結果后進行執行。也就是會繼續 yield 2 語句。

          當遇到 yield 2 時,又因為碰到了 yield 語句。此時函數又會被中斷,因為此時函數并沒有執行完成,并且yield 語句后緊挨著的是 2 所以第二個 g.next() 會返回 { value: 2 , done: false }

          同樣,yield 3; 回和前兩次執行邏輯相同。

          需要額外注意的是,當我們第四次調用迭代器 g.next() 時,因為第三次 g.next() 結束時生成器函數已經執行完畢了。所以再次調用 g.next() 時,由于函數結束 done 會變為 false 。同時因為函數不存在返回值,所以 value 為 undefined。

          上邊是一個基于 Generator 函數的簡單執行過程,其實它的本質非常簡單:

          調用生成器函數會返回一個生成器對象,每次調用生成器對象的 next 方法會執行函數到下一次 yield 關鍵字停止執行,并且返回一個 { value: Value, done: boolean }的對象。

          上述執行過程,我稍稍用了一些篇幅來描述這一簡單的過程。如果看到這里你還是沒有上面的 Demo 含義,那么此時請你停下往下的進度,會到開頭一定要搞清楚這個簡單的 Demo 。

          Generator 函數返回值

          在掌握了基礎 Generator 函數和 yield 關鍵字后,趁熱打鐵讓我們來一舉攻克 Generator 生成器函數的進階語法。

          老樣子,我們先來看這樣一段代碼:

          function* gen() {
            const a=yield 1;
            console.log(a,'this is a')
            const b=yield 2;
            console.log(b,'this is b')
            const c=yield 3;
            console.log(c,'this is c')
          }
          
          let g=gen();
          
          g.next(); // { value: 1, done: false }
          g.next('param-a'); // { value: 2, done: false }
          g.next('param-b'); // { value: 3, done: false }
          g.next('param-c'); // { value: undefined, done: true }
          
          // 控制臺會打印:
          // param-a this is a
          // param-b this is b
          // param-c this is c
          復制代碼
          

          這里,我們著重來看看調用生成器對象的 next 方法傳入參數時究竟會發生什么事情,理解 next() 方法的參數是后續 Generator 解決異步的重點實現思路。

          上文我們提到過,生成器函數中的 yield 關鍵字會暫停函數的運行,簡單來說比如我們第一次調用 g.next() 方法時函數會執行到 yield 1 語句,此時函數會被暫停。

          當第二次調用 g.next() 方法時,生成器函數會繼續從上一次暫停的語句開始執行。這里有一個需要注意的點:當生成器函數恢復執行時,因為上一次執行到 const a=yield 1 語句的右半段并沒有給 const a進行賦值。

          那么此時的賦值語句 const a=yield 1,a 會被賦為什么值呢?細心的同學可能已經發現了。我們在 g.next('param-a') 傳入的參數 param-a 會作為生成器函數重新執行時,上一次 yield 語句的返回值進行執行。

          簡單來說,也就是調用 g.next('param-a')恢復函數執行時,相當于將生成器函數中的 const a=yield 1; 變成 const a='param-a'; 進行執行。

          這樣,第二次調用 g.next('param-a')時自然就打印出了 param-a this is a

          同樣當我們第三次調用 g.next('param-b') 時,本次調用 next 函數傳入的參數會被當作 yield 2 運算結果賦值給 b 變量,執行到打印時會輸出 param-b this is b

          同理 g.next('paramc') 會輸出 param-c this is b

          總而言之,當我們為 next 傳遞值進行調用時,傳入的值會被當作上一次生成器函數暫停時 yield 關鍵字的返回值處理。

          自然,第一次調用 g.next() 傳入參數是毫無意義的。因為首次調用 next 函數時,生成器函數并沒有任何執行自然也沒有 yield 關鍵字處理。

          接下來我們來看看所謂的生成器函數返回值:

          function* gen() {
            const a=yield 1;
            console.log(a, 'this is a');
            const b=yield 2;
            console.log(b, 'this is b');
            const c=yield 3;
            console.log(c, 'this is c');
            return 'resultValue'
          }
          
          let g=gen();
          
          g.next(); // { value: 1, done: false }
          g.next('param-a'); // { value: 2, done: false }
          g.next('param-b') // { value: 3, done: false }
          g.next() // { value: 'resultValue', done: true }
          g.next() // { value: undefined, done: true }
          復制代碼
          

          當生成器函數存在 return 返回值時,我們會在第四次調用 g.next() 函數恢復執行,此時生成器函數繼續執行函數執行完畢。

          此時自然 done 會變為 true 表示生成器函數已經執行完畢,之后,由于函數存在返回值所以隨之本次的 value 會變為 'resultValue' 。

          也就是當生成器函數執行完畢時,原本本次調用 next 方法返回的 {done:true,value:undefined} 變為了{ done:true,value:'resultValue'}

          關于 Generator 函數的基本使用我們就介紹到這里,接下來我們來看看它是如何被 JavaScript 實現的。

          Generator 原理實現

          關于 Generator 函數的原理其實和我們后續的異步關系并不是很大,但是本著“知其然,知其所以然”的出發點。

          希望大家可以耐心去閱讀本小結,其實它的內部運行機制并不是很復雜。筆者自己也在某電商大廠面試中被問到過如何實現 Generator 的 polyfill。

          首先,你可以打開鏈接查看我已經編輯好的 ?? Babel Generator Demo[6]

          image.png

          乍一看也許很多同學會稍微有點懵,沒關系。這段代碼并不難,難的是你對未知恐懼的心態。

          這是 Babel 在低版本瀏覽器下為我們實現的 Generator 生成器函數的 polyfill[7] 實現。

          左側為 ES6 中的生成器語法,右側為轉譯后的兼容低版本瀏覽器的實現。

          首先左側的 gen 生成器函數被在右側被轉化成為了一個簡單的普通函數,具體 gen 函數內容我們先忽略它。

          在右側代碼中,對于普通 gen 函數包裹了一層 regeneratorRuntime.mark(gen) 處理,在源碼中這一步其實為了將普通 gen 函數繼承 GeneratorFunctionPrototype 從而實現將 gen() 返回的對象變成 Generator 實例對象的處理。

          這一步對于我們理解 Generator 顯得并不是那么重要,所以我們可以簡單的將 regeneratorRuntime.mark 改寫成為這樣的結構:

          // 自己定義regeneratorRuntime對象
          const regeneratorRuntime={
              // 存在mark方法,接受傳入的fn。原封不懂的返回fn
              mark(fn) {
                  return fn
              }
          }
          復制代碼
          

          我們自己定義了 regeneratorRuntime 對象,并且為他定義了一個 mark 方法。它的作用非常簡單,接受一個函數作為入參并且返回函數,僅此而已。

          之后我們再來進入 gen 函數內部,在左側源代碼中當我們調用 gen() 時,是會返回一個 Iterator 對象(它擁有 next 方法,并且每次調用 next 都會返回 {value:VALUE,done:boolean})。

          所以在右側我們了解到了,我們通過調用編譯后的普通 gen() 函數應該和右側返回一致,所謂 regeneratorRuntime.wrap() 方法也應該返回一個具有 next 屬性的迭代器對象。

          關于 regeneratorRuntime.wrap() 方法,這里傳入了兩個參數,第一個參數是一個接受傳入 context 的函數,第二個參數是我們之前處理的 _marked 對象。

          同樣關于 wrap() 方法的第二個參數我們不用過多糾結,它仍然是對于編譯后的生成器函數作為繼承使用的一個參數,并不影響函數的核心邏輯。所以我們暫時忽略它。

          此時,我們了解到,regeneratorRuntime 對象上應該存在一個 wrap 方法,并且 wrap 方法應該返回一個滿足迭代器協議[8]的對象。

          // 自己定義regeneratorRuntime對象
          const regeneratorRuntime={
              // 存在mark方法,接受傳入的fn。原封不懂的返回fn
              mark(fn) {
                  return fn
            },
            wrap(fn) {
              // ...
              return {
                next() {
                    done: ...,
                    value: ...
                }
              }
            }
          }
          復制代碼
          

          讓我們進入 regeneratorRuntime.wrap 內部傳入的具體函數來看看:

          function gen() {
            var a, b, c;
            return regeneratorRuntime.wrap(function gen$(_context) {
              // while(1) 配合函數的return沒有任何實際意義
              // 通常在編程中使用 while(1) 來表示while中的內部會被多次執行
              while (1) {
                switch ((_context.prev=_context.next)) {
                  case 0:
                    _context.next=2;
                    return 1;
          
                  case 2:
                    a=_context.sent;
                    console.log(a, 'this is a');
                    _context.next=6;
                    return 2;
          
                  case 6:
                    b=_context.sent;
                    console.log(b, 'this is b');
                    _context.next=10;
                    return 3;
          
                  case 10:
                    c=_context.sent;
                    console.log(c, 'this is c');
          
                  case 12:
                  case 'end':
                    return _context.stop();
                }
              }
            }, _marked);
          }
          復制代碼
          

          regeneratorRuntime.wrap 內部傳入的函數,使用了 while(1) 內部邏輯,因為我們在 while 循環中配合了函數的 return 語句,所以這里 while(1) 其實并沒有任何含義。

          通常,在編程中我們用 while(1) 來表示內部的邏輯會被執行很多次,的確在函數內部的 while 循環每次調用 next 方法其實都會進入這段邏輯執行。

          首先我們來看看 傳入的 _context 參數存在哪些屬性:

          • _context.prev 表示本次生成器函數執行時的指針位置。
          • _context.next 表示下次生成器函數調用時的指針位置。
          • _context.sent 表示調用 g.next(params) 時,傳入的 params 參數。
          • _context.stop 表示當調用 g.next() 生成器函數執行完畢調用的方法。

          在解釋了 _context 對象上的屬性含義之后,也許你還是不太明白它們各自的含義。我們先來看看簡化后的實現代碼:

          const regeneratorRuntime={
            // 存在mark方法,接受傳入的fn。原封不懂的返回fn
            mark(fn) {
              return fn;
            },
            wrap(fn) {
              const _context={
                next: 0, // 表示下一次執行生成器函數狀態機switch中的下標
                sent: '', // 表示next調用時候傳入的值 作為上一次yield返回值
                done: false, // 是否完成
                // 完成函數
                stop() {
                  this.done=true;
                },
              };
              return {
                next(param) {
                  // 1. 修改上一次yield返回值為context.sent
                  _context.sent=param;
                  // 2.執行函數 獲得本次返回值
                  const value=fn(_context);
                  // 3. 返回
                  return {
                    done: _context.done,
                    value,
                  };
                },
              };
            },
          };
          復制代碼
          

          完整的 regeneratorRuntime 對象就像上邊實現的那樣,它看起來非常簡單對吧。

          在 wrap 函數中,我們接受傳入的一個狀態機函數。每次調用 wrap() 方法返回的 next(param) 方法時,會將 next(param) 中傳入的參數傳遞給 wrap 函數中的維護的 _context.sent 作為模擬上一次 yield 返回值而出現。

          同時在 wrap(fn) 中傳入的 fn 你可以將它看作一個小型狀態機 ,每次調用 next() 方法都會執行狀態機 fn 函數。

          不過,因為狀態機中每次執行時 _context.prev 的值都是不同的造成了每次調用 next 函數都執行的是狀態機中不同的邏輯。

          直到,狀態機函數 fn 中的 switch 語句匹配完成返回 _context.stop() 函數,此時將 _context.done 變為 true 并且返回對應對象。

          所謂 Generator 核心代碼簡化后不過上述短短幾十行,它的內部核心思想本質上就是通過 regeneratorRuntime.wrap 函數包裹一個狀態機函數 fn 。

          wrap 函數內部維護一個 _context 對象,從而每次調用返回的生成器對象的 next 方法時,被包裹的狀態機函數根據 _context 的對應屬性匹配對應狀態來完成不同的邏輯。

          Generator 核心原理其實就是這樣,有興趣了解完整代碼的同學可以查看 facebook/regenerator[9]

          Generator 異步解決方案

          在講述完 Generator 的基礎概念和 polyfill 原理之后,我們來步入異步瞧瞧它是如何被應用在異步編程中的。

          大多數情況下,我們會直接使用 Promise 來處理異步問題。Promise 幫助我們解決了非常糟糕的“回調地獄”式的異步解決方案。

          但是 Promise 中仍然需要存在不停的 .then .then 當異步嵌套過多的情況下,Promise 中的 then 式調用也顯得不是那么一種直觀。

          每當問題的產生一定會伴隨解決方案的出現,在 Promise 處理異步問題時,Generator 顯然作為解決方案出現在了我們的視野中。

          function promise1() {
            return new Promise((resolve)=> {
              setTimeout(()=> {
                resolve('1');
              }, 1000);
            });
          }
          
          function promise2(value) {
            return new Promise((resolve)=> {
              setTimeout(()=> {
                resolve('value:' + value);
              }, 1000);
            });
          }
          
          function* readFile() {
            const value=yield promise1();
            const result=yield promise2(value);
            return result;
          }
          復制代碼
          

          我們來看看上述的代碼,readFile 函數是不是稍微有一些 async 函數的影子。

          假如我期望所謂 readFile() 方法和 async 函數行為一致,返回的 result 同樣是一個 Promise 并且保持上訴代碼的寫法,我們應該如何做?

          看到這里,你可以稍微思考下應該如何利用 Generator 函數的特性去實現。


          上邊我們提到過生成器函數具有可暫停的特點,當調用生成器函數后會返回一個生成器對象。每次調用生成器對象的 next 方法,生成器函數才會繼續往下執行直到碰到下一個 yield 語句,同時每次調用生成器對象的 next(param) 方法時,我們可以傳入一個參數作為上一次 yield 語句的返回值。

          利用上述特性,我們可以寫出如下的代碼:

          function promise1() {
            return new Promise((resolve)=> {
              setTimeout(()=> {
                resolve('1');
              }, 1000);
            });
          }
          
          function promise2(value) {
            return new Promise((resolve)=> {
              setTimeout(()=> {
                resolve('value:' + value);
              }, 1000);
            });
          }
          
          function* readFile() {
            const value=yield promise1();
            const result=yield promise2(value);
            return result;
          }
          
          function asyncGen(fn) {
            const g=fn(); // 調用傳入的生成器函數 返回生成器對象
            // 期望返回一個Promise
            return new Promise((resolve)=> {
              // 首次調用 g.next() 方法,會執行生成器函數直到碰到第一個yield關鍵字
              // 這里會執行到 yield promise1() 同時將 promise1() 返回為迭代器對象的 value 值
              const { value, done }=g.next();
              // 因為value為Promise 所以可以等待promise完成后,在then函數中繼續調用 g.next(res) 恢復生成器函數繼續執行
              value.then((res)=> {
                // 同時第二次調用g.next() 時是在上一次返回的promise.then中
                // 我們可以拿到上一次Promise的value值也就是 '1'
                // 傳入g.next('1') 作為上一次yield的值 這里相當于 const value='1'
                const { value, done }=g.next(res);
                // 同理,繼續上述過程
                value.then(resolve);
              });
            });
          }
          
          asyncGen(readFile).then((res)=> console.log(res)); // value: 1
          復制代碼
          

          我們通過定義了一個 asyncGen 函數來包裹 readFile 生成器函數,利用了生成器函數結合 yield 關鍵字可以被暫停的特點,同時結合 Promise.prototype.then 方法的特性實現了類似于 async 函數的異步寫法。

          看上去它和 async 很像對吧,不過目前的代碼存在一個致命的問題:

          asyncGen 函數并不具備通用性,上邊的例子中 readFile 函數內部包裹了兩層 yield 處理的 promise,我們在 asyncGen 函數內部同樣兩次調用 g.next() 方法。

          如果我們包裹三層 yield 處理的 Promise ,那么我是不是重新書寫 asyncGen 函數邏輯。又或者 readFile 中存在比如 yield '1' 并不是 Promise 的語句,那么我們當作 Promise 使用 then 方法處理肯定是會報錯。

          這樣的方法不具備任何通用性,所以在實際項目中沒有人會這樣去組織異步代碼。但是通過這個例子我相信你已經可以初步了解 Generator 生成器函數是如何結合 Promise 來作為異步解決方案的。

          tj/co

          上邊我們使用 Generator 作為異步解決方案,我們編寫了一個名為 asyncGen 的包裹函數,可是它并不具備任何通用性。

          接下來我們來思考如何讓這個方法變得更加通用,從而在各種不同場景下去更好的解決異步問題:

          同樣,我希望我的 readFile 方法書寫時方式和之前一樣直觀:

          function promise1() {
            return new Promise((resolve)=> {
              setTimeout(()=> {
                resolve('1');
              }, 1000);
            });
          }
          
          function promise2(value) {
            return new Promise((resolve)=> {
              setTimeout(()=> {
                resolve('value:' + value);
              }, 1000);
            });
          }
          
          function* readFile() {
            const value=yield promise1();
            const result=yield promise2(value);
            return result;
          }
          復制代碼
          

          在這之前我們使用 Generator 的特性來處理 Promise 的異步問題,每次都傻傻的去根據 yeild 關鍵字去嵌套函數邏輯處理。

          一些同學可能之前就想到了,對于無窮無盡的嵌套調用邏輯同時又存在邊界停止條件。那么當我們需要封裝出一個具有通用性的函數時,使用遞歸來處理不是更好嗎?

          或許根據這個思路,你先可以嘗試自己封裝一下。


          不賣關子了,我們來看看具有通用性且更加優雅的 Generator 異步解決方案:

          function co(gen) {
            return new Promise((resolve, reject)=> {
              const g=gen();
              function next(param) {
                const { done, value }=g.next(param);
                if (!done) {
                  // 未完成 繼續遞歸
                  Promise.resolve(value).then((res)=> next(res));
                } else {
                  // 完成直接重置 Promise 狀態
                  resolve(value);
                }
              }
              next();
            });
          }
          
          co(readFile).then((res)=> console.log(res));
          復制代碼
          

          我們定義了一個 co 函數來包裹傳入的 generator 生成器函數。

          在函數 co 中,我們返回了一個 Promise 來作為包裹函數的返回值,同時首次調用 co 函數時會調用 gen() 得到對應的生成器對象。

          之后我們定義了一次 next 方法,在 next 函數內部只要迭代器未完成那么此時我們就會在 value 的 then 方法中在此遞歸調用該 next 函數。

          其實關于異步迭代時,大多數情況下都可以使用類似該函數中的遞歸方式來處理。

          函數中稍微有三點需要大家額外注意:

          • 首先我們可以看到 next 函數接受傳入的一個 param 的參數。

          這是因為我們使用 Generator 來處理異步問題時,通過 const a=yield promise 將 promise 的 resolve 值交給 a ,所以我們需要在每次 then 函數中將 res 傳遞給下一次的 next(res) 作為上次 yield 的返回值。

          • 其次,細心的同學可以留意到這一句代碼Promise.resolve(value).then((res)=> next(res));

          我們使用 Promise.resolve 將 value 進行了一層包裹,這是因為當生成器函數中的 yield 方法后緊挨的并不是 Promise 時,此時我們需要統一當作 Promise 來處理,因為我們需要統一調用 .then 方法。

          • 最后,首次調用 next() 方法時,我們并沒有傳入 param 參數。

          相信這個并不難理解,當我們不傳入 param 時相當于直接調用 g.next() ,上邊我們提到過當調用生成器對象的 next 方法傳入參數時,該參數會當作上一次 yield 語句的返回值來處理。

          因為首次調用 g.next() 時,生成器函數內部之前并不存在 yield ,所以傳入參數是沒有任何意義的。

          它看來并不是很難對吧,但是通過這一小段代碼讓我們的 Generator 擁有了可以讓異步代碼同步調用的書寫方式來使用。

          其實這一小段代碼也是所謂 co[10] 庫的核心原理,當然所謂 co 遠遠不止這些,但是這段代碼足夠我們了解所謂在 Async/Await 未出現之前我們是如何使用所謂的 Generator 來作為終極異步解決方案了。

          Async/Await

          鋪墊了這么久終于來到現階段 JavaScript 解決異步的最終方案了。

          在前邊我們聊到過所謂 Generator 的基礎用法以及 Babel 是如何在 EcmaScript 5 中使用 Generator 生成器。

          同時我們順帶聊了下,在 Async 沒有出現之前我們如何使用 Generator 結合 Promise 來處理異步問題。

          雖然之前的異步調用方式看起來已經非常類似于 async 語法了:

          function* readFile() {
            const value=yield promise1();
            const result=yield promise2(value);
            return result;
          }
          復制代碼
          

          但是在使用 readFile 時,我們仍然需要使用 co 函數來將 Generator 來單獨處理一層:

          co(readFile).then((res)=> console.log(res));
          復制代碼
          

          那么此時,async 的出現解決了這一問題,照舊,我們先來看看在不支持 async 語法的低版本瀏覽器下 Babel 是如何處理它的:

          image.png

          乍一看可能信息有點多,別擔心。其實這些都是我們之前分析甚至實現過的代碼。

          在深入這段代碼之前,我先告訴你所謂 Async 語法是如何被實現的結論:

          在這之前,我們通過 Generator 和 Promise 解決異步問題時,需要將 Generator 函數額外使用 co 來包裹一層從而實現類似同步的異步函數調用。

          那么如果我們想要實現低版本瀏覽器下的 Async 語法,那么我們將 co 函數伴隨 Generator 的 polyfill 一起編譯出來不就可以了嗎?

          所謂 Async 其實就是將 Generator 包裹了一層 co 函數,所以它被稱為 Generator 和 Promise 的語法糖。

          接下來我們一起來看看右邊的 polyfil 實現。

          image.png

          這段函數是不是很眼熟,我們在之前簡單聊過關于它的實現原理。

          唯一有一點不同的是,它將 Generator 的實現額外包裹了一層 _asyncToGenerator 函數進行返回。

          function _asyncToGenerator(fn) {
            return function () {
              var self=this,
                args=arguments;
              return new Promise(function (resolve, reject) {
                var gen=fn.apply(self, args);
                function _next(value) {
                  asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
                }
                function _throw(err) {
                  asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
                }
                _next(undefined);
              });
            };
          }
          復制代碼
          

          來看看所謂的 _asyncToGenerator 函數,它內部接受一個傳入的 fn 函數。這個 fn 正是所謂的 Generator 函數。

          當調用 hello() 時,實質上最終相當于調用 _asyncToGenerator 內部的返回函數,它會返回一個 Promise。

          return new Promise(function (resolve, reject) {
                var gen=fn.apply(self, args);
                function _next(value) {
                  asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
                }
                function _throw(err) {
                  asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
                }
                _next(undefined);
              });
          復制代碼
          

          首先 Promise 中會進行:

          var gen=fn.apply(self, args);
          復制代碼
          

          它會調用我們傳入的 fn(生成器函數) 返回生成器對象,之后它定義了兩個方法:

          function _next(value) {
                  asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
                }
          function _throw(err) {
                   asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
               }
          復制代碼
          

          這兩個方法內部都基于 asyncGeneratorStep 來進行函數調用:

          function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
            try {
              var info=gen[key](arg);
              var value=info.value;
            } catch (error) {
              reject(error);
              return;
            }
            if (info.done) {
              resolve(value);
            } else {
              Promise.resolve(value).then(_next, _throw);
            }
          }
          復制代碼
          

          所謂 asyncGeneratorStep 其實你完全可以將它當作我們之前實現的異步解決方案 co 的原理,它們內部的思想和實現是完全一致的,唯一的區別就是這里 Babel 編譯后的實現考慮了 error 情況而我們當時并沒有考慮出現錯誤時的情況。

          本質上還是利用 Generator 函數內部可以被暫停執行的特性結合 Promise.prototype.then 中進行遞歸調用從而實現 Async 的語法糖。

          其實看到這里,經過前邊知識點的鋪墊我相信最終 Async/Await 的實現原理對你來說一點都不會陌生。

          之所以被稱為語法糖因為本質上它也并沒有任何針對于 Generator 生成器函數和 Promise 的其他知識拓展點,恰恰是更好的結合了這兩種語法的特點衍生出了更加優雅簡單的異步解決方案。



          avaScript 中的 async/await 是 AsyncFunction 特性 中的關鍵字。目前為止,除了 IE 之外,常用瀏覽器和 Node (v7.6+) 都已經支持該特性。


          我第一次看到 async/await 這組關鍵字并不是在 JavaScript 語言里,而是在 C# 5.0 的語法中。C# 的 async/await 需要在 .NET Framework 4.5 以上的版本中使用,因此我還很悲傷了一陣——為了要兼容 XP 系統,我們開發的軟件不能使用高于 4.0 版本的 .NET Framework。


          1. async 和 await 在干什么

          任意一個名稱都是有意義的,先從字面意思來理解。async 是“異步”的簡寫,而 await 可以認為是 async wait 的簡寫。所以應該很好理解 async 用于申明一個 function 是異步的,而 await 用于等待一個異步方法執行完成。

          另外還有一個很有意思的語法規定,await 只能出現在 async 函數中。然后細心的朋友會產生一個疑問,如果 await 只能出現在 async 函數中,那這個 async 函數應該怎么調用?

          如果需要通過 await 來調用一個 async 函數,那這個調用的外面必須得再包一個 async 函數,然后……進入死循環,永無出頭之日……

          如果 async 函數不需要 await 來調用,那 async 到底起個啥作用?

          1.1 async 起什么作用

          這個問題的關鍵在于,async 函數是怎么處理它的返回值的!

          我們當然希望它能直接通過 return 語句返回我們想要的值,但是如果真是這樣,似乎就沒 await 什么事了。所以,寫段代碼來試試,看它到底會返回什么:

          async function testAsync() {
              return "hello async";
          }
          
          const result=testAsync();
          console.log(result);

          看到輸出就恍然大悟了——輸出的是一個 Promise 對象。

          c:\var\test> node --harmony_async_await .
          Promise { 'hello async' }

          所以,async 函數返回的是一個 Promise 對象。從文檔中也可以得到這個信息。async 函數(包含函數語句、函數表達式、Lambda表達式)會返回一個 Promise 對象,如果在函數中 return 一個直接量,async 會把這個直接量通過 Promise.resolve() 封裝成 Promise 對象。

          補充知識點


          Promise.resolve(x) 可以看作是 new Promise(resolve=> resolve(x)) 的簡寫,可以用于快速封裝字面量對象或其他對象,將其封裝成 Promise 實例。

          async 函數返回的是一個 Promise 對象,所以在最外層不能用 await 獲取其返回值的情況下,我們當然應該用原來的方式:then() 鏈來處理這個 Promise 對象,就像這樣

          testAsync().then(v=> {
              console.log(v);    // 輸出 hello async
          });

          現在回過頭來想下,如果 async 函數沒有返回值,又該如何?很容易想到,它會返回Promise.resolve(undefined)。

          聯想一下 Promise 的特點——無等待,所以在沒有 await 的情況下執行 async 函數,它會立即執行,返回一個 Promise 對象,并且,絕不會阻塞后面的語句。這和普通返回 Promise 對象的函數并無二致。

          那么下一個關鍵點就在于 await 關鍵字了。

          1.2 await 到底在等啥


          一般來說,都認為 await 是在等待一個 async 函數完成。不過按語法說明(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await),await 等待的是一個表達式,這個表達式的計算結果是 Promise 對象或者其它值(換句話說,就是沒有特殊限定)。

          因為 async 函數返回一個 Promise 對象,所以 await 可以用于等待一個 async 函數的返回值——這也可以說是 await 在等 async 函數,但要清楚,它等的實際是一個返回值。注意到 await 不僅僅用于等 Promise 對象,它可以等任意表達式的結果,所以,await 后面實際是可以接普通函數調用或者直接量的。所以下面這個示例完全可以正確運行

          function getSomething() {
              return "something";
          }
          
          async function testAsync() {
              return Promise.resolve("hello async");
          }
          
          async function test() {
              const v1=await getSomething();
              const v2=await testAsync();
              console.log(v1, v2);
          }
          
          test();

          1.3 await 等到了要等的,然后呢


          await 等到了它要等的東西,一個 Promise 對象,或者其它值,然后呢?我不得不先說,await 是個運算符,用于組成表達式,await 表達式的運算結果取決于它等的東西。

          如果它等到的不是一個 Promise 對象,那 await 表達式的運算結果就是它等到的東西。

          如果它等到的是一個 Promise 對象,await 就忙起來了,它會阻塞后面的代碼,等著 Promise 對象 resolve,然后得到 resolve 的值,作為 await 表達式的運算結果。

          看到上面的阻塞一詞,心慌了吧……放心,這就是 await 必須用在 async 函數中的原因。async 函數調用不會造成阻塞,它內部所有的阻塞都被封裝在一個 Promise 對象中異步執行。

          2. async/await 幫我們干了啥

          2.1 作個簡單的比較

          上面已經說明了 async 會將其后的函數(函數表達式或 Lambda)的返回值封裝成一個 Promise 對象,而 await 會等待這個 Promise 完成,并將其 resolve 的結果返回出來。

          現在舉例,用 setTimeout 模擬耗時的異步操作,先來看看不用 async/await 會怎么寫

          function takeLongTime() {
              return new Promise(resolve=> {
                  setTimeout(()=> resolve("long_time_value"), 1000);
              });
          }
          
          takeLongTime().then(v=> {
              console.log("got", v);
          });

          如果改用 async/await 呢,會是這樣

          function takeLongTime() {
              return new Promise(resolve=> {
                  setTimeout(()=> resolve("long_time_value"), 1000);
              });
          }
          
          async function test() {
              const v=await takeLongTime();
              console.log(v);
          }
          
          test();

          眼尖的同學已經發現 takeLongTime() 沒有申明為 async。實際上,takeLongTime() 本身就是返回的 Promise 對象,加不加 async 結果都一樣,如果沒明白,請回過頭再去看看上面的“async 起什么作用”。

          又一個疑問產生了,這兩段代碼,兩種方式對異步調用的處理(實際就是對 Promise 對象的處理)差別并不明顯,甚至使用 async/await 還需要多寫一些代碼,那它的優勢到底在哪?

          2.2 async/await 的優勢在于處理 then 鏈

          單一的 Promise 鏈并不能發現 async/await 的優勢,但是,如果需要處理由多個 Promise 組成的 then 鏈的時候,優勢就能體現出來了(很有意思,Promise 通過 then 鏈來解決多層回調的問題,現在又用 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();
          
          // c:\var\test>node --harmony_async_await .
          // step1 with 300
          // step2 with 500
          // step3 with 700
          // result is 900
          // doIt: 1507.251ms

          輸出結果 result 是 step3() 的參數 700 + 200=900。doIt() 順序執行了三個步驟,一共用了 300 + 500 + 700=1500 毫秒,和 console.time()/console.timeEnd() 計算的結果一致。

          如果用 async/await 來實現呢,會是這樣

          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();

          結果和之前的 Promise 實現是一樣的,但是這個代碼看起來是不是清晰得多,幾乎跟同步代碼一樣

          2.3 還有更酷的

          現在把業務要求改一下,仍然是三個步驟,但每一個步驟都需要之前每個步驟的結果。

          function step1(n) {
              console.log(`step1 with ${n}`);
              return takeLongTime(n);
          }
          
          function step2(m, n) {
              console.log(`step2 with ${m} and ${n}`);
              return takeLongTime(m + n);
          }
          
          function step3(k, m, n) {
              console.log(`step3 with ${k}, ${m} and ${n}`);
              return takeLongTime(k + m + n);
          }

          這回先用 async/await 來寫:

          async function doIt() {
              console.time("doIt");
              const time1=300;
              const time2=await step1(time1);
              const time3=await step2(time1, time2);
              const result=await step3(time1, time2, time3);
              console.log(`result is ${result}`);
              console.timeEnd("doIt");
          }
          
          doIt();
          
          // c:\var\test>node --harmony_async_await .
          // step1 with 300
          // step2 with 800=300 + 500
          // step3 with 1800=300 + 500 + 1000
          // result is 2000
          // doIt: 2907.387ms

          除了覺得執行時間變長了之外,似乎和之前的示例沒啥區別啊!別急,認真想想如果把它寫成 Promise 方式實現會是什么樣子?

          function doIt() {
              console.time("doIt");
              const time1=300;
              step1(time1)
                  .then(time2=> {
                      return step2(time1, time2)
                          .then(time3=> [time1, time2, time3]);
                  })
                  .then(times=> {
                      const [time1, time2, time3]=times;
                      return step3(time1, time2, time3);
                  })
                  .then(result=> {
                      console.log(`result is ${result}`);
                      console.timeEnd("doIt");
                  });
          }
          
          doIt();

          有沒有感覺有點復雜的樣子?那一堆參數處理,就是 Promise 方案的死穴—— 參數傳遞太麻煩了,看著就暈!


          作者:邊城

          鏈接:https://segmentfault.com/a/1190000007535316


          邊城大佬這篇文章寫得非常好,但是最后忘記講Promise有可能reject的情況,這里我再做一下補充:

          function foo () {
            return Promise.reject('error');
          }
          
          async function bar () {
            try {
              let r=await foo();
            } catch (e) {
              console.error(e)
            }
          }
          
          bar();

          使用try...catch就能捕獲到reject的錯誤。

          efer和async是<script>標簽的兩個屬性,它們用于控制瀏覽器如何加載和執行外部腳本。這兩個屬性的引入,主要是為了解決傳統<script>標簽在加載和執行JavaScript文件時會阻塞頁面的問題。下面將詳細解析defer和async的區別:

          加載和執行順序

          • <script async>:當使用async屬性時,腳本會在加載完成后立即執行,而不會等待文檔解析完成。這意味著,如果多個<script>標簽都使用了async屬性,它們的執行順序是不確定的,誰先加載完成誰就先執行。
          • <script defer>:使用defer屬性時,腳本會在瀏覽器解析完文檔后按順序執行。即如果腳本A在腳本B之前出現在HTML中,并且兩者都使用了defer屬性,那么腳本A將在腳本B之前執行。

          頁面解析阻塞情況

          • <script async>:腳本加載是異步進行,不會阻塞頁面解析和其他資源的加載。但是,執行腳本時仍然會暫停頁面解析,直到腳本執行完成。
          • <script defer>:與async類似,使用defer屬性的腳本也是異步加載的,不會阻塞頁面解析。不同之處在于,defer屬性的腳本會在頁面解析完成后執行。

          依賴關系處理

          • <script async>:由于執行時機不確定,async屬性通常適合那些不依賴其他腳本的獨立模塊。例如,一些可選的用戶界面增強功能或分析工具等。
          • <script defer>:defer屬性適用于那些需要按順序執行的腳本,尤其是當腳本之間存在依賴關系時。例如,一個大型的應用或庫,其初始化代碼依賴于其他幾個模塊。

          適用場景

          • <script async>:對于一些小型且獨立的腳本,使用async能夠提高頁面加載速度,并盡快地激活這些腳本的功能。
          • <script defer>:對于那些需要在頁面完全解析完成后才執行的腳本,使用defer能確保腳本按預期順序執行,從而避免因執行順序不當導致的錯誤。

          綜上所述,defer和async屬性的主要區別在于加載和執行時機以及是否保證執行順序。根據實際開發需求,如果腳本之間沒有嚴格的依賴關系,可以使用async來提高頁面性能;如果腳本需要按特定順序執行,或者有明確的依賴關系,使用defer更為合適。


          主站蜘蛛池模板: 国产a∨精品一区二区三区不卡 | 国产精品综合AV一区二区国产馆 | 无码人妻啪啪一区二区| 国产精品99精品一区二区三区| 亚洲人成人一区二区三区| 国产成人欧美一区二区三区| 国产福利视频一区二区| 国产高清精品一区| 午夜视频在线观看一区| 无码国产精品一区二区免费I6| 日本不卡一区二区三区视频| 中文字幕一区二区在线播放| 高清无码一区二区在线观看吞精| 日韩免费一区二区三区在线 | 精品亚洲福利一区二区| 中字幕一区二区三区乱码| 亚洲AV福利天堂一区二区三| 国精产品一区一区三区| 国内国外日产一区二区| 在线精品亚洲一区二区三区| 国模无码一区二区三区不卡| 国产精品一区二区综合| 国产福利一区视频| 国产一区中文字幕在线观看| 国产精品视频一区二区三区无码 | 亚洲国产精品一区二区久久| 国模大尺度视频一区二区| 国产一区二区三区亚洲综合| 亚洲日韩一区精品射精| 精品少妇ay一区二区三区| 久久婷婷久久一区二区三区| 日本一区午夜爱爱| 国精产品一区一区三区MBA下载| 无码精品久久一区二区三区 | 中文字幕无码一区二区三区本日| 波多野结衣中文字幕一区二区三区| 亚洲精品色播一区二区| 无码国产精品一区二区免费式影视 | 国产视频一区二区在线观看| 日韩一区二区视频| 亚洲av无码一区二区三区人妖 |