目錄
JavaScript的單執行緒機制
任務佇列
事件與回呼函數
事件
回呼函數
Event Loop
計時器
Node.js的Event Loop
Promise
首頁 web前端 js教程 js執行機制實例詳解

js執行機制實例詳解

Mar 14, 2018 pm 05:21 PM
javascript 實例 詳解

想要理解JavaScript的運作機制,需要分別深刻理解幾個點:JavaScript的單執行緒機制、任務佇列(同步任務和非同步任務)、事件和回呼函數、定時器、Event Loop(事件循環)。

JavaScript的單執行緒機制

JavaScript的一個語言特性(也是這門語言的核心)就是單執行緒。單線程簡單地說就是同一時間只能做一件事,當有多個任務時,只能按照一個順序一個完成了再執行下一個。

JavaScript的單執行緒與它的語言用途是有關的。作為一門瀏覽器腳本語言,JavaScript的主要用途是完成使用者互動、操作DOM。這決定了它只能是單線程,否則會導致複雜的同步問題。

設想JavaScript同時有兩個線程,一個線程需要在某個DOM節點上添加內容,而另一個線程的操作是刪除了這個節點,那麼瀏覽器應該以誰為準呢?

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

為了提高CPU的使用率,HTML5提出Web Worker標準,允許JavaScript腳本建立多個線程,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以這個標準並沒有改變JavaScript單執行緒的本質。

任務佇列

一個接一個地完成任務也就意味著待完成的任務是需要排隊的,那麼為什麼會需要排隊呢?

通常排隊有以下兩種原因:

  • 任務運算量過大,CPU處於忙碌狀態;

  • 任務所需的東西未準備好所以無法繼續執行,導致CPU閒置,等待輸入輸出設備(I/O設備)。

    例如有的任務你需要Ajax取得到資料才能往下執行


 由此JavaScript的設計者也意識到,這時完全可以先執行後面已經就緒的任務來提高運作效率,也就是把等待中的任務先掛起放到一邊,等得到需要的東西再執行。就好比接電話時對方離開了一下,這時正好有另一個來電,於是你便把當前通話掛起,等那個通話結束後,再連回之前的通話。 所以也就出現了同步和非同步的概念,任務也被分成了兩種,一種是同步任務(Synchronous),另一種是非同步任務(Asynchronous)。

  • 同步任務:需要執行的任務在主執行緒上排隊,一個接一個,前一個完成了再執行下一個

  • 異步任務:沒有馬上被執行但需要執行的任務,存放在「任務佇列」(task queue)中,「任務佇列」會通知主執行緒什麼時候哪個非同步任務可以執行,然後這個任務就會進入主執行緒並被執行。

    所有的同步執行都可以看作是沒有非同步任務的非同步執行


 具體來說,非同步執行如下:

  • (1)所有同步任務都在主執行緒上執行,形成一個執行堆疊(execution context stack)。

    也就是所有能馬上執行的任務都在主執行緒上排好了隊,一個接一個的被執行。

  • (2)主執行緒之外,還有一個「任務佇列」(task queue)。只要非同步任務有了運行結果,就在「任務佇列」之中放置一個事件。

    也就是說每個非同步任務準備好了就會立一個唯一的flag,這個flag用來識別對應的非同步任務。

  • (3)一旦“執行堆疊”中的所有同步任務執行完畢,系統就會讀取“任務佇列”,看看裡面有哪些事件。那些對應的非同步任務,就結束等待狀態,進入執行堆疊開始被執行。

    也就是主執行緒把之前的任務做完了之後,就會來看「任務佇列」中的flag,來把對應的非同步任務打包來執行。

  • (4)主執行緒不斷重複以上三步驟。

    #只要主執行緒空了,就會去讀取「任務佇列」。這個過程會不斷重複,這就是JavaScript的運作機制。

那怎麼知道主執行緒執行端為空啊? js引擎存在monitoring process進程,會持續不斷的檢查主執行緒執行堆疊是否為空,一旦為空,就會去Event Queue檢查是否有等待被呼​​叫的函數。

下面用一

張導圖來說明主執行緒和任務佇列。

js執行機制實例詳解

導圖要表達的內容用文字來表達的話:

  • 同步和非同步任務分別進入不同的執行”場所”,同步的進入主線程,非同步的進入Event Table並註冊函數。

  • 當指定的事情完成時,Event Table會將這個函數移入Event Queue。

  • 主執行緒內的任務執行完畢為空,會去Event Queue讀取對應的函數,進入主執行緒執行。

  • 上述過程會不斷重複,也就是常說的Event Loop(事件循環)。

事件與回呼函數

事件

