日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 前端技术 > HTML >内容正文

HTML

「深入浅出」主流前端框架更新批处理方式

發(fā)布時間:2024/8/23 HTML 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 「深入浅出」主流前端框架更新批处理方式 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

作者?| 👽

來源?| 前端Sharing

背景

在不同的技術(shù)框架背景下,處理更新的手段各不相同,今天我們來探討一下,主流的前端框架批量處理的方式,和其內(nèi)部的實現(xiàn)原理。

通過今天的學(xué)習(xí),你將收獲這些內(nèi)容:

  • ?主流前端框架的批量更新方式。

  • ?vue 和 react 批量更新的實現(xiàn)。

  • ?宏任務(wù)和微任務(wù)的特點(diǎn)。

一次 vue 案例

首先來想一個問題。比如在 vue 中一次更新中。

<template><div>姓名:?{{ name }}年齡:?{{ age }}<button?@click="handleClick"?>點(diǎn)擊</button></div> </template><script> export?default?{data(){return?{age:0,name:''}},methods:{handleClick(){this.name?=?'alien'this.age?=?18}} } </script>

如上是一個非常簡單的邏輯代碼,點(diǎn)擊按鈕,會觸發(fā) name 和 age 的更新。那么首先想一個問題就是:

  • 正常情況下,vue 的數(shù)據(jù)層是通過響應(yīng)式處理的,那么比如 age 和 name 可以理解成做了一層屬性代理,字符串模版 template 里面的屬性 ( name 和 age ) 的 get 會和組件的渲染 watcher ( vue3.0 里面的 effect )建立起關(guān)聯(lián)。

  • 一次重新賦值會觸發(fā) set ,那么根據(jù)響應(yīng)式,會觸發(fā)渲染 watcher 重新執(zhí)行,然后就會重新更新組件,渲染視圖。

那么暴露的問題就是,我們在 handleClick 中,同時改變了 name 和 age 屬性,那么按照正常情況下,會分別觸發(fā) name 和 age 的 set,那么如果不做處理,那么會讓渲染 watcher 執(zhí)行兩次,結(jié)果就是組件會 update 兩次,但是結(jié)果是這樣的嗎?

結(jié)果是:vue 底層通過批量處理,只讓組件 update 一次。

一次 react 案例

上面介紹了在 vue 中更新批處理的案例之后,我們來看一下在 react 中的批量更新處理。把上述案例用 react 來實現(xiàn)一下:

function?Index(){const?[?age?,?setAge?]?=?React.useState(0)const?[?name,?setName?]?=?React.useState('')return?<div>姓名:?{name}年齡:?{age}<button?onClick={()=>{setAge(18)setName('alien')}}>點(diǎn)擊</button></div> }

點(diǎn)擊按鈕,觸發(fā)更新,會觸發(fā)兩次 useState 的更新函數(shù)。那么 React 的更新流程大致是這樣的。

  • 首先會找到 fiberRoot 。

  • 然后進(jìn)行調(diào)和流程。執(zhí)行 Index 組件,得到新的 element。

  • diff fiber,得到 effectList。

  • 執(zhí)行 effect list,得到最新的 dom ,并進(jìn)行渲染繪制。

那么按常理來說,Index 組件會執(zhí)行兩次。可事實是只執(zhí)行一次 render。

批量處理意義

通過上面的案例說明在主流框架中,對于更新都采用批處理。一次上下文中的 update 會被合并成一次更新。那么為什么要進(jìn)行更新批處理呢?

批處理主要是出于對性能方面的考慮,這里拿 react 為例子,看一下批處理前后的對比情況:

🌰例子一:假設(shè)沒有批量更新:

/ ------ js 層面 ------

  • 第一步:發(fā)生點(diǎn)擊事件觸發(fā)一次宏任務(wù)。

  • 第二步:執(zhí)行 setAge ,更新 fiber 狀態(tài)。

  • 第三步:進(jìn)行 render 階段,Index 執(zhí)行,得到新的 element。得到 effectlist.

  • 第四步:進(jìn)行 commit 階段,更新 dom。

  • 第五步:執(zhí)行 setName ,更新 fiber 狀態(tài)。

  • 第六步:重復(fù)執(zhí)行第三步,第四步。

