JavaScript 中的異步模式

JavaScript 中的各種異步模式

Callback

我們知道在 JavaScript 中,函數是一等公民,當一個函數傳入另外一個函數當作參數時,我們就可以把這個函數叫做 Callback 函數。而這裏的「另外一個函數」也有一個常見的名字,Hight order function 高階函數。

需要澄清的一點是,Callback 並非都是異步執行的。比如在我們常用的Array.prototype.map()中,其第一個參數也是一個回調函數,但是它是同步執行的。本文關注異步,如果沒有特殊說明,文中提到的 CallBack 指的都是異步回調函數。

下面是兩個典型的異步回調示例:

// 示例1
setTimeout(function cb() {
  output('callback')
}, 1000)

// 示例2
fs.readFile('./imaginary.txt', function(err, result) {
   if (err) {
      return console.error('Error:', err);
   }
      return output('Result:', result);
})

上述代碼很簡單,不過其說明了異步函數的兩個特點:

  1. Callback 實際上把程序分爲了立即執行部分和稍後執行部分,而兩部分之間發生了什麼,則在一定程度上並不受我們控制;

  2. 上面的 setTimeout 和 fs.readFile 都不是 JS 語言提供的方法,JS 中異步的實現嚴重依賴於宿主環境,實際上在 Promise 之前,JS 語言本身是沒有異步機制的;

Callback 存在着以下兩個問題而飽受詬病[1] :

  • 控制反轉(inversion of control);

  • 難以理解

所謂控制反轉指的是,Callback 函數的調用在一定程度上是不受我們的控制的,我們缺少可靠的機制確保回調函數能按照預期被執行。

所謂難以理解,令人生畏的回調地獄就是其具體體現。

回調地獄

回調地獄常常被人誤解爲,嵌套的回調結構,如下所示:

setTimeout(function() {
  output('one')
  setTimeout(function() {
    output('two')
    setTimeout(function() {
      output('three')
    }, 1000)
  }, 1000)
}, 1000)

你也能夠看出,上述代碼雖有有多層嵌套,但是總體上還是比較容易理解的。

稍微改動一下還可以寫做下面這種沒有嵌套的形式:

function one(cb) {
  output('one')
  setTimeout(cb, 1000)
}

function two(cb) {
  output('two')
  setTimeout(cb, 1000)
}

function three() {
  output('three')
}

one(function() {
  two(three)
})

可見回調地獄的問題不在於嵌套。那麼回調地獄的問題究竟出在哪裏呢?也許換一個更真實一些的例子,能表達得更清楚。

比如說,我們現在有這麼一個需求,我們有三個不同的文件file1,file2,file3,我們希望並行請求這些文件,並按照順序依次展示出文件內容,如果僅僅使用 Callback,該怎麼做呢?

這裏假設我們有以下工具函數:

  • fakeAjax: 用於請求文件內容,接受請求地址和 callback 兩個參數;

  • output: 輸入內容

我第一次遇到這個問題還真是的嘗試了好半天。

這個題目來自於我看過的一個網課[2] ,後續還會多次出現,使用不同的異步模式解決。

答案如下:

function getFile(file) {
  fakeAjax(file, function(text) {
    fileReceived(file, text)
  })
}

function fileReceived(file, text) {
  if (!responses[file]) {
    responses[file] = text
  }

  const files = ['file1', 'file2', 'file3']

  for (let i = 0; i < files.length; i++) {
    if (files[i] in responses) {
      if (responses[files[i]] !== true) {
        output(responses[files[i]])
        responses[files[i]] = true
      }
    } else {
      return false
    }
  }
  output('Complete!')
}

const responses = {}

getFile('file1')
getFile('file2')
getFile('file3')

這個問題最大的難點在於,我們需要保證順序輸出但是每個請求的「時間」卻是不確定的,使得我們不得不使用額外的變量來管理輸出的先後順序。

這其實才是回調地獄所在,必須使用額外的外層變量來協同不同的回調,這會明顯的增加代碼的複雜度,讓我們的代碼難以理解,難以書寫。

這讓我們很自然的去想,如果我們的異步代碼不用考慮「時間」,也許異步的邏輯就會簡單很多。還真的存在着這麼一種抽象方式 — thunk.

thunk