「任務佇列」是一個事件的佇列(也可以理解成是訊息的佇列),IO設備完成一項任務,就會在「任務佇列」中新增一個事件,表示相關的非同步任務可以進入「執行堆疊」。接著主執行緒讀取“任務佇列”,查看裡面有哪些事件。

「任務佇列」中的事件,除了IO設備的事件以外,還包括一些使用者產生的事件(例如滑鼠點擊、頁面滾動等等)。只要指定過回呼函數,這些事件發生時就會進入“任務佇列”,等待主執行緒讀取。

回呼函數

所謂「回呼函數」(callback),就是那些會被主執行緒掛起來的程式碼。非同步任務必須指定回呼函數,當主執行緒開始執行非同步任務,就是執行對應的回呼函數。

「任務佇列」是一個先進先出的資料結構,排在前面的事件,優先被主執行緒讀取。主執行緒的讀取過程基本上是自動的,只要執行堆疊一清空,「任務佇列」上第一位的事件就會自動進入主執行緒。但是,如果包含“定時器”,主執行緒首先要檢查執行時間,某些事件只有到了規定的時間,才能返回主執行緒。

Event Loop

主執行緒從「任務佇列」讀取事件,這個過程是循環不斷的,所以整個的運行機制又稱為「Event Loop」(事件循環)。

為了更好地理解Event Loop,下面參考Philip Roberts的演講中的一張圖。

Event Loop

上圖中,主執行緒在運行時,產生了heap(堆疊)和stack(堆疊),堆疊中的程式碼呼叫各種外部API,並在“任務佇列」中加入各種事件(click,load,done)。當堆疊中的程式碼執行完畢,主執行緒就會讀取“任務佇列”,並依序執行那些事件所對應的回呼函數。

執行堆疊中的程式碼(同步任務),總是在讀取「任務佇列」(非同步任務)之前執行。

let data = [];
$.ajax({    url:www.javascript.com,    data:data,    success:() => {        console.log('发送成功!');
    }
})console.log('代码执行结束');
登入後複製

上面是一段簡易的ajax請求程式碼:

  • ajax進入Event Table,註冊回呼函數success

  • 執行console.log('程式碼執行結束')

  • ajax事件完成,回呼函數success進入Event Queue。

  • 主執行緒從Event Queue讀取回呼函數success並執行。

計時器

除了放置非同步任務的事件,「任務佇列」還可以放置定時事件,也就是指定某些程式碼在多少時間之後執行。這叫做定時器(timer)功能,也就是定時執行的程式碼。

SetTimeout()setInterval()可以用來註冊在指定時間之後單次或重複呼叫的函數,它們的內部運作機製完全一樣,區別在於前者指定的程式碼是一次執行,後者會在指定毫秒數的間隔裡重複呼叫:

setInterval(updateClock, 60000); //60秒调用一次updateClock()
登入後複製

因為它們都是客戶端JavaScript中重要的全域函數,所以定義為Window物件的方法。

但作為通用函數,其實不會對視窗做什麼事情。

Window物件的setTImeout()方法用來實作一個函數在指定的毫秒數之後運行。所以它接受兩個參數,第一個是回呼函數,第二個是推遲執行的毫秒。 setTimeout()setInterval()回傳一個值,這個值可以傳遞給clearTimeout()用來取消這個函數的執行。

console.log(1);
setTimeout(function(){console.log(2);}, 1000);console.log(3);
登入後複製

上面程式碼的執行結果是1,3,2,因為setTimeout()將第二行延後到1000毫秒之後執行。

如果將setTimeout()的第二個參數設為0,就表示目前程式碼執行完(執行堆疊清空)以後,立即執行(0毫秒間隔)指定的回呼函數。

setTimeout(function(){console.log(1);}, 0);console.log(2)
登入後複製

上面程式碼的執行結果總是2,1,因為只有在執行完第二行以後,系統才會執行「任務佇列」中的回呼函數。

總之,setTimeout(fn,0)的意思是,指定某個任務在主執行緒最早可得的空閒時間執行,也就是盡可能提早執行。它在「任務佇列」的尾部新增一個事件,因此要等到同步任務和「任務佇列」現有的事件都處理完,才會執行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。

需要注意的是,setTimeout()只是将事件插入了“任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证回调函数一定会在setTimeout()指定的时间执行。

由于历史原因,setTimeout()setInterval()的第一个参数可以作为字符串传入。如果这么做,那这个字符串会在指定的超时时间或间隔之后进行求值(相当于执行eval())。

Node.js的Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

