<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從Vue.nextTick探究事件循環(huán)中的線程協(xié)作機(jī)制

          共 5016字,需瀏覽 11分鐘

           ·

          2021-06-02 03:40


          一、背景


          對vue里的nextTick()方法理解不清晰,會導(dǎo)致api代碼濫用的現(xiàn)象,我查看了vue官網(wǎng)的說明:


          Vue.nextTick()用于在下次 DOM 更新循環(huán)結(jié)束之后執(zhí)行延遲回調(diào)。


          問題來了,怎么確定下次DOM更新循環(huán)結(jié)束的時間點呢?


          二、Vue.nextTick源碼探索


          先看Vue.nextTick()源碼[1]的實現(xiàn)方式。next-tick.js源碼主要包含callbacks、pending、timerFunc、flushCallbacks四個變量:


          • callbacks,一個用于接收Vue.nextTick回調(diào)方法的隊列
          • flushCallbacks,先入先出執(zhí)行callbacks隊列中所有回調(diào),并清空隊列
          • timerFunc,判斷當(dāng)前環(huán)境兼容性,選擇對應(yīng)方法執(zhí)行flushCallbacks
          • pending,控制flushCallbacks在callbacks隊列清空前只執(zhí)行一次


          其中最關(guān)鍵的是timerFunc對于觸發(fā)flushCallbacks的方法選擇,這里貼出源碼:


          let timerFunc// 1、優(yōu)先采用原生Promise觸發(fā)flushCallbacksif (typeof Promise !== 'undefined' && isNative(Promise)) {  const p = Promise.resolve()  timerFunc = () => {    p.then(flushCallbacks)    // 在有問題的webView中添加空定時器強(qiáng)制刷新微任務(wù)隊列    if (isIOS) setTimeout(noop)  }  isUsingMicroTask = true}

          // 2、在promise不可用時采用原生MutationObserver生成一個Dom元素觸發(fā)flushCallbacks else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]')) { let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter))

          observer.observe(textNode, { characterData: true })

          timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true}

          // 3、采用原生setImmediate來降級處理 else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { setImmediate(flushCallbacks) }}

          // 4、采用setTimeout兜底處理 else { // Fallback to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0) }}

          上面的這段核心代碼,優(yōu)先采用了Promise保存回調(diào),然后依次采用了MutationObserver、setImmediate、setTimeout兜底。下面是Vue.nextTick方法的流程圖:



          timerFunc這里的初始化方式利用了在不同環(huán)境下采用JavaScript的事件循環(huán)(eventLoop)機(jī)制做了觸發(fā)回調(diào)的優(yōu)雅降級。


          三、事件循環(huán)機(jī)制


          JavaScript運行時,按任務(wù)環(huán)境不同劃分出了宏任務(wù)(macrotask)和微任務(wù)(microtask)。宏任務(wù)是由宿主環(huán)境發(fā)起的,宿主環(huán)境有瀏覽器、Node,常見的添加宏任務(wù)的方法為setTimeout、Ajax、I/O、UI交互事件等;微任務(wù)是由語言本身自帶的,常見的添加方法有Promise.then、MutationObserver等。


          事件循環(huán)的執(zhí)行機(jī)制為:


          1、當(dāng)js執(zhí)行棧中的所有任務(wù)的執(zhí)行過程中若遇到微任務(wù)或宏任務(wù),則將其添加到對應(yīng)隊列中;


          2、執(zhí)行棧中任務(wù)順序執(zhí)行完畢后去檢查微任務(wù)隊列是否為空,不為空則把任務(wù)按先入先出順序依次拉取微任務(wù)隊列中方法到j(luò)s執(zhí)行棧中運行;


          3、執(zhí)行棧以及微任務(wù)隊列都清空后去檢查宏任務(wù)隊列是否為空,不為空把任務(wù)按先入先出順序加入當(dāng)前執(zhí)行棧;


          4、當(dāng)執(zhí)行棧執(zhí)行完畢后,檢查微任務(wù)隊列是否為空,然后檢查宏任務(wù)隊列是否為空,以此循環(huán)至微任務(wù)隊列、宏任務(wù)隊列同時為空。



          四、事件循環(huán)中的Dom渲染時機(jī)


          結(jié)合上面nextTick的源碼可以看出,Vue.nextTick將回調(diào)方法優(yōu)先使用Promise.then放入了當(dāng)前執(zhí)行棧的微任務(wù)隊列,采用了setTimeout放入宏任務(wù)隊列兜底。那可以得出微任務(wù)是在dom更新循環(huán)結(jié)束后觸發(fā)的,為什么有這樣的規(guī)定呢,dom樹更新后什么時候渲染呢?帶著這個問題,我做了一個小測試。


          document.body.style.background = 'blue';console.log(1)

          setTimeout(() => { document.body.style.background = 'yellow'; console.log(2)},0)

          Promise.resolve().then(()=>{ document.body.style.background = 'red'; console.log(3)})console.log(4);


          上面這段代碼的輸出結(jié)果是1,4,3,2,頁面的變化是由紅色轉(zhuǎn)黃色,沒有渲染為藍(lán)色,以及沒有由藍(lán)轉(zhuǎn)紅的過程,可以證明渲染是在微任務(wù)之后,宏任務(wù)之前執(zhí)行的


          然后我在每次打印時加上了對當(dāng)前dom樹的查詢,代碼如下:


          document.body.style.background = 'blue';console.log(1,document.body.style.background)

          Promise.resolve().then(()=>{ document.body.style.background = 'red'; console.log(2,document.body.style.background)})

          setTimeout(() => { document.body.style.background = 'yellow'; console.log(3,document.body.style.background)},0)console.log(4,document.body.style.background);


          可以看到Dom樹的變化是實時生效的,但對于Dom樹的渲染是延遲生效的,并且晚于微任務(wù),早于宏任務(wù)。這樣不用頻繁的觸發(fā)渲染,而把一輪微任務(wù)隊列中Dom樹的變化收集起來統(tǒng)一渲染也節(jié)省了渲染性能消耗。


          五、事件循環(huán)中的線程協(xié)作


          主要負(fù)責(zé)Dom渲染部分的是與js線程同處于瀏覽器中渲染進(jìn)程下的GUI渲染線程,下面結(jié)合瀏覽器運行機(jī)制來描述一下事件循環(huán)過程中的線程協(xié)作機(jī)制,本文大部分瀏覽器相關(guān)知識來源于李兵的《瀏覽器工作原理與實踐》這門課。


          首先,瀏覽器是多進(jìn)程運行的,如常用的Chrome瀏覽器程序運行時包括:1個瀏覽器主進(jìn)程、1個GPU進(jìn)程、1個網(wǎng)絡(luò)進(jìn)程、多個渲染進(jìn)程、多個插件進(jìn)程。



          其中,每個標(biāo)簽頁配置了一個單獨的渲染進(jìn)程,而渲染進(jìn)程中包含js引擎線程、事件觸發(fā)線程、GUI渲染線程、異步HTTP請求線程、定時器觸發(fā)線程。而事件循環(huán)就是通過渲染進(jìn)程中各線程的協(xié)作,從而讓單線程的JS能夠執(zhí)行異步任務(wù)。



          1、JavaScript引擎線程,處理頁面與用戶的交互,以及操作DOM樹、CSS樣式樹來給用戶呈現(xiàn)一份動態(tài)而豐富的交互體驗和服務(wù)器邏輯的交互處理,與GUI渲染引擎互斥。


          2、GUI渲染線程,負(fù)責(zé)渲染瀏覽器界面, 與JavaScript引擎線程互斥,當(dāng)界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時,該線程就會執(zhí)行。


          3、事件觸發(fā)線程,事件觸發(fā)時負(fù)責(zé)把事件添加到待處理隊列的隊尾,等待JS引擎的處理。事件類型包括定時任務(wù)、AJAX異步請求、DOM事件如鼠標(biāo)點擊等,但由于JS的單線程關(guān)系所有這些事件都得排隊等待JS引擎處理。


          4、定時器線程,負(fù)責(zé)計時并觸發(fā)定時。舉例為SetTimeout的實現(xiàn)過程是在使用SetTimeout設(shè)置定時任務(wù)后,會將回調(diào)添加在延時執(zhí)行隊列中,然后用定時器開始計時,計時結(jié)束后將延時執(zhí)行隊列中的回調(diào)任務(wù)移出到j(luò)s執(zhí)行隊列中,按js執(zhí)行隊列順序執(zhí)行。


          5、異步http請求線程,在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求,將檢測到狀態(tài)變更時,如果設(shè)置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件放到JS引擎的宏任務(wù)隊列中等待處理。


          將渲染進(jìn)程中各線程功能和事件循環(huán)相結(jié)合,可以得到下圖:



          六、總結(jié)


          • 探索源碼發(fā)現(xiàn),nextTick在不同環(huán)境下采用事件循環(huán)機(jī)制做了觸發(fā)回調(diào)的優(yōu)雅降級。


          • 事件循環(huán)機(jī)制中,Dom樹的變化是即時生效的,但Dom樹的渲染晚于微任務(wù),早于宏任務(wù)。而且把微任務(wù)隊列中Dom樹的變化收集起來統(tǒng)一渲染節(jié)省了渲染性能消耗。


          • 結(jié)合瀏覽器相關(guān)知識,得出了事件循環(huán)的線程協(xié)作機(jī)制,其中包括了渲染線程的執(zhí)行時機(jī)。


          六、最佳實踐


          1、對于vue實例跟dom雙向綁定的數(shù)據(jù)更新,需要在nexttick的回調(diào)后獲取更新后的dom元素。


          // vue官網(wǎng)api用法說明// 修改數(shù)據(jù)vm.msg = 'Hello'

          // DOM 還沒有更新Vue.nextTick(function () { // DOM 更新了})


          這里在修改vue實例的數(shù)據(jù)后沒有立即更新dom,這里是由于vue數(shù)據(jù)的雙向綁定機(jī)制導(dǎo)致的,在修改vm.msg后會按續(xù)觸發(fā)setter()[Object.defineProperty] =》 notify() =》 update() =》 queueWatcher() =》 nextTick


          可以看到修改數(shù)據(jù)后最終是通過nextTick添加了微任務(wù)去添加dom更新事件,所以必須使用vue.nextTick才能獲取到更新后的dom元素,并且這里是還沒有渲染的。這里就不詳細(xì)講vue的雙向綁定機(jī)制了,感興趣的同學(xué)可以去閱讀源碼,上面提到的方法都標(biāo)記了源文件地址。


          2、對于非vue雙向綁定的dom更新,在處理dom更新的語句后面可直接操作更新后的dom元素。


          3、操作dom的多次更新(無論是否使用vue雙向綁定)應(yīng)該放在同一輪事件循環(huán)的當(dāng)前js執(zhí)行?;蛭⑷蝿?wù)中,僅需調(diào)用一次渲染線程更新dom,避免放在下一輪宏任務(wù)中。這樣處理可將多次處理dom優(yōu)化為一次渲染,避免重復(fù)渲染,減少性能損失。


          注:[1] https://github.com/vuejs/vue/blob/dev/src/core/util/next-tick.js




          作者簡介


          楊亮,騰訊前端開發(fā)工程師,負(fù)責(zé)IEG健康系統(tǒng)相關(guān)前端業(yè)務(wù),畢業(yè)于南京大學(xué)軟件學(xué)院。




          6月5日,Techo TVP 開發(fā)者峰會 ServerlessDays China 2021,即將重磅來襲!

          掃碼立即參會贏好禮??


          瀏覽 50
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  一级免费爱爱视频 | 中文字幕超碰在线播放 | 天天爽天天日天天射天天舔天天操天天射天天搞 | 亚洲成人情趣大香蕉视频 | 日韩18成人久久久 |