thunk 是一個在 1961 年就被提出的概念。這裏有一篇論文[3],表明了其起源和含義,感興趣可以點擊鏈接閱讀。總的來說,thunk 是一種函數,其返回值也是一個函數。

提到 thunk,你可能馬上就會想到 redux-thunk,其對自己的定義如下:

Redux Thunk middleware allows you to write action creators that return a function instead of an action.

同 Callback 一樣,thunk 也有同步和異步之分。這裏我們引用 getify 對二者的定義。

同步 thunk

Form a synchronous perspective , a thunk is a function that has everything already that it needs to do to give your some value back. you do not need to pass any arguments in, you simply call it and it will give you a value back .
從同步的角度看,thunk 是一種函數,這種函數已經包含了所有你需要的值,你不需要傳入任何參數,僅僅需要調用它,它就會將值返回給你。

以下是一個同步 thunk 的示例

function add(x,y){
    return x + y;
}

var thunk = function(){
    return add(10,15)
}

thunk()

從不同的角度看上述代碼可能會有不同的理解,不知道你會不會想到,上面的函數 thunk 其實可以看作一個值的包裹體,我們完全不用考慮其在內部做了什麼,但是我們卻能保證,只要調用 thunk 函數,我們就能獲得一個固定的值。

下面是一個異步 thunk 的示例:

function addAsync(x,y,cb){
    setTimeout(function(){
        cb(x+y)
    },1000)
}

const thunk = function(cb){
    addAsync(10,15,cb)
}

thunk((sum)=>{output(sum)})

初看起來,thunk 好像讓我們的代碼變得更加複雜了,不過如果我們仔細想想就能發現 thunk 把時間的概念抽象出去的,執行 thunk 函數後,我們只需要等待結果就行,無需去關心 addAsync 是什麼,做了什麼事情,需要花費多少時間。上面我們提到時間是程序中最複雜的狀態因素。管理時間是程序中最複雜的問題之一,而這裏通過thunk 我們把時間抽象出去了。

我們再回頭看看前面那個「併發請求,順序輸出」的問題,利用異步 thunk 這種模式,就可以按照下面這樣來寫:

function getFile(file) {
  let resp

  fakeAjax(file, function(text) {
    if (!resp) resp = text
    else resp(text)
  })

  return function th(cb) {
    if (resp) cb(resp)
    else resp = cb
  }
}

const th1 = getFile('file1')
const th2 = getFile('file2')
const th3 = getFile('file3')

th1(function ready(text) {
  output(text)
  th2(function ready(text) {
    output(text)
    th3(function ready(text) {
      output(text)
      output('Complete!')
    })
  })
})

比起上面的純 Callback 方案,利用 thunk,我們的代碼好理解很多。thunk 的本質其實是使用閉包來管理狀態。

因爲 thunk 真的很有用,也存在很多將異步 callback 轉換爲 thunk 的工具庫,比如 thunks 或 node-thunkify ,感興趣也可以看看。

異步 thunk 讓時間不再是問題,如果我們換個角度看 ,它就好似是給一個未來的值添加了展位符。有沒有覺得這種說法似曾相識,沒錯,Promise 也是如此。在 Promise 中,時間也被抽離了出去。

Promises is a time-independent wrapper around a value, it just has a fancier API. Thunk is a promises without fancy api. — getify

雖然異步 thunk 抽離出時間後,我們的代碼稍微更好理解了,但是回調的另外一個問題 — 依賴反轉,通過 thunk 卻難以克服。如果用人話來說「依賴反轉」,其實這是一種信任問題,回調函數的調用其實是受外界控制的,其會不會被調用,會被調用幾次都不能完全受我們控制。爲了解決這個問題,Promise 粉墨登場。

Promise

有時候在想,學習一門語言的新語法,其實不應該侷限於其用法,而應當嘗試去了解其背後的理念,其想解決的問題。我其實使用 Promise 很久了,甚至是在現在的工作中,使用最多的還是它。但是其實直到不久前,我才理清 Promise 實際上有以下三重身份:

  1. 爲一個未來值提供了佔位符,消除時間的影響;

  2. 事件監聽器,監聽 then 時間;

  3. 提供了一種以可靠的方式管理我們的回調;

Promise 實在是太常用了,在此不再贅述其用法,如果我們使用 Promise 再次

解決上面那個順序輸出文件內容的問題,則可以按照下面這樣寫:

