<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JavaScript 異步編程史

          共 7691字,需瀏覽 16分鐘

           ·

          2021-06-06 14:36

          前言

          早期的 Web 應(yīng)用中,與后臺進行交互時,需要進行 form 表單的提交,然后在頁面刷新后給用戶反饋結(jié)果。在頁面刷新過程中,后臺會重新返回一段 HTML 代碼,這段 HTML 中的大部分內(nèi)容與之前頁面基本相同,這勢必造成了流量的浪費,而且一來一回也延長了頁面的響應(yīng)時間,總是會讓人覺得 Web 應(yīng)用的體驗感比不上客戶端應(yīng)用。

          2004 年,AJAX 即“Asynchronous JavaScript and XML”技術(shù)橫空出世,讓 Web 應(yīng)用的體驗得到了質(zhì)的提升。再到 2006 年,jQuery 問世,將 Web 應(yīng)用的開發(fā)體驗也提高到了新的臺階。

          由于 JavaScript 語言單線程的特點,不管是事件的觸發(fā)還是 AJAX 都是通過回調(diào)的方式進行異步任務(wù)的觸發(fā)。如果我們想要線性的處理多個異步任務(wù),在代碼中就會出現(xiàn)如下的情況:

          getUser(token, function (user{
            getClassID(user, function (id{
              getClassName(id, function (name{
                console.log(name)
              })
            })
          })

          我們經(jīng)常將這種代碼稱為:“回調(diào)地獄”。

          事件與回調(diào)

          眾所周知,JavaScript 的運行時是跑在單線程上的,是基于事件模型來進行異步任務(wù)觸發(fā)的,不需要考慮共享內(nèi)存加鎖的問題,綁定的事件會按照順序齊齊整整的觸發(fā)。要理解 JavaScript 的異步任務(wù),首先就要理解 JavaScript 的事件模型。

          由于是異步任務(wù),我們需要組織一段代碼放到未來運行(指定時間結(jié)束時或者事件觸發(fā)時),這一段代碼我們通常放到一個匿名函數(shù)中,通常稱為回調(diào)函數(shù)。

          setTimeout(function ({
            // 在指定時間結(jié)束時,觸發(fā)的回調(diào)
          }, 800)
          window.addEventListener("resize"function({
            // 當(dāng)瀏覽器視窗發(fā)生變化時,觸發(fā)的回調(diào)
          })

          未來運行

          前面說過回調(diào)函數(shù)的運行是在未來,這就說明回調(diào)中使用的變量并不是在回調(diào)聲明階段就固定的。

          for (var i = 0; i < 3; i++) {
            setTimeout(function ({
              console.log("i =", i)
            }, 100)
          }

          這里連續(xù)聲明了三個異步任務(wù),100毫秒 后會輸出變量 i 的結(jié)果,按照正常的邏輯應(yīng)該會輸出 0、1、2 這三個結(jié)果。

          然而,事實并非如此,這也是我們剛開始接觸 JavaScript 的時候會遇到的問題,因為回調(diào)函數(shù)的實際運行時機是在未來,所以輸出的 i 的值是循環(huán)結(jié)束時的值,三個異步任務(wù)的結(jié)果一致,會輸出三個 i = 3。

          經(jīng)歷過這個問題的同學(xué),一般都知道,我們可以通過閉包的方式,或者重新聲明局部變量的方式解決這個問題。

          事件隊列

          事件綁定之后,會將所有的回調(diào)函數(shù)存儲起來,然后在運行過程中,會有另外的線程對這些異步調(diào)用的回調(diào)進行調(diào)度的處理,一旦滿足“觸發(fā)”條件就會將回調(diào)函數(shù)放入到對應(yīng)的事件隊列(這里只是簡單的理解成一個隊列,實際存在兩個事件隊列:宏任務(wù)、微任務(wù))中。

          滿足觸發(fā)條件一般有以下幾種情況:

          1. DOM 相關(guān)的操作進行的事件觸發(fā),比如點擊、移動、失焦等行為;
          2. IO 相關(guān)的操作,文件讀取完成、網(wǎng)絡(luò)請求結(jié)束等;
          3. 時間相關(guān)的操作,到達定時任務(wù)的約定時間;

          上面的這些行為發(fā)生時,代碼中之前指定的回調(diào)函數(shù)就會被放入一個任務(wù)隊列中,主線程一旦空閑,就會將其中的任務(wù)按照先進先出的流程一一執(zhí)行。當(dāng)有新的事件被觸發(fā)時,又會重新放入到回調(diào)中,如此循環(huán)??,所以 JavaScript 的這一機制通常被稱為“事件循環(huán)機制”。

          for (var i = 1; i <= 3; i++) {
            const x = i
            setTimeout(function ({
              console.log(`第${x}個setTimout被執(zhí)行`)
            }, 100)
          }

          可以看到,其運行順序滿足隊列先進先出的特點,先聲明的先被執(zhí)行。

          線程的阻塞

          由于 JavaScript 單線程的特點,定時器其實并不可靠,當(dāng)代碼遇到阻塞的情況,即使事件到達了觸發(fā)的時間,也會一直等在主線程空閑才會運行。

          const start = Date.now()
          setTimeout(function ({
            console.log(`實際等待時間: ${Date.now() - start}ms`)
          }, 300)

          // while循環(huán)讓線程阻塞 800ms
          while(Date.now() - start < 800) {}

          上面代碼中,定時器設(shè)置了 300ms 后觸發(fā)回調(diào)函數(shù),如果代碼沒有遇到阻塞,正常情況下會 300ms 后,會輸出等待時間。

          但是我們在還沒加了一個 while 循環(huán),這個循環(huán)會在 800ms 后才結(jié)束,主線程一直被這個循環(huán)阻塞在這里,導(dǎo)致時間到了回調(diào)函數(shù)也沒有正常運行。

          Promise

          事件回調(diào)的方式,在編碼的過程中,就特別容易造成回調(diào)地獄。而 Promise 提供了一種更加線性的方式編寫異步代碼,有點類似于管道的機制。

          // 回調(diào)地獄
          getUser(token, function (user{
            getClassID(user, function (id{
              getClassName(id, function (name{
                console.log(name)
              })
            })
          })

          // Promise
          getUser(token).then(function (user{
            return getClassID(user)
          }).then(function (id{
            return getClassName(id)
          }).then(function (name{
            console.log(name)
          }).catch(function (err{
            console.error('請求異常', err)
          })

          Promise 在很多語言中都有類似的實現(xiàn),在 JavaScript 發(fā)展過程中,比較著名的框架 jQuery、Dojo 也都進行過類似的實現(xiàn)。2009 年,推出的 CommonJS 規(guī)范中,基于 Dojo.Deffered 的實現(xiàn)方式,提出 Promise/A 規(guī)范。也是這一年 Node.js 橫空出世,Node.js 很多實現(xiàn)都是依照 CommonJS 規(guī)范來的,比較熟悉的就是其模塊化方案。

          早期的 Node.js 中也實現(xiàn)了 Promise 對象,但是 2010 年的時候,Ry(Node.js 作者)認為 Promise 是一種比較上層的實現(xiàn),而且 Node.js 的開發(fā)本來就依賴于 V8 引擎,V8 引擎原生也沒有提供 Promise 的支持,所以后來 Node.js 的模塊使用了 error-first callback 的風(fēng)格(cb(error, result))。

          const fs = require('fs')
          // 第一個參數(shù)為 Error 對象,如果不為空,則表示出現(xiàn)異常
          fs.readFile('./README.txt'function (err, buffer{
            if (err !== null) {
              return
            }
            console.log(buffer.toString())
          })

          這一決定也導(dǎo)致后來 Node.js 中出現(xiàn)了各式各樣的 Promise 類庫,比較出名的就是 Q.js、Bluebird。關(guān)于 Promise 的實現(xiàn),之前有寫過一篇文章,感興趣可以看看:《手把手教你實現(xiàn) Promise》。

          在 Node.js@8 之前,V8 原生的 Promise 實現(xiàn)有一些性能問題,導(dǎo)致原生 Promise 的性能甚至不如一些第三方的 Promise 庫。

          所以,低版本的 Node.js 項目中,經(jīng)常會將 Promise 進行全局的替換:

          const Bulebird = require('bluebird')
          global.Promise = Bulebird

          Generator & co

          Generator(生成器) 是 ES6 提供的一種新的函數(shù)類型,主要是用于定義一個能自我迭代的函數(shù)。通過 function * 的語法能夠構(gòu)造一個 Generator 函數(shù),函數(shù)執(zhí)行后會返回一個iteration(迭代器)對象,該對象具有一個 next() 方法,每次調(diào)用 next() 方法就會在 yield 關(guān)鍵詞前面暫停,直到再次調(diào)用 next() 方法。

          function * forEach(array{
            const len = array.length
            for (let i = 0; i < len; i ++) {
              yield i;
            }
          }
          const it = forEach([246])
          it.next() // { value: 2, done: false }
          it.next() // { value: 4, done: false }
          it.next() // { value: 6, done: false }
          it.next() // { value: undefined, done: true }

          next() 方法會返回一個對象,對象有兩個屬性 value、done

          • value:表示 yield 后面的值;
          • done:表示函數(shù)是否執(zhí)行完畢;

          由于生成器函數(shù)具有中斷執(zhí)行的特點,將生成器函數(shù)當(dāng)做一個異步操作的容器,再配合上 Promise 對象的 then 方法可以將交回異步邏輯的執(zhí)行權(quán),在每個 yeild 后面都加上一個 Promise 對象,就能讓迭代器不停的往下執(zhí)行。

          function * gen(token{
            const user = yield getUser(token)
            const cId = yield getClassID(user)
            const name = yield getClassName(cId)
            console.log(name)
          }

          const g = gen('xxxx-token')

          // 執(zhí)行 next 方法返回的 value 為一個 Promise 對象
          const { value: promise1 } = g.next()
          promise1.then(user => {
            // 傳入第二個 next 方法的值,會被生成器中第一個 yield 關(guān)鍵詞前面的變量接受
            // 往后推也是如此,第三個 next 方法的值,會被第二個 yield 前面的變量接受
            // 只有第一個 next 方法的值會被拋棄
            const { value: promise2 } = gen.next(user).value
            promise2.then(cId => {
              const { value: promise3, done } = gen.next(cId).value
              // 依次先后傳遞,直到 next 方法返回的 done 為 true
            })
          })

          我們將上面的邏輯進行一下抽象,讓每個 Promise 對象正常返回后,就自動調(diào)用 next,讓迭代器進行自執(zhí)行,直到執(zhí)行完畢(也就是 donetrue)。

          function co(gen, ...args{
            const g = gen(...args)
            function next(data{
              const { value: promise, done } = g.next(data)
              if (done) return promise
              promise.then(res => {
                next(res) // 將 promise 的結(jié)果傳入下一個 yield
              })
            }
            
            next() // 開始自執(zhí)行
          }

          co(gen, 'xxxx-token')

          這也就是 koa 早期的核心庫 co 的實現(xiàn)邏輯,只是 co 進行了一些參數(shù)校驗與錯誤處理。通過 generator 加上 co 能夠讓異步流程更加的簡單易讀,對開發(fā)者而言肯定是階段歡喜的一件事。

          async/await

          async/await 可以說是 JavaScript 異步變成的解決方案,其實本質(zhì)上就是 Generator & co 的一個語法糖,只需要在異步的生成器函數(shù)前加上 async,然后將生成器函數(shù)內(nèi)的 yield 替換為 await。

          async function fun(token{
            const user = await getUser(token)
            const cId = await getClassID(user)
            const name = await getClassName(cId)
            console.log(name)
          }

          fun()

          async 函數(shù)將自執(zhí)行器進行了內(nèi)置,同時 await 后不限制為 Promise 對象,可以為任意值,而且 async/await 在語義上比起生成器的 yield 更加清楚,一眼就能明白這是一個異步操作。

          - END -


          瀏覽 74
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品免费在线 | 日日干天天干视频 | 韩国毛片一级 | 无码精品人妻一区二区欧美 | 又粗又长又大一级a片免费看 |