搜尋
首頁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
es6数组怎么去掉重复并且重新排序es6数组怎么去掉重复并且重新排序May 05, 2022 pm 07:08 PM

去掉重复并排序的方法:1、使用“Array.from(new Set(arr))”或者“[…new Set(arr)]”语句,去掉数组中的重复元素,返回去重后的新数组;2、利用sort()对去重数组进行排序,语法“去重数组.sort()”。

JavaScript的Symbol类型、隐藏属性及全局注册表详解JavaScript的Symbol类型、隐藏属性及全局注册表详解Jun 02, 2022 am 11:50 AM

本篇文章给大家带来了关于JavaScript的相关知识,其中主要介绍了关于Symbol类型、隐藏属性及全局注册表的相关问题,包括了Symbol类型的描述、Symbol不会隐式转字符串等问题,下面一起来看一下,希望对大家有帮助。

原来利用纯CSS也能实现文字轮播与图片轮播!原来利用纯CSS也能实现文字轮播与图片轮播!Jun 10, 2022 pm 01:00 PM

怎么制作文字轮播与图片轮播?大家第一想到的是不是利用js,其实利用纯CSS也能实现文字轮播与图片轮播,下面来看看实现方法,希望对大家有所帮助!

JavaScript对象的构造函数和new操作符(实例详解)JavaScript对象的构造函数和new操作符(实例详解)May 10, 2022 pm 06:16 PM

本篇文章给大家带来了关于JavaScript的相关知识,其中主要介绍了关于对象的构造函数和new操作符,构造函数是所有对象的成员方法中,最早被调用的那个,下面一起来看一下吧,希望对大家有帮助。

javascript怎么移除元素点击事件javascript怎么移除元素点击事件Apr 11, 2022 pm 04:51 PM

方法:1、利用“点击元素对象.unbind("click");”方法,该方法可以移除被选元素的事件处理程序;2、利用“点击元素对象.off("click");”方法,该方法可以移除通过on()方法添加的事件处理程序。

JavaScript面向对象详细解析之属性描述符JavaScript面向对象详细解析之属性描述符May 27, 2022 pm 05:29 PM

本篇文章给大家带来了关于JavaScript的相关知识,其中主要介绍了关于面向对象的相关问题,包括了属性描述符、数据描述符、存取描述符等等内容,下面一起来看一下,希望对大家有帮助。

foreach是es6里的吗foreach是es6里的吗May 05, 2022 pm 05:59 PM

foreach不是es6的方法。foreach是es3中一个遍历数组的方法,可以调用数组的每个元素,并将元素传给回调函数进行处理,语法“array.forEach(function(当前元素,索引,数组){...})”;该方法不处理空数组。

整理总结JavaScript常见的BOM操作整理总结JavaScript常见的BOM操作Jun 01, 2022 am 11:43 AM

本篇文章给大家带来了关于JavaScript的相关知识,其中主要介绍了关于BOM操作的相关问题,包括了window对象的常见事件、JavaScript执行机制等等相关内容,下面一起来看一下,希望对大家有帮助。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

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

AI Clothes Remover

AI Clothes Remover

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

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
2 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
2 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

SublimeText3 英文版

SublimeText3 英文版

推薦:為Win版本,支援程式碼提示!

Dreamweaver Mac版

Dreamweaver Mac版

視覺化網頁開發工具

ZendStudio 13.5.1 Mac

ZendStudio 13.5.1 Mac

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具