function getFile(file) {
  return new Promise(function(resolve) {
    fakeAjax(file, resolve)
  })
}

function output(text) {
  output(text)
}

const p1 = getFile('file1')
const p2 = getFile('file2')
const p3 = getFile('file3')

p1.then(output)
  .then(function() {
    return p2
  })
  .then(output)
  .then(function() {
    return p3
  })
  .then(output)
  .then(function() {
    output('Complete!')
  })

這個代碼太好讀了,怪不得人人都愛 Promise。如果你習慣函數式編程,我們

甚至還可以進一步簡化爲下面這樣:

['file1', 'file2', 'file3']
  .map(getFile)
  .reduce(
    function(chain, filePromise) {
      return chain
        .then(function() {
          return filePromise
        })
        .then(output)
    },
    Promise.resolve()
  )
  .then(function() {
    output('Complete!')
  })

很多人覺得 Promise 的好是好在其鏈式調用的語法(我剛接觸 Promise 的時

候,也是這麼覺得,畢竟它看起來比嵌套的回調清晰太多了。)不過鏈式調用真的不是 Promise 的核心,這種鏈式調用的方式可以比較容易通過 Callback 模擬的,具體怎麼做,可參看一些 polyfill 中的實現,Promise 的 Polyfill 其實很多,如 es6-promise,promise-polyfill ,native-promise-only)。

Promise 的核心在於其通過一種協議[4]保障了then後註冊的函數只會被執行一次。這和上面提到的回調不同,普通的 callback 實際上是第三方直接調用我們的函數,這個第三方不一定是完全可信的,我們的回調函數可能會被調用,也可能不會調用,還可能會調用多次。Promise 則將代碼的執行控制在我們自己手裏,要麼成功要麼失敗,then後面的函數只會執行一次。

不過 Promise 也有一些缺陷被人詬病,主要體現在以下兩個方面:

  • 一旦開始執行就沒辦法手動終止;在滿足一些條件時我們可能會希望不再執行後續的 then,這在 Promise 中就很難優雅的做到;

  • 我們無法完全捕獲可能的錯誤。比如說 .catch 中的錯誤就難以再被捕獲;

介於此,Generator 應運而生。

補充說明:
值得一提的是,Promise 並非 JavaScript 首創,這個概念在 1976 年就被提出,並在多種語言中有實現 Futures and promises - Wikipedia ,最早的實現可能是 e 語言中的 future。有時候不得不感慨,如果能對計算機軟件相關的各種歷史背景有更多的瞭解,學或者用起來一些東西,肯定會更加得心應手。

generator

generator 算是 JavaScript 中一個比較難學的概念了,感覺我自己花了好久才弄得比較明白。它被認爲是 JavaScript 這門語言中最具革命性的改變了。本文不會具體去講解 generator 該怎麼用,如果你覺得還不太會,推薦閱讀以下資料。

  • The Basics Of ES6 Generators

  • Diving Deeper With ES6 Generators[5]

  • Going Async With ES6 Generators

  • Getting Concurrent With ES6 Generators

這四篇是一個系列,把 generator 講解得算是很透徹了。

歸納起來 generator 函數具有以下特點:

  • 函數可暫停和繼續;

  • 可返回多個值給外部;

  • 在繼續的時候,外面也可以再傳入值;

  • 通過 Generator 寫的異步代碼看起來就像是同步的;

可以像同步代碼那樣捕獲錯誤;

generator 把我們的代碼分割成了獨立可阻塞的部分,局部的阻塞不會導致全局的阻塞,有時候在想這個特性其實讓我們可能可以去模擬獨立的線程做的事情,還挺有意思的。

generator 被詬病比較多的一個地方是它不能自動執行,每當遇到yield就會暫停,就需要我們手動調用 .next()來繼續執行後面的內容。這個方法在任何地方都可能被調用,因此又出現了在 callback 中出現過的「控制反轉」問題。我們完全不知道誰會在什麼地方調用.next(),結合 Promise 我們可以比較輕鬆的解決「控制反轉」的問題,一些人把 Promise + Generator 當作是異步最好的解決方案之一。

  • Promise 用以解決控制反轉問題;

  • Generator 則讓我們的異步代碼看起來像是同步的,非常容易書寫和理解;

具體來說,我們可以在 Promise 中調用 .next(),Promise 機制保證了.next() 的調用是受控制的。