/ ------ 瀏覽器渲染 ------

  • js 執(zhí)行完畢,渲染真實的 dom 元素。

我們可以看到如果沒有批量更新處理,那么會多走很多步驟,包括 render 階段 ,commit 階段,dom 的更新等,這些都會造成性能的浪費(fèi),接下來看一下有批量更新的情況。

🌰例子二:存在批量更新。

/ ------ js 層面 ------

  • 第一步:發(fā)生點(diǎn)擊事件觸發(fā)一次宏任務(wù)。

  • 第二步:setAge 和 setName 批量處理 ,更新 fiber 狀態(tài)。

  • 第三步:進(jìn)行 render 階段,Index 執(zhí)行,得到新的 element。得到 effectlist.

  • 第四步:進(jìn)行 commit 階段,更新 dom。

/ ------ 瀏覽器渲染 ------

  • js 執(zhí)行完畢,渲染真實的 dom 元素。

從上面可以直觀看到更新批處理的作用了,本質(zhì)上在 js 的執(zhí)行上下文上優(yōu)化了很多步驟,減少性能開銷。

簡述宏任務(wù)和微任務(wù)

在正式講批量更新之前,先來溫習(xí)一下宏任務(wù)微任務(wù),這應(yīng)該算是前端工程師必須掌握的知識點(diǎn)。

所謂宏任務(wù),我們可以理解成,<script> 標(biāo)簽中主代碼執(zhí)行,一次用戶交互(比如觸發(fā)了一次點(diǎn)擊事件引起的回調(diào)函數(shù)),定時器 setInterval ,延時器 setTimeout 隊列, MessageChannel 等。這些宏任務(wù)通過 event loop,來實現(xiàn)有條不紊的執(zhí)行。

例如在瀏覽器環(huán)境下,宏任務(wù)的執(zhí)行并不會影響到瀏覽器的渲染和響應(yīng)。我們來做個實驗。

function?Index(){const?[?number?,?setNumber?]?=?useState(0)useEffect(()=>{let?timerfunction?run(){timer?=?setTimeout(()?=>?{console.log('----宏任務(wù)執(zhí)行----')run()},?0)}run()return?()?=>?clearTimeout(timer)},[])return?<div><button?onClick={()?=>?setNumber(number?+?1?)}??>點(diǎn)擊{number}</button></div> }

如上簡單的 demo 中,通過遞歸調(diào)用 run 函數(shù),讓 setTimeout 宏任務(wù)反復(fù)執(zhí)行。

這種情況下 setTimeout 執(zhí)行并不影響點(diǎn)擊事件的執(zhí)行和頁面的正常渲染。

什么是微任務(wù)呢 ?

那么我們再來分析一下微任務(wù),在 js 執(zhí)行過程中,我們希望一些任務(wù),不阻塞代碼執(zhí)行,又能讓該任務(wù)在此輪 event loop 執(zhí)行完畢,那么就引入了一個微任務(wù)隊列的概念了。

微任務(wù)相比宏任務(wù)有如下特點(diǎn):

  • 微任務(wù)在當(dāng)前 js 執(zhí)行完畢后,立即執(zhí)行,會阻塞瀏覽器的渲染和響應(yīng)。

  • 一次宏任務(wù)完畢后,會清空微任務(wù)隊列。

常見的微任務(wù),有 Promise, queueMicrotask ,瀏覽器環(huán)境下的 MutationObserver ,node 環(huán)境下 process.nextTick 等。

我們同樣做個實驗看一下微任務(wù):

function?Index(){const?[?number?,?setNumber?]?=?useState(0)useEffect(()=>{function?run(){Promise.resolve().then(()=>{run()})}run()},[])return?<div><button?onClick={()?=>?setNumber(number?+?1?)}??>點(diǎn)擊{number}</button></div> }
  • 在這種情況下,瀏覽器直接卡死了,沒有了響應(yīng),證實了上述的結(jié)論。

微任務(wù)|宏任務(wù)實現(xiàn)批量更新

講完了宏任務(wù)和微任務(wù),繼續(xù)來看第一種批量更新的實現(xiàn),就是基于宏任務(wù)微任務(wù) 來實現(xiàn)。

