這篇文章帶給大家的內容是關於詳解javascript瀏覽器的事件循環機制,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。
拋在前面的問題:
單執行緒如何做到非同步
事件循環的過程是怎樣的
macrotask 和microtask 是什麼,它們有何區別
單線程和非同步
提到js,就會想到單線程,非同步,那麼單線程是如何做到非同步的呢?概念先行,先要了解下單線程和非同步之間的關係。
js的任務分為同步和非同步兩種,它們的處理方式也不同,同步任務是直接在主執行緒上排隊執行,非同步任務則會被放到任務佇列中,若有多個任務(非同步任務)則要在任務佇列中排隊等待,任務佇列類似一個緩衝區,任務下一步會被移到呼叫堆疊(call stack),然後主執行緒執行呼叫堆疊的任務。
單執行緒是指js引擎中負責解析執行js程式碼的執行緒只有一個(主執行緒),即每次只能做一件事,而我們知道一個ajax請求,主執行緒在等待它回應的同時是會去做其它事的,瀏覽器先在事件表註冊ajax的回調函數,響應回來後回調函數被添加到任務隊列中等待執行,不會造成線程阻塞,所以說js處理ajax請求的方式是異步的。
總而言之,檢查呼叫堆疊是否為空,以及確定把哪個task加入呼叫堆疊的這個過程就是事件循環,而js實作非同步的核心就是事件循環。
呼叫堆疊和任務佇列
顧名思義,呼叫堆疊是一個堆疊結構,函數呼叫會形成一個棧幀,幀中包含了當前執行函數的參數和局部變數等上下文信息,函數執行完後,它的執行上下文會從堆疊中彈出。
下圖就是呼叫堆疊和任務佇列的關係圖
#事件循環
關於事件循環, HTML規範的介紹
There must be at least one event loop per user agent, and at most
one event loop per unit of related similar-origin browsing contexts.
An event loop has one or more task queues.
Each task is defined as coming from a specific task source.
從規範理解,瀏覽器至少有一個事件循環,一個事件循環至少有一個任務佇列(macrotask),每個外任務都有自己的分組,瀏覽器會為不同的任務群組設定優先權。
macrotask & microtask
規格有提到兩個概念,但沒有詳細介紹,查閱一些資料大概可總結如下:
macrotask:包含執行整體的js程式碼,事件回調,XHR回調,定時器(setTimeout/setInterval/setImmediate),IO操作,UI render
microtask:更新應用程式狀態的任務,包括promise回調,MutationObserver,process.nextTick,Object.observe
其中setImmediate和process.nextTick是nodejs的實現,在nodejs篇會詳細介紹。
事件處理過程
關於macrotask和microtask的理解,光這樣看會有些晦澀難懂,結合事件循壞的機制理解清晰很多,下面這張圖可以說是介紹得非常清楚了。
總結起來,一次事件循環的步驟包括:
1. 檢查macrotask佇列是否為空,非空則到2,為空則到3
2. 執行macrotask中的一個任務
3. 繼續檢查microtask佇列是否為空,若有則到4,否則到5
4. 取出microtask中的任務執行,執行完成返回步驟3
5.執行視圖更新
mactotask & microtask的執行順序
讀完這麼多乾巴巴的概念介紹,不如看一段程式碼感受下
console.log('start') setTimeout(function() { console.log('setTimeout') }, 0) Promise.resolve().then(function() { console.log('promise1') }).then(function() { console.log('promise2') }) console.log('end')
列印台輸出的log順序是什麼?結合上述的步驟分析,係不繫so easy~
首先,全域程式碼(main())壓入呼叫堆疊執行,列印start;
接下來setTimeout壓入macrotask佇列,promise.then回呼放入microtask佇列,最後執行console.log('end'),列印出end;
至此,呼叫堆疊中的程式碼執行完成,回顧macrotask的定義,我們知道全域程式碼屬於macrotask,macrotask執行完,那接下來就是執行microtask佇列的任務了,執行promise回呼印出promise1;
promise回调函数默认返回undefined,promise状态变为fullfill触发接下来的then回调,继续压入microtask队列,event loop会把当前的microtask队列一直执行完,此时执行第二个promise.then回调打印出promise2;
这时microtask队列已经为空,从上面的流程图可以知道,接下来主线程会去做一些UI渲染工作(不一定会做),然后开始下一轮event loop,执行setTimeout的回调,打印出setTimeout;
这个过程会不断重复,也就是所谓的事件循环。
视图渲染的时机
回顾上面的事件循环示意图,update rendering(视图渲染)发生在本轮事件循环的microtask队列被执行完之后,也就是说执行任务的耗时会影响视图渲染的时机。通常浏览器以每秒60帧(60fps)的速率刷新页面,据说这个帧率最适合人眼交互,大概16.7ms渲染一帧,所以如果要让用户觉得顺畅,单个macrotask及它相关的所有microtask最好能在16.7ms内完成。
但也不是每轮事件循环都会执行视图更新,浏览器有自己的优化策略,例如把几次的视图更新累积到一起重绘,重绘之前会通知requestAnimationFrame执行回调函数,也就是说requestAnimationFrame回调的执行时机是在一次或多次事件循环的UI render阶段。
以下代码可以验证
setTimeout(function() {console.log('timer1')}, 0) requestAnimationFrame(function(){ console.log('requestAnimationFrame') }) setTimeout(function() {console.log('timer2')}, 0) new Promise(function executor(resolve) { console.log('promise 1') resolve() console.log('promise 2') }).then(function() { console.log('promise then') }) console.log('end')
可以看到,结果1中requestAnimationFrame()是在一次事件循环后执行,而在结果2,它的执行则是在三次事件循环结束后。
总结
1、事件循环是js实现异步的核心
2、每轮事件循环分为3个步骤:
a) 执行macrotask队列的一个任务
b) 执行完当前microtask队列的所有任务
c) UI render
3、浏览器只保证requestAnimationFrame的回调在重绘之前执行,没有确定的时间,何时重绘由浏览器决定
以上是詳解javascript瀏覽器的事件循環機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!