以下是二者結合使用的一個示例:

// 這裏封裝的 runGenerator 可以讓 generator 自動運行起來
function runGenerator(g) {
  const it = g()
  let ret
  (function iterate(val) {
    ret = it.next(val)
    if (!ret.done) {
      if ('then' in ret.value) {
        ret.value.then(iterate)
      } else {
        setTimeout(function() {
          iterate(ret.value)
        }, 0)
      }
    }
  })()
}

function makeAjaxCall(url, cb) {
  setTimeout(cb(url), 1000)
}

function request(url) {
  return new Promise(function(resolve, reject) {
    makeAjaxCall(url, function(err, text) {
      if (err) reject(err)
      else resolve(text)
    })
  })
}

runGenerator(function* main() {
  let result1
  try {
    result1 = yield request('http://some.url.1')
  } catch (err) {
    output('Error: ' + err)
    return
  }
  const data = JSON.parse(result1)

  let result2
  try {
    result2 = yield request('http://some.url.2?id=' + data.id)
  } catch (err) {
    output('Error: ' + err)
    return
  }
  const resp = JSON.parse(result2)
  output('The value you asked for: ' + resp.value)
})

上述代碼中,我們用 Promise + generator 模擬了 async 函數。如果有面試題問可以怎麼自己實現一個 async await 就可以按照上面的思路這樣來寫一個 webpack 插件來解決了,這裏還有一篇文章對此進行了詳細的介紹[6]。

如果使用 generator 來解決上面那個老問題,則可以按照下面這樣寫了,非常清晰明瞭。

function* loadFiles() {
  const p1 = getFile('file1')
  const p2 = getFile('file2')
  const p3 = getFile('file3')

  output(yield p1)
  output(yield p2)
  output(yield p3)
}

runGenerator(loadFiles)

你可能會說還是需要額外封裝 runGenerator 函數,真是麻煩。果然還是 async函數 最好。在一些場景下,async 函數確實非常好用,不過也有一些缺點,後文會有描述。

generator 是一種新的語法形式,所以不能像 Promise 那樣通過引用 polyfill 就可以使用,不過通過 Babel 等工具可以將其轉換爲 ES5 語法,如果你想了解轉換具體在底層到底是怎麼做的,可以參看 Pre-ES6 Generators[7]。

Async await 函數

使用 async 解決我們上述提出的問題,可以使用下面這樣的方式。

async function loadFiles(){
  const p1 = getFile('file1')
  const p2 = getFile('file2')
  const p3 = getFile('file3')

  output(await p1)
  output(await p2)
  output(await p3)
}

loadFiles()

確實更加簡單優雅了。很長一段時間裏,我都把 Async 函數當作是 JavaScript 中處理異步最完美的方案。直到看到 redux-saga 的作者明確表明不會使用 async await 取代 generator 來重寫 redux-saga [8]才意識到 async 函數還是有很多缺陷的。

async 函數的一些缺陷如下:

  • await 關鍵字只能結合 Promise 控制異步;

  • 無法在外界取消一個正在運行中的 async 函數;

我們應當明確,async 函數並非一種讓 generator 更便於使用的語法糖。async 函數只有在結束時,纔會返回的是一個 Promise。我們無法控制其中間狀態,而 generator 返回的是迭代器,迭代器讓你有充分的控制權。關於 async 函數的這種實現方式, getify 在之前還有過下面這樣的吐槽:

This is an unbelievably ,ridiculously stupid idea, in my option. They ought to have async functions return something else that lets you control it and listen to the promise, and it terrible idea what they have come up with but that was happened .

他還推薦如果想讓異步更受控制,我們應該儘可能多使用 generator。

不過話說回來,對於一些一般複雜的異步問題,async 函數其實挺好用的了

async generator

在 ES2018 中引入了 Asynchronous iteration 的概念,我們可以在 async 函數中使用 for await … of 語法,迭代異步對象,可以通過 Symbol.asyncIterator 自定義異步迭代處理邏輯。

在 MDN (參看 for await…of - JavaScript | MDN[9])上還有一種結合 generator 和 async 使用的例子:

async function* streamAsyncIterator(stream) {
  const reader = stream.getReader();
  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) {
        return;
      }
      yield value;
    }
  } finally {
    reader.releaseLock();
  }
}