先來描述一下這種方式,比如每次更新,我們先并不去立即執(zhí)行更新任務(wù),而是先把每一個更新任務(wù)放入一個待更新隊列 updateQueue 里面,然后 js 執(zhí)行完畢,用一個微任務(wù)統(tǒng)一去批量更新隊列里面的任務(wù),如果微任務(wù)存在兼容性,那么降級成一個宏任務(wù)。這里優(yōu)先采用微任務(wù)的原因就是微任務(wù)的執(zhí)行時機(jī)要早于下一次宏任務(wù)的執(zhí)行。

典型的案例就是 vue 更新原理,vue.$nextTick原理 ,還有 v18 中 scheduleMicrotask 的更新原理。

以 vue 為例子我們看一下 nextTick 的實現(xiàn):

runtime-core/src/scheduler.ts

const?p?=?Promise.resolve()? /*?nextTick?實現(xiàn),用微任務(wù)實現(xiàn)的?*/ export?function?nextTick(fn?:?()?=>?void):?Promise<void>?{return?fn???p.then(fn)?:?p }
  • 可以看到 nextTick 原理,本質(zhì)就是 Promise.resolve() 創(chuàng)建的微任務(wù)。

再看看 react v18 里面的實現(xiàn)。

react-reconciler/src/ReactFiberWorkLoop/ensureRootIsScheduled

function?ensureRootIsScheduled(root,?currentTime)?{/*?省去沒有必要的邏輯?*/if?(newCallbackPriority?===?SyncLane)?{/*?支持微任務(wù)?*/if?(supportsMicrotasks)?{/*?通過微任務(wù)處理?*/scheduleMicrotask(flushSyncCallbacks);}} }

接下里看一下 scheduleMicrotask 是如何實現(xiàn)的。

/*?向下兼容?*/ var?scheduleMicrotask?=?typeof?queueMicrotask?===?'function'???queueMicrotask?:?typeof?Promise?!==?'undefined'???function?(callback)?{return?Promise.resolve(null).then(callback).catch(handleErrorInNextTick); }?:?scheduleTimeout;

scheduleMicrotask 也是用的 Promise.resolve ,還有一個 setTimeout 向下兼容的情況。

接下來模擬一下,這個方式的實現(xiàn)。

class?Scheduler?{constructor(){this.callbacks?=?[]/*?微任務(wù)批量處理?*/queueMicrotask(()=>{this.runTask()})}/*?增加任務(wù)?*/addTask(fn){this.callbacks.push(fn)}runTask(){console.log('------合并更新開始------')while(this.callbacks.length?>?0){const?cur?=?this.callbacks.shift()cur()}console.log('------合并更新結(jié)束------')console.log('------開始更新組件------')} } function?nextTick(cb){const?scheduler?=?new?Scheduler()cb(scheduler.addTask.bind(scheduler)) }/*?模擬一次更新?*/ function?mockOnclick(){nextTick((add)=>{add(function(){console.log('第一次更新')})console.log('----宏任務(wù)邏輯----')add(function(){console.log('第二次更新')})}) }mockOnclick()

我們來模擬一下具體實現(xiàn)細(xì)節(jié):

  • 通過一個 Scheduler 調(diào)度器來完成整個流程。

  • 通過 addTask 每次向隊列中放入任務(wù)。

  • 用 queueMicrotask 創(chuàng)建一個微任務(wù),來統(tǒng)一處理這些任務(wù)。

  • mockOnclick 模擬一次更新。我們用 nextTick 來模擬一下更新函數(shù)的處理邏輯。

可控任務(wù)實現(xiàn)批量更新

上述介紹了通過微任務(wù)的方式實現(xiàn)了批量更新,還有一種方式,通過攔截把任務(wù)變成可控的,典型的就是 React v17 之前的 batchEventUpdate 批量更新。這種情況的更新來源于對事件進(jìn)行攔截,比如 React 的事件系統(tǒng)。

以 React 的事件批量更新為例子,比如我們的 onClick ,onChange 事件都是被 React 的事件系統(tǒng)處理的。外層用一個統(tǒng)一的處理函數(shù)進(jìn)行攔截。而我們綁定的事件都是在該函數(shù)的執(zhí)行上下文內(nèi)部被調(diào)用的。

那么比如在一次點(diǎn)擊事件中觸發(fā)了多次更新。本質(zhì)上外層在 React 事件系統(tǒng)處理函數(shù)的上下文中,這樣的情況下,就可以通過一個開關(guān),證明當(dāng)前更新是可控的,可以做批量處理。接下來 React 就用一次就可以了。

