1. 程式人生 > 程式設計 >在例項中重學JavaScript事件迴圈

在例項中重學JavaScript事件迴圈

單執行緒的JS

眾所周知js是一門單執行緒語言,即同一時間只能做一件事。為什麼js是單執行緒的呢,主要與它的用途有關。

作為瀏覽器指令碼語言,js的主要用途是和使用者互動&操作DOM,我們並不想並行的操作DOM。如果不是單執行緒的話,我們一個執行緒在給DOM節點上新增內容,另一個執行緒卻刪除了這個節點,到底該以哪個為準呢?

所以為了避免複雜性,從一誕生,JavaScript 就是單執行緒。

事件迴圈(event loop)

JS是一門單執行緒語言,意味著程式碼要一行一行的執行。所有任務都要排隊,前一個任務結束,才會執行後一個任務。

但平時大家開發時常用到的ajax,setTimeOut,promise之類的並沒有阻塞程序。如果瀏覽器只有一個js引擎構成,遇到上面這些比較耗時的請求或操作時,瀏覽器就會阻塞住,這肯定不是我們想要的。

其實js單執行緒是指瀏覽器在解釋和執行js程式碼時只有一個執行緒,即js引擎執行緒。但瀏覽器還包括一些其他的執行緒來處理這些非同步的方法,比如Web APIs執行緒,GUI渲染執行緒等。

事件迴圈的處理流程:

JS執行緒依靠呼叫棧來處理執行js程式碼,當遇到一些非同步的操作時,則將其移交給Web APIs,自己繼續往下進行。
Web APIs執行緒則將收到的事件按一定的規則和順序新增到任務佇列裡去。
JS執行緒處理完當前所有任務(即執行棧為空),則去檢查任務佇列裡是否有等待被處理的事件,若有,則取出一個事件回撥放入執行棧中執行。
然後不斷迴圈第三步。

巨集任務與微任務

任務佇列又分為巨集任務佇列和微任務佇列:

  • 巨集任務佇列(macrotask queue):存放的是setTimeout,setInterval,setImmediate,I/O,UI rendering等。
  • 微任務佇列(microtask queue):存放的是Promises,Object.observe,MutationObserver,process.nextTick等。

所以我們細化一下事件迴圈的處理流程(瀏覽器環境):

JS執行緒依靠呼叫棧來處理執行js程式碼,當遇到一些非同步的操作時,則將其移交給Web APIs,自己繼續往下進行。
Web APIs執行緒則將收到的事件按一定的規則和順序新增到任務佇列裡去。巨集任務事件則新增到巨集任務佇列,微任務事件則新增到微任務佇列。

JS執行緒處理完當前所有任務(即執行棧為空),會先去微任務佇列檢查是否有待處理的事件,若有,會將微任務佇列裡的所有事件一件件執行完直到微任務佇列為空,再去巨集任務佇列取出最前面的一個事件執行,執行完這一個巨集任務事件後再去檢查微任務佇列是否有事件待處理。
然後不斷迴圈第三步。

實際需求中重學JavaScript事件迴圈

什麼是JS事件迴圈?

在秋招的時候經常會被問到這個問題,但自己的理解僅限於以上,然後刷過幾道輸出值順序的題目,沒有過業務中的實際應用場景。後來拿到offer後就忘的一乾二淨了,直到畢業入職後開始寫程式碼重新遇到了這才有了更深一步的理解。

背景
使用者上傳多張圖片,前端拿到每張圖片的url和寬高發送給後端。

解決
首先是拿到使用者上傳的檔案,做一些校驗和限制

// 呼叫系統彈框新增圖片的方法
addFile(e) {
 let uploadFiles = e.target.files,self = this;

 self.getListData = []; // 要傳給後端的物件陣列

 self.testFiles(uploadFiles) // 對使用者上傳的檔案做一些校驗和限制
  
 self.loadAll(uploadiles) // 呼叫loadAll方法
},

然後讓我們看一下loadAll,主要是遍歷上傳的這些圖片檔案,然後每一個圖片檔案都呼叫了loadImg

loadAll(files) {
 let promises = [],self = this
 
 // 遍歷檔案流
 files.forEach((file,i) => {
  // 建立物件,push到數組裡
  (self.getListData).push({
   imageUrl: '',});
  
  let eachPromise = self.loadImg(file,i)
  // 儲存當前promise物件
  promises.push(eachPromise)
 })
 
 Promise.all(promises).then(() => {
  //全部完成,向後端傳送請求
 }).catch(err => {
  console.log(err)
 })
},

然後讓我們看一下loadImg,這個方法返回一個Promise物件,主要是為了保證拿到圖片的URL以及在img.onload裡拿到圖片的寬高,因為這兩個事件都是非同步事件。

實際上js主執行緒是不會等待這兩個結果,就會繼續往下執行的。但因為我們在img.onload裡才會把Promise給resolve出去,而loadAll方法裡用了一個Promise.all來等待所有promise都完成,這樣就可以保證向後端傳送請求時所有的圖片的url和寬高都能拿到。

loadImg(file,i) {
 return new Promise(async (resolve,reject) => {
  let self = this
  // 呼叫公司wos服務,拿圖片檔案的url
  let successRes = await _fileUpload.uploadFile(item)
  if(successRes && successRes !== 'error'){
    self.getListData[i]['imageUrl'] = successRes.url
  }
  let img = new Image()
  img.src = successRes.url
  img.onload = () => {
   self.getListData[i]['width'] = img.width
   self.getListData[i]['height'] = img.height
   resolve()
  }
  img.onerror = (e) => {
   reject(e)
  }
 })
}

讓我們想一下如果把loadImg裡拿圖片的url這個操作放到loadAll裡呢?

loadAll(files) {
 let promises = [],self = this
 
 // 遍歷檔案流
 files.forEach(async(file,});
  
  // 呼叫公司wos服務,拿圖片檔案的url
  let successRes = await _fileUpload.uploadFile(item)
  if(successRes && successRes !== 'error'){
    self.getListData[i]['imageUrl'] = successRes.url
  }
  
  let eachPromise = self.loadImg(file,

如果變成這種寫法,因為js的主執行緒執行棧不會等待await返回結果,迴圈裡await _fileUpload.uploadFile(item)這行程式碼後面的內容會被交給Web APIs然後跳出async函式。繼續執行主執行緒,而現在Promise.all的引數是一個空陣列,然後就直接發了請求。但現在並沒有拿到圖片的URL和寬高。

關鍵字await只能使async函式一直等待,執行棧當然不可能停下來等待的,await將其後面的內容包裝成Promise交給Web APIs後,執行棧會跳出async函式繼續執行,直到Promise執行完並返回結果。await只在async函式裡面奏效。

總結

從上面這個需求的實現中,好像對事件迴圈的理解更深刻了!像Promise.then裡和await後面的程式碼都會等待返回結果後再被放入對應事件的任務佇列中等待執行,JS執行緒會繼續向下執行呼叫棧。包括vue中的watch handler也是被先放入了任務佇列裡等待。

所以可知事件迴圈在實際工作中對寫程式碼和優化程式碼都非常重要~如理解有誤請在評論區多多指教。

以上就是在例項中重學JavaScript事件迴圈的詳細內容,更多關於JavaScript 事件迴圈的資料請關注我們其它相關文章!