async function getResponseSize(url) {
  const response = await fetch(url);
  let responseSize = 0;
  for await (const chunk of streamAsyncIterator(response.body)) {
    responseSize += chunk.length;
  }

  console.log(`Reponse Size: ${responseSize} bytes`);
  return responseSize;
}
getResponseSize('https://jsonplaceholder.typicode.com/photos');

async generator 允許 await 和 yield 兩個關鍵字一起使用,await負責獲取值(pull,從其它地方讀取內容),yield 負責輸出值(push 將值輸出),我覺得這還真的是一個非常棒的改進,我們的代碼可讀性更強了,generator 也更容易使用了。

上面講述的所有內容其實都還比較偏向於常規的異步模式,下面要談到的 Observable 則需要我們換一種思維模式來看待異步。

Observable

RxJS 是 Observable 的 Javascript 實現。關於 RxJS ,可講的實在太多了,關於它的書都有好多本。說實話,其實我還沒有在生產環境中用過它,不過最近也看了很多關於它的資料,覺得稍微有些理解其背後的思想,所以在此記錄,如果有不對的地方非常歡迎大家指出。

我們可以從幾個不同的角度來理解 Observable:

角度一:

observable is a collection that arrives over time。

如果我們換個角度看待異步,其實它們就像是時間流中的數據片段,這和我們熟悉的數組很像,我們知道,數組中元素的索引是從小變大的數值,我們大可以開一下腦洞,將異步數據流中的元素的索引看作是時間的先後。這種想法好美。在上文中我們提到過,使用 thunk ,我們是可以把時間抽象出去的,對於數組的操作,我們則再熟悉不過了。

提前放一下下圖,幫我們加深這種理解

observable is a collection that arrives over time

角度二:

Observable is Observer + Iterator。

迭代器模式Iterator 和觀察者模式Observer 是兩種大家更爲熟悉的設計模式 。

理解這兩種設計模式之間的關係也是理解 Observable 的關鍵。

上面兩種模式的區別在於誰佔據主動權:

  • Iterator 消費者佔主動權,消費者說 next 如果有的話,就將獲取下一個值,對於消費者來說,這是一個拉(pull)的過程;

  • Observer 生產者佔主動權,有了新的內容,生產着就自動通知給所有訂閱它的人,對於消費者來說,這是一個推(push)的過程;

web 上有非常多的基於觀察者模式的 APIs,比如說:

  • DOM Events

  • Websockets

  • Server-send Events

  • Node Streams

  • Service Workers

  • setInterval

  • 異步請求 等等

雖然它們都是觀察者模式,但是用法卻並不統一。相對而言迭代器的用法則是統一的。RxJS 實際上就提供了一種辦法將上述 api 轉換爲 observable,而 observable 的返回值其實可以看作是一個可迭代的序列。

下面是一個創建 Observable 的示例:

const Rx = require('rxjs')

const source = Rx.Observable.create(observer => {
  setTimeout(() => {
    observer.next(42)
  }, 500)
  console.log('Observable is started')
})

source.forEach(x => {
  console.log(x)
})

這裏我們使用 Rx.Observable.create 可以很輕鬆的創建了一個 Observable ,其它的一些異步操作,使用 RxJS 也可以用類似的辦法很容易的創建。

角度三

An observable is nothing but an object with a forEach method.

繼續上面的例子,創建的 Observable 的過程並不會執行其內部的函數[10],我們僅僅只是將函數按照一定規則組合起來,返回了一個可迭代序列。observable 是惰性的,只有我們則外部調用 source.forEach 時,其中內容纔會真實的執行。

在統一起來爲 observable 後,我們還可以通過一些簡單的的方法組合和控制它們。

如下是一些常見的方法:

  • map

  • filter

  • concatAll

  • TakeUntil

  • mergeAll

  • switchLatest

  • distchUntilChanged

這裏我們不去講述不同的方法的具體含義,僅僅已switchLatest爲例說明 observable 組合的強大。

switchLatest 意味着切換到最新的 observable,不再去管前一個 observable 後續未完成的操作。舉一個常見的交互爲例,比如說在搜索框中進行搜索時,可以把用戶的每一次輸入都看作一個 observable,每個字符的輸入都會觸發後續的一系列操作,如果用戶連續輸入,通過 switchLatest,我們就可以很容易取消一些可能沒有用的請求。