來看一下 React 的底層實現(xiàn)邏輯:

react-dom/src/events/ReactDOMUpdateBatching.js

export?function?batchedEventUpdates(fn,?a)?{/*?開啟批量更新??*/const?prevExecutionContext?=?executionContext;executionContext?|=?EventContext;try?{/*?這里執(zhí)行了的事件處理函數(shù),?比如在一次點(diǎn)擊事件中觸發(fā)setState,那么它將在這個函數(shù)內(nèi)執(zhí)行?*/return?fn(a);}?finally?{/*?try?里面?return?不會影響?finally?執(zhí)行??*//*?完成一次事件,批量更新??*/executionContext?=?prevExecutionContext;if?(executionContext?===?NoContext)?{/*?立即執(zhí)行更新。??*/flushSyncCallbackQueue();}} }

在 React 事件執(zhí)行之前通過 isBatchingEventUpdates=true 打開開關(guān),開啟事件批量更新,當(dāng)該事件結(jié)束,再通過 isBatchingEventUpdates = false; 關(guān)閉開關(guān),然后在 scheduleUpdateOnFiber 中根據(jù)這個開關(guān)來確定是否進(jìn)行批量更新。

比如一次點(diǎn)擊事件中:

const?[?age?,?setAge?]?=?React.useState(0) const?[?name,?setName?]?=?React.useState('') const?handleClick=()=>{setAge(18)setName('alien') }
  • 那么首先 handleClick 是由點(diǎn)擊事件產(chǎn)生的,那么在 React 系統(tǒng)中,先執(zhí)行事件代理函數(shù),然后執(zhí)行 batchedEventUpdates。這個時候開啟了批量更新的狀態(tài)。

  • 接下來?setAge 和 setName 在批量狀態(tài)下不會立即更新。

  • 最后通過 flushSyncCallbackQueue 來立即處理更新任務(wù)。

接下來我們模擬一下具體的實現(xiàn):

<body>??<button?onclick="handleClick()"?>點(diǎn)擊</button> </body> <script>let??batchEventUpdate?=?false?let?callbackQueue?=?[]function?flushSyncCallbackQueue(){console.log('-----執(zhí)行批量更新-------')while(callbackQueue.length?>?0?){const?cur?=?callbackQueue.shift()cur()}console.log('-----批量更新結(jié)束-------')}function?wrapEvent(fn){return?function?(){/*?開啟批量更新狀態(tài)?*/batchEventUpdate?=?truefn()/*?立即執(zhí)行更新任務(wù)?*/flushSyncCallbackQueue()/*?關(guān)閉批量更新狀態(tài)?*/batchEventUpdate?=?false}}function?setState(fn){/*?如果在批量更新狀態(tài)下,那么批量更新?*/if(batchEventUpdate){callbackQueue.push(fn)}else{/*?如果沒有在批量更新條件下,那么直接更新。?*/fn()}}function?handleClick(){setState(()=>{console.log('---更新1---')})console.log('上下文執(zhí)行')setState(()=>{console.log('---更新2---')})}/*?讓?handleClick?變成可控的??*/handleClick?=?wrapEvent(handleClick)</script>

分析一下核心流程:

  • 本方式的核心就是讓 handleClick 通過 wrapEvent 變成可控的。首先 wrapEvent 類似于事件處理函數(shù),在內(nèi)部通過開關(guān) batchEventUpdate 來判斷是否開啟批量更新狀態(tài),最后通過 flushSyncCallbackQueue 來清空待更新隊列。

  • 在批量更新條件下,事件會被放入到更新隊列中,非批量更新條件下,那么立即執(zhí)行更新任務(wù)。

往期推薦

Redis 緩存擊穿(失效)、緩存穿透、緩存雪崩怎么解決?

如果被問到分布式鎖,應(yīng)該怎樣回答?

三分鐘教你用 Scarlet 寫一個 WebSocket App

Java 底層知識:什么是?“橋接方法”??

點(diǎn)分享

點(diǎn)收藏

點(diǎn)點(diǎn)贊

點(diǎn)在看

總結(jié)

以上是生活随笔為你收集整理的「深入浅出」主流前端框架更新批处理方式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。