從 Vue3 原始碼中再談 nextTick
開始之前先看下官方對其的定義
定義:在下次DOM更新迴圈結束之後執行延遲迴調。在修改資料之後立即使用這個方法,獲取更新後的DOM
看完是不是有一堆問號?我們從中找出來產生問號的關鍵詞
下次DOM更新迴圈結束之後?
執行延遲迴調?
更新後的DOM?
從上面三個疑問大膽猜想一下
vue更新DOM是有策略的,不是同步更新
nextTick可以接收一個函式做為入參
nextTick後能拿到最新的資料
好了,問題都丟擲來了,先來看一下如何使用
import{createApp,nextTick}from'vue'
const app=createApp({
setup(){
const message=ref('Hello!')
const changeMessage=async newMessage=>{
message.value=newMessage
//這裡獲取DOM的value是舊值
await nextTick()
//nextTick後獲取DOM的value是更新後的值
console.log('Now DOM is updated')
}
}
})
<a href="親自試一試</a>
那麼nextTick是怎麼做到的呢?為了後面的內容更好理解,這裡我們得從js的執行機制說起
JS執行機制
我們都知道JS是單執行緒語言,即指某一時間內只能幹一件事,有的同學可能會問,為什麼JS不能是多執行緒呢?多執行緒就能同一時間內幹多件事情了
是否多執行緒這個取決於語言的用途,一個很簡單的例子,如果同一時間,一個新增了DOM,一個刪除了DOM,這個時候語言就不知道是該添還是該刪了,所以從應用場景來看JS只能是單執行緒
單執行緒就意味著我們所有的任務都需要排隊,後面的任務必須等待前面的任務完成才能執行,如果前面的任務耗時很長,一些從使用者角度上不需要等待的任務就會一直等待,這個從體驗角度上來講是不可接受的,所以JS中就出現了非同步的概念
概念
同步在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務
非同步不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行
執行機制
(1)所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。
(2)主執行緒之外,還存在一個"任務佇列"(task queue)。只要非同步任務有了執行結果,就在"任務佇列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務佇列",看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)主執行緒不斷重複上面的第三步
image.png
nextTick
現在我們回來vue中的nextTick
實現很簡單,完全是基於語言執行機制實現,直接建立一個非同步任務,那麼nextTick自然就達到在同步任務後執行的目的
const p=Promise.resolve()
export function nextTick(fn?:()=>void):Promise<void>{
return fn?p.then(fn):p
}
<a href="親自試一試</a>
看到這裡,有的同學可能又會問,前面我們猜想的DOM更新也是非同步任務,那他們的這個執行順序如何保證呢?
別急,在原始碼中nextTick還有幾個兄弟函式,我們接著往下看
queueJob and queuePostFlushCb
queueJob維護job列隊,有去重邏輯,保證任務的唯一性,每次呼叫去執行queueFlush queuePostFlushCb維護cb列隊,被呼叫的時候去重,每次呼叫去執行queueFlush
const queue:(Job|null)[]=[]
export function queueJob(job:Job){
//去重
if(!queue.includes(job)){
queue.push(job)
queueFlush()
}
}
export function queuePostFlushCb(cb:Function|Function[]){
if(!isArray(cb)){
postFlushCbs.push(cb)
}else{
postFlushCbs.push(...cb)
}
queueFlush()
}
queueFlush
開啟非同步任務(nextTick)處理flushJobs
function queueFlush(){
//避免重複呼叫flushJobs
if(!isFlushing&&!isFlushPending){
isFlushPending=true
nextTick(flushJobs)
}
}
flushJobs
處理列隊,先對列隊進行排序,執行queue中的job,處理完後再處理postFlushCbs,如果佇列沒有被清空會遞迴呼叫flushJobs清空佇列
function flushJobs(seen?:CountMap){
isFlushPending=false
isFlushing=true
let job
if(__DEV__){
seen=seen||new Map()
}
//Sort queue before flush.
//This ensures that:
//1.Components are updated from parent to child.(because parent is always
//created before the child so its render effect will have smaller
//priority number)
//2.If a component is unmounted during a parent component's update,
//its update can be skipped.
//Jobs can never be null before flush starts,since they are only invalidated
//during execution of another flushed job.
queue.sort((a,b)=>getId(a!)-getId(b!))
while((job=queue.shift())!==undefined){
if(job===null){
continue
}
if(__DEV__){
checkRecursiveUpdates(seen!,job)
}
callWithErrorHandling(job,null,ErrorCodes.SCHEDULER)
}
flushPostFlushCbs(seen)
isFlushing=false
//some postFlushCb queued jobs!
//keep flushing until it drains.
if(queue.length||postFlushCbs.length){
flushJobs(seen)
}
}
好了,實現全在上面了,好像還沒有解開我們的疑問,我們需要搞清楚queueJob及queuePostFlushCb是怎麼被呼叫的
//renderer.ts
function createDevEffectOptions(
instance:ComponentInternalInstance
):ReactiveEffectOptions{
return{
scheduler:queueJob,
onTrack:instance.rtc?e=>invokeArrayFns(instance.rtc!,e):void 0,
onTrigger:instance.rtg?e=>invokeArrayFns(instance.rtg!,e):void 0
}
}
//effect.ts
const run=(effect:ReactiveEffect)=>{
...
if(effect.options.scheduler){
effect.options.scheduler(effect)
}else{
effect()
}
}
看到這裡有沒有恍然大悟的感覺?原來當響應式物件發生改變後,執行effect如果有scheduler這個引數,會執行這個scheduler函式,並且把effect當做引數傳入
繞口了,簡單點就是queueJob(effect),嗯,清楚了,這也是資料發生改變後頁面不會立即更新的原因
effect傳送門
為什麼要用nextTick
一個例子讓大家明白
{{num}}
for(let i=0;i<100000;i++){
num=i
}
如果沒有nextTick更新機制,那麼num每次更新值都會觸發檢視更新,有了nextTick機制,只需要更新一次,所以為什麼有nextTick存在,相信大家心裡已經有答案了。
總結
nextTick是vue中的更新策略,也是效能最佳化手段,基於JS執行機制實現
vue中我們改變資料時不會立即觸發檢視,如果需要實時獲取到最新的DOM,這個時候可以手動呼叫nextTick
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69995861/viewspace-2761553/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 【原始碼&庫】Vue3 中的 nextTick 魔法背後的原理原始碼Vue
- 再談原始碼閱讀原始碼
- 從 原始碼 談談 redux compose原始碼Redux
- 從promise、process.nextTick、setTimeout出發,談談Event Loop中的Job queuePromiseOOP
- 從原始碼裡面瞭解vue的nextTick的使用原始碼Vue
- Vue原始碼解析之nextTickVue原始碼
- 談談元件化-從原始碼到理解元件化原始碼
- 從原始碼的角度再學「Thread」原始碼thread
- 從 Vue3 原始碼學習 Proxy & ReflectVue原始碼
- 基於原始碼分析Vue的nextTick原始碼Vue
- 從原始碼角度談談AsyncTask的使用及其原理原始碼
- 在vue3?原始碼中學會typescript? - "is"Vue原始碼TypeScript
- CesiumJS 原始碼雜談 - 從光到 UniformJS原始碼ORM
- 從原始碼層面談談mybatis的快取設計原始碼MyBatis快取
- 從 Redux 原始碼談談函數語言程式設計Redux原始碼函數程式設計
- 談談LruCache原始碼原始碼
- [精讀原始碼系列]Vue中DOM的非同步更新和Vue.nextTick()原始碼Vue非同步
- Vue2.0原始碼閱讀筆記(四):nextTickVue原始碼筆記
- 原始碼解讀-vue是如何實現$nextTick的原始碼Vue
- Vue3 原始碼之 reactivityVue原始碼React
- vue3原始碼難學,先從petite-vue開始吧Vue原始碼
- 「從原始碼中學習」Vue原始碼中的JS騷操作原始碼VueJS
- Vue原始碼閱讀- 批量非同步更新與nextTick原理Vue原始碼非同步
- Vue原始碼閱讀 - 批量非同步更新與nextTick原理Vue原始碼非同步
- 前端筆記-vue v2.6.10原始碼註釋-nextTick前端筆記Vue原始碼
- 從原始碼的角度來談一談HashMap的內部實現原理原始碼HashMap
- Vue3原始碼分析之compositionApiVue原始碼API
- 結合原始碼談談ThreadLocal!原始碼thread
- 再讀 Larave 核心原始碼原始碼
- vue中的nextTickVue
- 淺談Kotlin中的Sequences原始碼解析(十)Kotlin原始碼
- Vue原始碼閱讀一:說說vue.nextTick實現Vue原始碼
- 從event loop看vue的nextTickOOPVue
- 來談談限流-RateLimiter原始碼分析MIT原始碼
- 線上直播系統原始碼,Vue3中全域性配置 axios原始碼VueiOS
- Vue中$nextTick的理解Vue
- 從bootstrap原始碼中學習Sass(一)boot原始碼
- 再談vbo