示例如下:

var searchResultSets = keyPresses
  .throttle(250)
  .map(key => getJSON('/searchResults?q=' + input.value).retry(3))
  .switchLatest()

RxMarbles[11]是一個有助於我們學習這些方法的網站,其用可交互的方式展示了 Observables 相關的多種方法具體的含義,能幫助我們更好的理解上面提到的各種屬性實際上做的事情。

如上圖所示,我們完全可以把橫軸看作時間,時間當然只有一條,但是在同一條時間線上,可能有多種操作流在同時發生,我們異步的本質不就是在處理併發嘛,希望能按照我們預期的順序獲取到結果。通過 Observable ,我們可以方便的使用不同的方法組合和控制異步流。據說通過 RxMarbles 就可以學會一半的 RxJS ,非常推薦你點擊鏈接去看看。值得一提的是 Observable 對象本質上只有一個方法,forEach,其它的方法實際上都是在它的基礎上演變而來。

這種編程模式其實又被稱作 reactive programming。上面提到的 Observables 的組合其實也沒有什麼黑魔法,有人把它比作 Excel 中的單元格。我們知道 Excel 非常強大,我們可以選中 Excel 中的若干單元格進行復雜的運算後,並將結果存儲到另外一個單元格中。隨後如果前面的單元格中任意一個地方的值有所改變,之前得到的結果也會跟着改變。對應到函數之中,其實就是通過 callback,按照一定的規則組件起一個越來越大的等待着被執行的函數。這個函數充分利用了回調和閉包來保證其按照我們的預期行爲來執行。

JS Bin 上有一個縮略版的 RxJS 實現,在 frontend master 上還有一個配套的講解課程[12],如果有興趣可以去看看。

有人說,observable 是可以控制所有異步操作的模式,你可以通過 observable 使用所有的異步 API。關於 observable 還有很多可以聊的內容,比如說 副作用,或者說 hot / cold observable,我缺少 observable 的使用經驗,很多地方的理解可能會有欠缺。下面這些資料,我還只看了一部分,但是覺得還挺好的,推薦你查看閱讀

  • Learn JavaScript Asynchronous Programming - Jafar Husain

  • 深入淺出RxJS

該選擇什麼異步模式

各種異步模式其實是不同的工具,就我看來其實也不存在完全的優劣,應當都有所理解,在正確的時機使用正確的工具。也並非越強大的工具越好,更強大的往往也意味着更大的學習和使用成本。

在上面提到的 RxJS 那門網課中,Jafar Husain 甚至認爲,在瀏覽器中永遠存在着併發,就該優先使用 RxJS,相比較而言 Promise 和 Async 函數,在 node 端會更有用。

在我的大部分工作中,我其實覺得 Promise 就夠用了。不過最近我參與到一個 IM 系統的開發中,前端的交互和邏輯相比較而言還有些複雜,通常一個地方的改變意味着其它幾個地方需要跟着同步改變,在開發中也會明顯感覺到往常習慣的一些模式雖然也可以用,但是覺得代碼寫得並不足夠清晰,也是這就是應當換一種模式處理異步問題的時機了,後面我可能也會嘗試使用其它的模式處理相似的問題能不能讓代碼更爲簡潔。

這裏還有一個對比 Observable 和 Promise 的視頻,也非常推薦你觀看。

有時候保持開放其實還挺難的,我們熟悉的東西會影響我們的思維方式。也許在熟悉了各種異步模式後,遇到了具體的問題,第一時間想到的就會是最合適的方式。

JS 中是怎麼實現異步的

前面我們提到,在 Promise 之前,JavaScript 語言本書是沒有異步這個概念的。比如說我們常用的 setTimeout 等api 實際上是由 JavaScript 的運行環境提供的,其存在於 html Timers 相關標準中。

這一節本來想要再描述一下 EventLoop、Task Queue、Job Queue 等等概念,準備過程中發現下面這一個視頻和一篇文章,他們講解的實際上比我想說的清楚多了,所以我就不再贅述。強烈推薦你點擊閱讀:

Tasks, microtasks, queues and schedules - JakeArchibald.com[13]
What the heck is the event loop anyway? | Philip Roberts | JSConf EU - YouTube[14]

下面是從上面的視頻中截出來的關於 Event loop 的一張示意圖:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章