JS基礎總結(5)—— JS執行機制與EventLoop

lzg9527發表於2020-02-03

前言

農曆2019已經過去,趁著上班前這段時間,整理了一下javascript的基礎知識,在此給大家做下分享,喜歡的大佬們可以給個小贊。 本人github: github.com/Michael-lzg

相關文章:

js 是一門單執行緒語言。 js 引擎有一個主執行緒(main thread)用來解釋和執行 js 程式,實際上還存在其他的執行緒。例如:處理 ajax 請求的執行緒、處理 DOM 事件的執行緒、定時器執行緒、讀寫檔案的執行緒(例如在 node.js 中)等等。這些執行緒可能存在於 js 引擎之內,也可能存在於 js 引擎之外,在此我們不做區分。不妨叫它們工作執行緒。

JS 執行上下文

當程式碼執行時,會產生一個對應的執行環境,在這個環境中,所有變數會被事先提出來(變數提升),有的直接賦值,有的為預設值 undefined,程式碼從上往下開始執行,就叫做執行上下文。

例子 1: 變數提升

foo // undefined
var foo = function() {
  console.log('foo1')
}

foo() // foo1,foo賦值

var foo = function() {
  console.log('foo2')
}

foo() // foo2,foo重新賦值
複製程式碼

例子 2:函式提升

foo() // foo2
function foo() {
  console.log('foo1')
}

foo() // foo2

function foo() {
  console.log('foo2')
}

foo() // foo2
複製程式碼

例子 3:宣告優先順序,函式 > 變數

foo() // foo2
var foo = function() {
  console.log('foo1')
}

foo() // foo1,foo重新賦值

function foo() {
  console.log('foo2')
}

foo() // foo1
複製程式碼

執行環境

在 JavaScript 的世界裡,執行環境有三種,分別是:

  1. 全域性環境:程式碼首先進入的環境
  2. 函式環境:函式被呼叫時執行的環境
  3. eval 函式:(不常用)

執行上下文特點

  1. 單執行緒,在主程式上執行
  2. 同步執行,從上往下按順序執行
  3. 全域性上下文只有一個,瀏覽器關閉時會被彈出棧
  4. 函式的執行上下文沒有數目限制
  5. 函式每被呼叫一次,都會產生一個新的執行上下文環境

執行上下文棧

執行全域性程式碼時,會產生一個執行上下文環境,每次呼叫函式都又會產生執行上下文環境。當函式呼叫完成時,這個上下文環境以及其中的資料都會被消除,再重新回到全域性上下文環境。處於活動狀態的執行上下文環境只有一個。

其實這是一個壓棧出棧的過程——執行上下文棧。

JS基礎總結(5)—— JS執行機制與EventLoop

var // 1.進入全域性上下文環境
  a = 10,
  fn,
  bar = function(x) {
    var b = 20
    fn(x + b) // 3.進入fn上下文環境
  }

fn = function(y) {
  var c = 20
  console.log(y + c)
}

bar(5) // 2.進入bar上下文環境
複製程式碼

執行上下文生命週期

JS基礎總結(5)—— JS執行機制與EventLoop

1、建立階段

  • 生成變數物件
  • 建立作用域鏈
  • 確定 this 指向

2、執行階段

  • 變數賦值
  • 函式引用
  • 執行其他程式碼

3、銷燬階段

  • 執行完畢出棧,等待回收被銷燬

javascript 事件迴圈

  • 同步和非同步任務分別進入不同的執行"場所",同步的進入主執行緒,非同步的進入 Event Table 並註冊函式。
  • 當指定的事情完成時,Event Table 會將這個函式移入 Event Queue
  • 主執行緒內的任務執行完畢為空,會去 Event Queue 讀取對應的函式,進入主執行緒執行。
  • 上述過程會不斷重複,也就是常說的 Event Loop(事件迴圈)。

JS基礎總結(5)—— JS執行機制與EventLoop

同步任務和非同步任務,我們對任務有更精細的定義:

macro-task(巨集任務):

可以理解是每次執行棧執行的程式碼就是一個巨集任務(包括每次從事件佇列中獲取一個事件回撥並放到執行棧中執行)。

瀏覽器為了能夠使得 JS 內部(macro)task與 DOM 任務能夠有序執行,會在一個(macro)task執行結束後,在下一個(macro)task執行開始前,對頁面進行重新渲染。

(macro)task 主要包含:script(整體程式碼)、setTimeoutsetInterval、I/O、UI 互動事件、postMessageMessageChannel、setImmediate(Node.js 環境)

micro-task(微任務):

可以理解是在當前 task 執行結束後立即執行的任務。也就是說,在當前 task 任務後,下一個 task 之前,在渲染之前。所以它的響應速度相比 setTimeout(setTimeout 是 task)會更快,因為無需等渲染。也就是說,在某一個 macrotask 執行完後,就會將在它執行期間產生的所有 microtask 都執行完畢(在渲染前)。

microtask 主要包含:Promise.thenMutaionObserverprocess.nextTick(Node.js 環境)

舉個例子