Node.js的运行机制如下。

  • (1)V8引擎解析JavaScript脚本。

  • (2)解析后的代码,调用Node API。

  • (3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

  • (4)V8引擎再将结果返回给用户。

除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与”任务队列”有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对”任务队列”的理解。

process.nextTick方法可以在当前”执行栈”的尾部—-下一次Event Loop(主线程读取”任务队列”)之前—-触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前”任务队列”的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。请看下面的例子

process.nextTick(function A() {console.log(1);process.nextTick(function B(){console.log(2);});});
setTimeout(function timeout() {console.log('TIMEOUT FIRED');
}, 0)// 1// 2// TIMEOUT FIRED
登入後複製

上面代码中,由于process.nextTick方法指定的回调函数,总是在当前”执行栈”的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前”执行栈”执行。

现在,再看setImmediate。

setImmediate(function A() {console.log(1);
setImmediate(function B(){console.log(2);});});
setTimeout(function timeout() {console.log('TIMEOUT FIRED');
}, 0);
登入後複製

上面代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1–TIMEOUT FIRED–2,也可能是TIMEOUT FIRED–1–2。

令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。

setImmediate(function (){setImmediate(function A() {console.log(1);
setImmediate(function B(){console.log(2);});});
setTimeout(function timeout() {console.log('TIMEOUT FIRED');
}, 0);
}); 
// 1 // TIMEOUT FIRED // 2
登入後複製

上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1–TIMEOUT FIRED–2,这时函数A一定在timeout前面触发。至于2排在TIMEOUT FIRED的后面(即函数B在timeout后面触发),是因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。

我们由此得到了process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前”执行栈”一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取”事件队列”!

process.nextTick(function foo() {process.nextTick(foo);
});
登入後複製

事实上,现在要是你写出递归的process.nextTick,Node.js会抛出一个警告,要求你改成setImmediate。

另外,由于process.nextTick指定的回调函数是在本次”事件循环”触发,而setImmediate指定的是在下次”事件循环”触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查”任务队列”)。

Promise

除了广义的同步任务和异步任务,任务还有更精细的定义:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval

  • micro-task(微任务):Promise,process.nextTick

事件循环,宏任务,微任务的关系如图所示:

按照宏任务和微任务这种分类方式,JS的执行机制是

  • 执行一个宏任务,过程中如果遇到微任务,就将其放到微任务的【事件队列】里

  • 当前宏任务执行完成后,会查看微任务的【事件队列】,并将里面全部的微任务依次执行完

请看下面的例子:

setTimeout(function(){
     console.log('定时器开始啦')
 });

 new Promise(function(resolve){
     console.log(&#39;马上执行for循环啦&#39;);     for(var i = 0; i < 10000; i++){
         i == 99 && resolve();
     }
 }).then(function(){
     console.log(&#39;执行then函数啦&#39;)
 }); console.log(&#39;代码执行结束&#39;);
登入後複製
  • 首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里

  • 遇到 new Promise直接执行,打印”马上执行for循环啦”

  • 遇到then方法,是微任务,将其放到微任务的【队列里】

  • 打印 “代码执行结束”

  • 本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印”执行then函数啦”

  • 到此,本轮的event loop 全部完成。

  • 下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印”定时器开始啦”

所以最后的执行顺序是【马上执行for循环啦 — 代码执行结束 — 执行then函数啦 — 定时器开始啦】

我们来分析一段较复杂的代码,看看你是否真的掌握了js的执行机制:

console.log(&#39;1&#39;);

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

setTimeout(function() {
    console.log(&#39;9&#39;);    process.nextTick(function() {
        console.log(&#39;10&#39;);
    })
    new Promise(function(resolve) {
        console.log(&#39;11&#39;);
        resolve();
    }).then(function() {
        console.log(&#39;12&#39;)
    })
})
登入後複製

第一轮事件循环流程分析如下:

  • 整体script作为第一个宏任务进入主线程,遇到console.log,输出1。

  • 遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1

  • 遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1

  • 遇到Promisenew Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1

  • 又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2

宏任务Event Queue 微任务Event Queue
setTimeout1 process1
setTimeout2 then1

*   上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。

  • 我们发现了process1then1两个微任务。

  • 执行process1,输出6。

  • 执行then1,输出8。

好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:

  • 首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2

宏任务Event Queue 微任务Event Queue
setTimeout2 process2

then2

*   第二轮事件循环宏任务结束,我们发现有process2then2两个微任务可以执行。
*   输出3。
*   输出5。
*   第二轮事件循环结束,第二轮输出2,4,3,5。
*   第三轮事件循环开始,此时只剩setTimeout2了,执行。
*   直接输出9。
*   将process.nextTick()分发到微任务Event Queue中。记为process3
*   直接执行new Promise,输出11。
*   将then分发到微任务Event Queue中,记为then3

宏任务Event Queue 微任务Event Queue

process3

then3

*   第三轮事件循环宏任务执行结束,执行两个微任务process3then3
*   输出10。
*   输出12。
*   第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。
(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)

以上是js執行機制實例詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1422
52
Laravel 教程
1316
25
PHP教程
1266
29
C# 教程
1239
24
Win11管理員權限取得詳解 Win11管理員權限取得詳解 Mar 08, 2024 pm 03:06 PM

Windows作業系統是全球最受歡迎的作業系統之一,其新版本Win11備受矚目。在Win11系統中,管理員權限的取得是一個重要的操作,管理員權限可以讓使用者對系統進行更多的操作和設定。本文將詳細介紹在Win11系統中如何取得管理員權限,以及如何有效地管理權限。在Win11系統中,管理員權限分為本機管理員和網域管理員兩種。本機管理員是指具有對本機電腦的完全管理權限

Oracle SQL中的除法運算詳解 Oracle SQL中的除法運算詳解 Mar 10, 2024 am 09:51 AM

OracleSQL中的除法運算詳解在OracleSQL中,除法運算是一種常見且重要的數學運算運算,用來計算兩個數相除的結果。除法在資料庫查詢中經常用到,因此了解OracleSQL中的除法運算及其用法是資料庫開發人員必備的技能之一。本文將詳細討論OracleSQL中除法運算的相關知識,並提供具體的程式碼範例供讀者參考。一、OracleSQL中的除法運算

linux系統呼叫system()函數詳解 linux系統呼叫system()函數詳解 Feb 22, 2024 pm 08:21 PM

Linux系統呼叫system()函數詳解系統呼叫是Linux作業系統中非常重要的一部分,它提供了一種與系統核心互動的方式。其中,system()函數是常用的系統呼叫函數之一。本文將詳細介紹system()函數的使用方法,並提供對應的程式碼範例。系統呼叫的基本概念系統呼叫是使用者程式與作業系統核心互動的一種方式。使用者程式透過呼叫系統呼叫函數來請求作業系統

PHP模運算子的作用及用法詳解 PHP模運算子的作用及用法詳解 Mar 19, 2024 pm 04:33 PM

PHP中的模運算子(%)是用來取得兩個數值相除的餘數的。在本文中,我們將詳細討論模運算子的作用及用法,並提供具體的程式碼範例來幫助讀者更好地理解。 1.模運算子的作用在數學中,當我們將一個整數除以另一個整數時,就會得到一個商和一個餘數。例如,當我們將10除以3時,商數為3,餘數為1。模運算子就是用來取得這個餘數的。 2.模運算子的用法在PHP中,使用%符號來表示模

Linux的curl指令詳解 Linux的curl指令詳解 Feb 21, 2024 pm 10:33 PM

Linux的curl命令詳解摘要:curl是一種強大的命令列工具,用於與伺服器進行資料通訊。本文將介紹curl指令的基本用法,並提供實際的程式碼範例,幫助讀者更好地理解和應用該指令。一、curl是什麼? curl是命令列工具,用於發送和接收各種網路請求。它支援多種協議,如HTTP、FTP、TELNET等,並提供了豐富的功能,如檔案上傳、檔案下載、資料傳輸、代

深入了解Promise.resolve() 深入了解Promise.resolve() Feb 18, 2024 pm 07:13 PM

Promise.resolve()詳解,需要具體程式碼範例Promise是JavaScript中一種用來處理非同步操作的機制。在實際開發中,常常需要處理一些需要依序執行的非同步任務,而Promise.resolve()方法就是用來傳回一個已經Fulfilled狀態的Promise物件。 Promise.resolve()是Promise類別的靜態方法,它接受一個

numpy版本查詢方法詳解 numpy版本查詢方法詳解 Jan 19, 2024 am 08:20 AM

Numpy是一款Python科學計算庫,提供了豐富的陣列操作函數與工具。升級Numpy版本時需要查詢目前版本以確保相容性,本文將詳細介紹Numpy版本查詢的方法,並提供具體的程式碼範例。方法一:使用Python程式碼查詢Numpy版本使用Python程式碼可以輕鬆查詢Numpy的版本,以下是實作方法和範例程式碼:importnumpyasnpprint(np

學習Golang指標轉換的最佳實務範例 學習Golang指標轉換的最佳實務範例 Feb 24, 2024 pm 03:51 PM

Golang是一門功能強大且高效的程式語言,可用於開發各種應用程式和服務。在Golang中,指標是一種非常重要的概念,它可以幫助我們更靈活和有效率地操作資料。指標轉換是指在不同類型之間進行指標操作的過程,本文將透過具體的實例來學習Golang中指標轉換的最佳實踐。 1.基本概念在Golang中,每個變數都有一個位址,位址就是變數在記憶體中的位置。

See all articles