這篇文章帶給大家的內容是關於深入剖析JavsScript異步之事件輪詢,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所幫助。
JavsScript 是一門單執行緒的程式語言,這意味著一個時間裡只能處理一件事,也就是說JavaScript 引擎一次只能在一個執行緒裡處理一條語句。
雖然單執行緒簡化了程式碼,因為你不必太擔心並發引出的問題,這也意味著你將在阻塞主執行緒的情況下執行長時間的操作,例如網路請求。
想像一下從API中請求一些數據,根據具體的情況,伺服器需要一些時間來處理請求,同時阻塞主線程,使網頁長時間處於無響應的狀態。
這就是引入非同步 JavaScript 的原因。使用非同步JavaScript(如回呼函數、promise、async/await),可以不用阻塞主執行緒的情況下長時間執行網路請求:)
可能你知道不知道異步JavsScript 是如何運作,並且不要緊,但知道它是如何運作,對JavaScript 非同步更深入的了解是有幫助的。
所以不在囉嗦了,我們開始吧 :)
在深入研究非同步JavaScript
之前,讓我們先了解同步 JavaScript
程式碼如何在 JavaScript
引擎中執行。例如:
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first();
要理解上述程式碼如何在 JavaScript 引擎中執行,我們必須理解執行上下文和呼叫堆疊(也稱為執行堆疊)的概念。
函數程式碼在函數執行上下文中執行,全域程式碼在全域執行上下文中執行。每個函數都有自己的執行上下文。
呼叫堆疊顧名思義是一個具有LIFO(後進先出)結構的堆疊,用於儲存在程式碼執行期間所建立的所有執行上下文。
JavaScript 只有一個呼叫堆疊,因為它是一種單執行緒程式語言。呼叫堆疊具有 LIFO 結構,這意味著項目只能從堆疊頂部新增或刪除。
讓我們回到上面的程式碼片段,並試著理解程式碼如何在JavaScript引擎中執行。
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first();
當執行此程式碼時,將建立一個全域執行上下文(由main()表示)並將其推到呼叫堆疊的頂端。當遇到對first()的呼叫時,它會被推送到堆疊的頂部。
接下來,console.log('Hi there!')被推送到堆疊的頂部,當它完成時,它會從堆疊中彈出。之後,我們呼叫second(),因此second()函數被推到堆疊的頂端。
console.log('Hello there!')被推送到堆疊頂部,並在完成時彈出堆疊。 second() 函數結束,因此它從堆疊中彈出。
console.log(“the End”)被推到堆疊的頂部,並在完成時刪除。之後,first()函數完成,因此從堆疊中刪除它。
程式在這一點上完成了它的執行,所以全域執行上下文(main())從堆疊中彈出。
現在我們已經對呼叫堆疊和同步JavaScript的工作原理有了基本的了解,讓我們回到非同步JavaScript。
讓我們假設我們正在以同步的方式進行映像處理或網路請求。例如:
const processImage = (image) => { /** * doing some operations on image **/ console.log('Image processed'); } const networkRequest = (url) => { /** * requesting network resource **/ return someData; } const greeting = () => { console.log('Hello World'); } processImage(logo.jpg); networkRequest('www.somerandomurl.com'); greeting();
做映像處理和網路請求需要時間,當processImage()函數被呼叫時,它會根據圖像的大小花費一些時間。
processImage() 函數完成後,將從堆疊中刪除它。然後呼叫 networkRequest() 函數並將其推入堆疊。同樣,它也需要一些時間來完成執行。
最後,當networkRequest()函數完成時,呼叫greeting()函數,因為它只包含一個控制台。日誌語句和控制台。日誌語句通常很快,因此greeting()函數立即執行並傳回。
因此,我們必須等待函數(如processImage()或networkRequest())完成。這意味著這些函數阻塞了呼叫堆疊或主執行緒。因此,在執行上述程式碼時,我們不能執行任何其他操作,這是不理想的。
最簡單的解決方案是異步回呼。我們使用非同步回調使程式碼非阻塞。例如:
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest();
這裡我使用了setTimeout方法來模擬網路請求。請記住setTimeout不是JavaScript引擎的一部分,它是web api(在瀏覽器中)和C/ c api(在node.js中)的一部分。
為了理解這段程式碼是如何執行的,我們必須理解更多的概念,例如事件輪詢和回呼隊列(或訊息隊列)。
事件轮询、web api和消息队列不是JavaScript引擎的一部分,而是浏览器的JavaScript运行时环境或Nodejs JavaScript运行时环境的一部分(对于Nodejs)。在Nodejs中,web api被c/c++ api所替代。
现在让我们回到上面的代码,看看它是如何异步执行的。
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest(); console.log('The End');
当上述代码在浏览器中加载时,console.log(' Hello World ') 被推送到堆栈中,并在完成后弹出堆栈。接下来,将遇到对 networkRequest() 的调用,因此将它推到堆栈的顶部。
下一个 setTimeout() 函数被调用,因此它被推到堆栈的顶部。setTimeout()有两个参数:
1) 回调和
2) 以毫秒(ms)为单位的时间。
setTimeout() 方法在web api环境中启动一个2s的计时器。此时,setTimeout()已经完成,并从堆栈中弹出。cosole.log(“the end”) 被推送到堆栈中,在完成后执行并从堆栈中删除。
同时,计时器已经过期,现在回调被推送到消息队列。但是回调不会立即执行,这就是事件轮询开始的地方。
事件轮询的工作是监听调用堆栈,并确定调用堆栈是否为空。如果调用堆栈是空的,它将检查消息队列,看看是否有任何挂起的回调等待执行。
在这种情况下,消息队列包含一个回调,此时调用堆栈为空。因此,事件轮询将回调推到堆栈的顶部。
然后是 console.log(“Async Code”) 被推送到堆栈顶部,执行并从堆栈中弹出。此时,回调已经完成,因此从堆栈中删除它,程序最终完成。
消息队列还包含来自DOM事件(如单击事件和键盘事件)的回调。例如:
document.querySelector('.btn').addEventListener('click',(event) => { console.log('Button Clicked'); });
对于DOM事件,事件侦听器位于web api环境中,等待某个事件(在本例中单击event)发生,当该事件发生时,回调函数被放置在等待执行的消息队列中。
同样,事件轮询检查调用堆栈是否为空,并在调用堆栈为空并执行回调时将事件回调推送到堆栈。
我们还可以使用setTimeout
来延迟函数的执行,直到堆栈清空为止。例如
const bar = () => { console.log('bar'); } const baz = () => { console.log('baz'); } const foo = () => { console.log('foo'); setTimeout(bar, 0); baz(); } foo();
打印结果:
foo baz bar
当这段代码运行时,第一个函数foo()被调用,在foo内部我们调用console.log('foo'),然后setTimeout()被调用,bar()作为回调函数和时0秒计时器。
现在,如果我们没有使用 setTimeout, bar() 函数将立即执行,但是使用 setTimeout 和0秒计时器,将bar的执行延迟到堆栈为空的时候。
0秒后,bar()回调被放入等待执行的消息队列中。但是它只会在堆栈完全空的时候执行,也就是在baz和foo函数完成之后。
我们已经了解了异步回调和DOM事件是如何执行的,它们使用消息队列存储等待执行所有回调。
ES6引入了任务队列的概念,任务队列是 JavaScript 中的 promise 所使用的。消息队列和任务队列的区别在于,任务队列的优先级高于消息队列,这意味着任务队列中的promise 作业将在消息队列中的回调之前执行,例如:
const bar = () => { console.log('bar'); }; const baz = () => { console.log('baz'); }; const foo = () => { console.log('foo'); setTimeout(bar, 0); new Promise((resolve, reject) => { resolve('Promise resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); baz(); }; foo();
打印结果:
foo baz Promised resolved bar
我们可以看到 promise 在 setTimeout 之前执行,因为 promise 响应存储在任务队列中,任务队列的优先级高于消息队列。
因此,我们了解了异步 JavaScript 是如何工作的,以及调用堆栈、事件循环、消息队列和任务队列等概念,这些概念共同构成了 JavaScript 运行时环境。虽然成为一名出色的JavaScript开发人员并不需要学习所有这些概念,但是了解这些概念是有帮助的:)
以上是深入剖析JavaScript非同步之事件輪詢的詳細內容。更多資訊請關注PHP中文網其他相關文章!