我們來分析一段較複雜的程式碼,看看你是否真的掌握了 js 的執行機制:

console.log('1')

setTimeout(function() {
  console.log('2')
  process.nextTick(function() {
    console.log('3')
  })
  new Promise(function(resolve) {
    console.log('4')
    resolve()
  }).then(function() {
    console.log('5')
  })
})
process.nextTick(function() {
  console.log('6')
})
new Promise(function(resolve) {
  console.log('7')
  resolve()
}).then(function() {
  console.log('8')
})

setTimeout(function() {
  console.log('9')
  process.nextTick(function() {
    console.log('10')
  })
  new Promise(function(resolve) {
    console.log('11')
    resolve()
  }).then(function() {
    console.log('12')
  })
})

// 1,7,8,2,4,5,6,3,9,11,12,10
複製程式碼

再來一段

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}
console.log('script start')
setTimeout(function() {
  console.log('setTimeout')
}, 0)
async1()
new Promise(function(resolve) {
  console.log('promise1')
  resolve()
}).then(function() {
  console.log('promise2')
})
console.log('script end')

// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
複製程式碼

解決非同步問題的方法

  1. 回撥函式
ajax('XXX1', () => {
  // callback 函式體
  ajax('XXX2', () => {
    // callback 函式體
    ajax('XXX3', () => {
      // callback 函式體
    })
  })
})
複製程式碼
  • 優點:解決了同步的問題
  • 缺點:回撥地獄,不能用 try catch 捕獲錯誤,不能 return

2、Promise 為了解決 callback 的問題而產生。

Promise 實現了鏈式呼叫,也就是說每次 then 後返回的都是一個全新 Promise,如果我們在 then 中 return ,return 的結果會被 Promise.resolve() 包裝。

  • 優點:解決了回撥地獄的問題
  • 缺點:無法取消 Promise ,錯誤需要通過回撥函式來捕獲

3、Async/await

  • 優點是:程式碼清晰,不用像 Promise 寫一大堆 then 鏈,處理了回撥地獄的問題
  • 缺點:await 將非同步程式碼改造成同步程式碼,如果多個非同步操作沒有依賴性而使用 await 會導致效能上的降低。

總結

  • javascript 是一門單執行緒語言
  • Event Loop 是 javascript 的執行機制

node 環境和瀏覽器的區別

1、全域性環境下 this 的指向

  • node 中 this 指向 global
  • 瀏覽器中 this 指向 window
  • 這就是為什麼 underscore 中一上來就定義了一 root;

瀏覽器中的 window 下封裝了不少的 API 比如 alert 、document、location、history 等等還有很多, 我門就不能在 node 環境中 xxx();或 window.xxx();了。因為這些 API 是瀏覽器級別的封裝,存 javascript 中是沒有的。當然 node 中也提供了不少 node 特有的 API。

2、js 引擎

  • 在瀏覽器中不同的瀏覽器廠商提供了不同的瀏覽器核心,瀏覽器依賴這些核心解釋折我們編寫的 js。但是考慮到不同核心的少量差異,我們需要對應相容性好在有一些優秀的庫幫助我們處理這個問題。比如 jquery、underscore 等等。

  • nodejs 是基於 Chrome's JavaScript runtime,也就是說,實際上它是對 GoogleV8 引擎(應用於 Google Chrome 瀏覽器)進行了封裝。V8 引 擎執行 Javascript 的速度非常快,效能非常好。

3、DOM 操作

  • 瀏覽器中的 js 大多數情況下是在直接或間接(一些虛擬 DOM 的庫和框架)的操作 DOM。因為瀏覽器中的程式碼主要是在表現層工作。
  • node 是一門服務端技術。沒有一個前臺頁面,所以我們不會再 node 中操作 DOM。

4、I/O 讀寫

與瀏覽器不同,我們需要像其他服務端技術一樣讀寫檔案,nodejs 提供了比較方便的元件。而瀏覽器(確保相容性的)想在頁面中直接開啟一個本地的圖片就麻煩了好多(別和我說這還不簡單,相對路徑。。。。。。試試就知道了要麼找個庫要麼二進位制流,要麼上傳上去有了網路地址在顯示。不然人家為什麼要搞一個 js 庫呢),而這一切 node 都用一個元件搞定了。

5、模組載入

  • javascript 有個特點,就是原生沒提供包引用的 API 一次性把要載入的東西全執行一遍,這裡就要看各位閉包的功力了。所用東西都在一起,沒有分而治之,搞的特別沒有邏輯性和複用性。如果頁面簡單或網站當然我們可以通過一些 AMD、CMD 的 js 庫(比如 requireJS 和 seaJS)搞定事實上很多大型網站都是這麼幹的。

  • nodeJS 中提供了 CMD 的模組載入的 API,如果你用過 seaJS,那麼應該上手很快。node 還提供了 npm 這種包管理工具,能更有效方便的管理我們飲用的庫

推薦文章

關注的我的公眾號不定期分享前端知識,與您一起進步!

JS基礎總結(5)—— JS執行機制與EventLoop

相關文章