【修正模型】4-3 事件循环(Event Loop)与任务队列(Job Queue)

同步(Synchronous)与非同步(Asynchronous)

在理解执行上下文与呼叫堆叠之後,眼尖的读者应该会发现一个问题,那就是既然程序码是几乎是一行一行解读的情况下,那假如我在资源请求(request)的时候载入一个非常大的档案,那麽我的程序不就停在这里了吗?

function getPageData() {
  var result;
  fetch('https://raw.githubusercontent.com/shawnlin0201/ithelp-2020/main/3-4-1-fetch-request.json')
    .then(res => res.json())
    .then(res => {
        result = res
    })
   
  return result
}

var data = getPageData()

以上方程序码为例,假设我在程序码中执行了这个 getPageData 函式,并透过 fetch 向某个服务器请求资源,但如果依照先前解析的概念来说,这时我们得等待最後资源请求的内容回来之後,接着才会离开 getPageData 当下所创造出来的执行上下文中,并把结果交还给 data

然而实际上执行的时候你会发现无论你再怎麽请求,data 内永远记录的是 undefined

而当你想透过 console.log 来确认资料是否会回传时,却发现资料是有回来的:

function getPageData() {
  var result;
  fetch('https://raw.githubusercontent.com/shawnlin0201/ithelp-2020/main/3-4-1-fetch-request.json')
    .then(res => res.json())
    .then(res => {
        console.log('res', res) // 有资料
        result = res
    })
   
  return result
}

var data = getPageData() // 仍然是 undefined

这时你可能会开始怀疑之前所理解的那套认知历程不太对劲,实际上这是因为在 JavaScript 中存在着 非同步 的作法在里面。

而同步与非同步的概念所指的是在执行程序码的时候主要分为两种执行结果:会 立即返回资料 的即是同步的程序码;相对的,不会立即返回资料 的即是非同步的程序码。

并且其中最重要的概念是 立即与不立即的区别并不是以秒数来决定的,而是这些程序运用到了浏览器中的 WEB APIs 的一些相关机制:

  • 计时器(Timer):setTimeoutsetInterval
  • 资源请求、等待回应类:XMLHttpRequestfetchpromise
  • 使用者操作类:键盘事件、滑鼠点击事件等

所以即便你使用计时器输入等待 0 秒时,该段程序码仍然被视为需要被非同步所执行的程序码:

setTimeout(function(){
  
  // 在这个区块程序码中所执行的程序会采用非同步的方法执行

},'0') // 等待零秒

同步与非同步的执行

基本上在浏览器中所有的程序码只会在 主线程中执行,而一般执行的过程就好比我们在执行上下文中(Execution Context)与呼叫堆叠(Call Stack)的章节时所讲的一样。

然而被非同步执行的程序码,被引擎解析到的当下,会先排入在浏览器中各自属於的队伍(如 setTimeout 会有个 watcher 的队伍):

接着达到条件(例如计时的时间到或是请求的资源回应时),就会被排入一个叫做任务队列(Job Queue)或称事件队列(Event Queue)的队伍当中。

最後当整个主程序的程序码都执行完毕时,这时才会开始从任务队列中陆续的将程序码放回主线程当中,接着依照主线程执行程序码的方式继续执行,等到下次主线程又没有程序码时,就会继续轮询各个事件处理器与任务队列是否还有东西要执行。

若你对於整个过程的视觉化处理有兴趣的话,可以参考 这个专案 所做的内容。

延伸阅读:Microtasks & Marcotask

上方所介绍的任务队列(Job Queue)实际上还可以分为 MarcotaskMircotask 两个队伍,而各自所排入的事件分别为:

  • Marcotasks:setTimeoutsetInterval、使用者操作、UI 渲染
  • Mircotasks:PromisesMutationObserver

并且在执行顺序上 Mircotasks 优於 Marcotasks 中的队伍,且会等到 Mircotasks 都执行完了才会回来检查 Marcotasks

console.log('start')

setTimeout(() => {
  console.log('setTimeout1')
})
Promise.resolve()
  .then(()=> {
    console.log('Promise1')
  })
  .then(()=> {
    console.log('Promise2')
  })
setTimeout(() => {
  console.log('setTimeout2')
})
console.log('end')

执行完毕会显示:

'start'
'end'
'Promise1'
'Promise2'
'setTimeout1'
'setTimeout2'

非同步的处理方式

现在我们除了会分辨哪些程序码是被非同步的执行後,现在我们要回头来解决上面一开始所遇到的问题。

function getPageData() {
  var result;
  fetch('https://raw.githubusercontent.com/shawnlin0201/ithelp-2020/main/3-4-1-fetch-request.json')
    .then(res => res.json())
    .then(res => {
        console.log('res', res) // 有资料
        result = res
    })
   
  return result
}

var data = getPageData() // 仍然是 undefined

既然我们现在知道非同步的程序码会排入另一个队列中再回来执行,因此我们可以藉由要把後续做的事情放在需要被非同步执行的程序码中,最後即可跟着那些程序码一起被执行:

function getPageData() {
  fetch('...')
    .then(res => res.json())
    .then(res => {
      // 在这里做後续的事情
    })
}

但是这样做不仅会让该函式做的事情被限缩,其他人也要使用这个函式取得资源,就会被迫也要跟着处理後续的问题,因此较好的作法可以藉由回呼函式将内容抛出:

function getPageData(callback) {
  fetch('...')
    .then(res => res.json())
    .then(res => {
      callback(res)
    })
}

如此一来我们只要有使用 getPageData 的需求,就可以透过传入函式来决定我们取到资料後要做什麽事情:

getPageData(function(res){
  console.log('Get Responses', res)
  // do something
})

当然若你想使用更新的语法如 Async/Await 也同样可以处理,因为非同步的机制还是一致的。而到了这里其实你已经将 JavaScript 大致上的概念都走了一轮,剩下的部分基本上就可以依照实作中的需求去搜寻,必要时再去做深入的了解与使用,明天我们将回过头来检视整个过去所学到的内容。


<<:  Flutter学习Day4 Widget 观念 StatefulWidget

>>:  【30天Lua重拾笔记34】番外篇: Fengari - 一个JS实现的Lua,运行Lua在浏览器内吧!

ISBN Barcode Scanner实作 Day 20

今天实作将Barcode Scanner结合在我的Button上 根据昨天的AVFoundation...

[Day 30] - 终成行男

呼,想当初在铁人赛开赛前还在犹豫到底要不要开赛呢? 参赛後是要写什麽主题呢? 一探 React Na...

Day 26: Server我也不要了,Mock Ktor 环境

Keyword: Ktor MockEngine, Unit Test 直到27日,完成KMM的测试...

【day12】连续上班日做便当

终於要回到正轨了 其实参加这个系列 主要是期许自己 在忙碌的工作之余 还可以每天现做便当 自从上个中...

jquery实例演练02

JQuery DOM元素操作 透过text()、Val()来显示文本内容 范例一 <p id=...