Vue3源碼分析調度器與watch用法原理
更新時間:2023年01月18日 15:43:32 作者:豬豬愛前端
這篇文章主要為大家介紹了Vue3源碼分析調度器與watch用法原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
本文主要內容
- 學習
Vue3
的調度器原理。 - 了解
nextTick
的實現、為何在nextTick
中可以獲取到修改后的DOM屬性。 - pre、post、和普通任務的執(zhí)行過程。
watch
的實現原理。
調度器
1.添加任務(queueJobs)
- 調度器想要運轉需要添加任務到調度器隊列當中,我們需要知道Vue調度器隊列一共有兩種,分別為
queue
、pendingPostFlushCbs
。 queue
:裝載前置任務和普通任務的隊列。pendingPostFlushCbs
:裝載后置任務的隊列。
下面我們來看看對于前置任務和普通任務添加到queue
中的函數queueJobs
。
//遞歸:當前父親正在執(zhí)行一個任務,在執(zhí)行任務 //期間又添加了一個新的任務,這個新的任務與當前 //執(zhí)行的任務是同一個任務,跳過去重的檢驗 //如果不允許遞歸,那么任務不會被添加到隊列中 function queueJob(job) { //job自身允許遞歸,那么跳過去重檢查(只跳過當前執(zhí)行任務的去重檢查) if ( !queue.length || !queue.includes( job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex ) ) { //如果任務沒有id代表沒有優(yōu)先級 //放到任務隊列的最后面 if (job.id == null) { queue.push(job); } //利用二分法找到任務優(yōu)先級需要插入的位置 else { queue.splice(findInsertionIndex(job.id), 0, job); } //執(zhí)行任務 queueFlush(); } }
- 這里我們需要知道一個概念-->遞歸,這里的遞歸是指:當前正在執(zhí)行的任務和需要添加的任務是同一個任務,如果設置了需要遞歸
(job.allowRecurse=true)
那么就允許這個任務進入queue隊列中,否則不允許進入。 job
:我們還需要知道一個任務的格式。首先job必須是一個函數,他還可以具有以下屬性。
const job = function(){} job.id:Number //用于設置當前任務的優(yōu)先級越小的值優(yōu)先級越高。 job.allowRecurse:Boolean //是否允許遞歸。 job.pre:Boolean //用于判斷是否是前置任務。 job.active:Boolean //當前任務是否可以執(zhí)行。為false在執(zhí)行階段跳過執(zhí)行。
queueJobs
執(zhí)行流程:根據任務的id(優(yōu)先級)利用二分法找到需要插入的位置,插入到queue隊列當中,調用queueFlush
推入執(zhí)行任務的函數到微任務隊列。
2.二分法找到插入位置(findInsertionIndex)
- 這個函數比較簡單,大家看看代碼就可以啦!
//找到插入的位置 //例如[1,2,3,8,9,10,100] //當前插入的id為20 //插入后應該為[1,2,3,8,9,10,20,100] //也就是說最終返回的start=6 //插入流程解析: //1.假設當前執(zhí)行到第二個任務即flushIndex=2 //那么start = 2;end = 7;middle=4; // middleJobId=9;9<20 start=5; //繼續(xù)循環(huán):middle=6;middleJobId=100;end=6 //結束循環(huán)start = 6;這就是需要插入的位置 function findInsertionIndex(id) { let start = flushIndex + 1; let end = queue.length; while (start < end) { // 1000>>>1=>100 8=>4 // 1100>>>1=>110 12=>6 // 1010>>>1=>101 10=>5 // 1001>>>1=>100 9=>4 //計算出中間值,向下取整 const middle = (start + end) >>> 1; //獲取job的id const middleJobId = getId(queue[middle]); middleJobId < id ? (start = middle + 1) : (end = middle); } return start; } //獲取當前任務的id const getId = (job) => (job.id == null ? Infinity : job.id);
3.將執(zhí)行任務的函數推入微任務隊列(queueFlush)
function queueFlush() { //當前沒有執(zhí)行任務且沒有任務可執(zhí)行 if (!isFlushing && !isFlushPending) { //等待任務執(zhí)行 isFlushPending = true; //將flushJobs放入微任務隊列 currentFlushPromise = resolvedPromise.then(flushJobs); } }
isFlushing
:判斷當前是否正在執(zhí)行任務。isFlushPending
:判斷當前是否有等待任務,任務的執(zhí)行是一個微任務,它將會被放到微任務隊列,那么對于渲染主線程來說,當前還沒有執(zhí)行這個微任務,在執(zhí)行這個微任務之前都屬于等待階段。queueFlush
執(zhí)行流程:判斷當前是否沒有執(zhí)行任務、且任務隊列當中沒有任務,如果是那么設置當前為等待階段。最后將flushJobs(執(zhí)行任務的函數)推入微任務隊列。
4.執(zhí)行普通任務(flushJobs)
function flushJobs(seen) { isFlushPending = false; //當前不是等待狀態(tài) isFlushing = true; //當前正在執(zhí)行任務 seen = seen || new Map(); //原文譯文: //在flush之前對queue排序這樣做是為了: //1.組件更新是重父組件到子組件(因為父組件總是在子組件之前創(chuàng)建 //所以父組件的render副作用將會有更低的優(yōu)先級 //2.如果子組件在父組件更新期間并未掛載,那么可以跳過 queue.sort(comparator); //監(jiān)測當前任務是否已經超過了最大遞歸層數 const check = (job) => checkRecursiveUpdates(seen, job); try { for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { const job = queue[flushIndex]; if (job && job.active !== false) { if (check(job)) { continue; } callWithErrorHandling(job, null, 14); } } } finally { //執(zhí)行完所有的任務之后,初始化queue //調用post任務,這些任務調用完折后 //可能在執(zhí)行這些任務的途中還有新的 //任務加入所以需要繼續(xù)執(zhí)行flushJobs flushIndex = 0; queue.length = 0; flushPostFlushCbs(seen); isFlushing = false; currentFlushPromise = null; if (queue.length || pendingPostFlushCbs.length) { flushJobs(seen); } } }
seen
:這是一個Map
,用于緩存job
的執(zhí)行次數,如果超過了RECURSION_LIMIT
的執(zhí)行次數,將會警用戶。RECURSION_LIMIT
:Vue
默認值為100
。這個值不可以讓用戶修改(常量值)。flushJobs
執(zhí)行流程:獲取queue
隊列中的每一個任務,檢測這個任務是否嵌套執(zhí)行了100
次以上,超過了則警告用戶。然后執(zhí)行當前任務直到flushIndex === queue.length
。(queue的長度可能會持續(xù)增加)。調用flushPostFlushCbs
執(zhí)行后置隊列的任務。- 由于在執(zhí)行后置隊列任務的時候可能又向
queue
中添加了新的任務,那么就需要執(zhí)行完后置隊列后再調用flushJobs
。
5.添加后置任務(queuePostFlushCb)
function queuePostFlushCb(cb) { if (!shared.isArray(cb)) { if ( !activePostFlushCbs || !activePostFlushCbs.includes( cb, cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex ) ) { pendingPostFlushCbs.push(cb); } } else { pendingPostFlushCbs.push(...cb); } queueFlush(); }
- 與添加普通任務到隊列中一樣,添加完成后調用
queueFlush
開啟調度。
6.queuePostRenderEffect
function queueEffectWithSuspense(fn, suspense) { //對suspense的處理,暫時不詳細解釋 if (suspense && suspense.pendingBranch) { if (shared.isArray(fn)) { suspense.effects.push(...fn); } else { suspense.effects.push(fn); } } else { //如果是普通的任務則放入后置隊列 queuePostFlushCb(fn); } }
- 如果傳遞了
suspense
那么調用suspense的api
。 - 沒有傳遞
suspense
當作一般的后置任務即可。
7.執(zhí)行后置隊列任務(flushPostFlushJobs)
function flushPostFlushCbs(seen) { if (pendingPostFlushCbs.length) { //克隆等待執(zhí)行的pendingPost const deduped = [...new Set(pendingPostFlushCbs)]; pendingPostFlushCbs.length = 0; //設置為0 //當前函數是后置隊列的任務發(fā)起的,那么不能 //直接運行任務,而是將任務放到avtivePostFlushCbs任務之后 if (activePostFlushCbs) { activePostFlushCbs.push(...deduped); return; } activePostFlushCbs = deduped; seen = seen || new Map(); //排序(post依然有優(yōu)先級) activePostFlushCbs.sort((a, b) => getId(a) - getId(b)); for ( postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++ ) { //檢測執(zhí)行深度 if (checkRecursiveUpdates(seen, activePostFlushCbs[postFlushIndex])) { continue; } //調用這個postJob activePostFlushCbs[postFlushIndex](); } //初始化 activePostFlushCbs = null; postFlushIndex = 0; } }
flushPostFlushCbs
執(zhí)行流程:和flushJobs
差不多,拿到pendingPostFlushCbs
隊列中的任務并執(zhí)行他們,在執(zhí)行完成后初始化postFulshIndex指針。- 之所以后置隊列一定會在完成普通任務和前置任務后執(zhí)行,是因為無論你是通過
queueJobs
添加任務發(fā)起調度還是通過queuePostFlushCb
添加任務發(fā)起調度,都總是調用flushJobs
,而在flushJobs
的實現中,總是先清空queue
隊列在執(zhí)行pendingPostFlushCbs
。 activePostFlushCbs
作用:想象一個場景,如果我直接通過調用flushPostFlushJobs
發(fā)起調度那么任務將不會是異步的,并且會打亂調度器的執(zhí)行順序,所以有了這個屬性。若當前已經存在了activePostFlushCbs
表示正在執(zhí)行后置隊列的任務,在任務中調用flushPostFlushJobs
并不會直接執(zhí)行,而是會把pendingPostFlushcbs
中的任務放到avtivePostFlushCbs
任務的后面。這樣就保證了調度器的順序執(zhí)行。
8.執(zhí)行前置任務隊列(flushPreFlushCbs)
function flushPreFlushCbs(seen, i = isFlushing ? flushIndex + 1 : 0) { seen = seen || new Map(); for (; i < queue.length; i++) { const cb = queue[i]; if (cb && cb.pre) { if (checkRecursiveUpdates(seen, cb)) { continue; } queue.splice(i, 1); i--; cb(); } } }
- 添加前置任務的方法:對添加的任務函數Job添加pre屬性。
job.pre = true
- 這里需要注意,對于前置任務和普通任務都會被添加到
queue
當中,如果調用的flushJobs
觸發(fā)任務執(zhí)行,那么前置任務和普通任務都會被執(zhí)行。他們的執(zhí)行順序為高優(yōu)先級的先執(zhí)行(id小的先執(zhí)行)。相同優(yōu)先級的前置任務先執(zhí)行。 flushPreFlushCbs
執(zhí)行流程:在queue
中找到帶有pre
屬性的任務,執(zhí)行并在queue
中刪除這個任務。- 對于處于執(zhí)行后置任務的狀態(tài),同時調用了
flushPostFlushCbs
發(fā)起后置任務的調度,那么會將新增的任務加到activePostFlushCbs
中。但是對于前置任務是不需要這么做的,如果通過調用flushPreFlushCbs
發(fā)起調度那么前置任務將會是同步執(zhí)行。我們來看這樣一個例子。
function a(){ console.log(222) } function b(){ console.log(111) } a.pre = true queueJobs(a) queueJobs(b) flushPreFlushCbs() //打印:222 111
- 如何理解呢?首先
a任務
是前置任務,a、b任務
都被添加到了queue
隊列中,同時發(fā)起了調度,但是這是一個微任務,而當前執(zhí)行的任務還未執(zhí)行完成,所以會先調用flushPreFlushCbs
。那么就會調用前置任務也就是a任務
。調用完成后刪除queue隊列中的a任務,此時queue隊列
中只有b任務
了。然后執(zhí)行微任務,進一步調用b任務
。
9.nextTick
- 場景:在修改了響應式數據后,想要獲取到最新DOM上的數據,因為只修改了相應式數據,目前DOM還未發(fā)生改變所以獲取不到改變后的DOM屬性。
<script> import { nextTick } from 'vue' export default { data() { return { count: 0 } }, methods: { async increment() { this.count++ // DOM 還未更新 // 0 console.log(document.getElementById('counter').textContent) await nextTick() // DOM 此時已經更新 console.log(document.getElementById('counter').textContent) // 1 } } } </script> <template> <button id="counter" @click="increment">{{ count }}</button> </template>
nextTick
實現:
function nextTick(fn) { const p = currentFlushPromise || resolvedPromise; return fn ? p.then(this ? fn.bind(this) : fn) : p; }
currentFlushPromise
:在調用queueFlush時會創(chuàng)建一個微任務,將flushJobs推入微任務隊列。
function queueFlush() { if (!isFlushing && !isFlushPending) { isFlushPending = true; currentFlushPromise = resolvedPromise.then(flushJobs); } }
resolvedPromise
:狀態(tài)為fulfilled
的Promise
。- 如果當前隊列中沒有任務則
p=resolvedPromise
,直接將fn
推入微任務隊列。因為調度器隊列中無任務所以不存在DOM的更新。 - 如果當前隊列中有任務則
p=currentFlushPromise
,若當前正在執(zhí)行flushJobs
那么currentFlushPromise
的狀態(tài)為fulfilled
則會將fn
推入微任務隊列,當然前提是flushJobs
已經執(zhí)行完才有可能執(zhí)行fn
,而只要flushJobs
執(zhí)行完畢DOM也已經完成了更新。若當前沒有執(zhí)行flushJobs
,那么currentFlushPromise
的狀態(tài)為pending
,就不可能將fn
推入微任務隊列。綜上就保證了fn
一定在DOM更新后觸發(fā)。
調度器總結
- 調度器的調度隊列分為后置隊列和普通隊列。
- 普通隊列中包含了前置任務和普通任務。如果通過
flushPreFlushCbs
調用那么前置任務為同步任務。執(zhí)行完成后刪除普通隊列中相對應的任務。如果通過flushJobs
調用,那么調用順序按照優(yōu)先級高低排列,相同優(yōu)先級的前置任務先調用。 - 后置隊列任務一定在普通隊列清空后執(zhí)行。
- 普通任務和后置任務為異步,前置任務可能為同步可能為異步。
- 在將任務放入隊列當中時就已經自動發(fā)起了調度,用戶可以不通過手動調用。如果手動調用
flushPostFlushCbs
實際上是將任務放到隊列中,而不是重新開啟調度。
watch用法
- 選項式
<script> export default { watch{ a(){}, b:"meth"http://在methods中聲明的方法 c:{ handler(val,oldVal){}, deep:true,//開啟深度監(jiān)視 immediate:true//立即調用handler }, "d.a":function(){} } } </script>
- 函數式
const callback = ([aOldVal,aVal],[bOldVal,bVal])=>{} //監(jiān)聽源 監(jiān)聽源發(fā)生改變的回調函數 選項 watch(["a","b"], callback, { flush: 'post', onTrack(e) { debugger }, deep:true, immediate:true, })
選項式watch Api的實現
//這一段代碼在Vue3源碼分析(7)中出現過 //不了解的可以看看上一篇文章 //對每一個watch選項添加watcher if (watchOptions) { for (const key in watchOptions) { createWatcher(watchOptions[key], ctx, publicThis, key); } }
- 這里的
watchOptions
就是用戶寫的選項式api的watch對象。
創(chuàng)建watch對象(createWatchr)
function createWatcher(raw, ctx, publicThis, key) { //可以監(jiān)聽深度數據例如a.b.c const getter = key.includes(".") ? createPathGetter(publicThis, key) : () => publicThis[key]; //raw可以是字符串,會讀取methods中的方法 if (shared.isString(raw)) { const handler = ctx[raw]; if (shared.isFunction(handler)) { //進行監(jiān)聽 watch(getter, handler); } else { warn(`Invalid watch handler specified by key "${raw}"`, handler); } } //如果是函數 監(jiān)聽 else if (shared.isFunction(raw)) { watch(getter, raw.bind(publicThis)); } //如果是對象 else if (shared.isObject(raw)) { //數組遍歷,獲取每一個監(jiān)聽器在執(zhí)行createWatcher if (shared.isArray(raw)) { raw.forEach((r) => createWatcher(r, ctx, publicThis, key)); } //對象 else { //handler可能是字符串重ctx上獲取 //也可能是函數 //獲取到handler后調用watch const handler = shared.isFunction(raw.handler) ? raw.handler.bind(publicThis) : ctx[raw.handler]; if (shared.isFunction(handler)) { watch(getter, handler, raw); } else { warn( `Invalid watch handler specified by key "${raw.handler}"`, handler ); } } } else { warn(`Invalid watch option: "${key}"`, raw); } }
- 選項式watch的鍵可以是
"a.b.c"
這樣的形式也可以是普通的"a"
形式,它的值可以是字符串,函數,對象,數組。此函數主要對不同形式的參數做重載。最終都是調用watch
函數。 - 對于鍵為
"a.b"
形式的需要調用createPathGetter
創(chuàng)建一個getter函數,getter函數返回"a.b"
的值。 - 對于值為字符串的我們需要從
methods
中獲取對應的方法。因為之前許多重要屬性都代理到ctx上了所以只需要訪問ctx
即可。 - 對于值為函數的我們只需要將
key
作為watch
的第一個參數,值作為watch
的第二個參數即可。 - 對于值為對象的獲取
handler
作為watch
第二個參數,將raw
作為第三個參數(選項)傳入watch
即可。 - 對于值為數組的,表示需要開啟多個監(jiān)聽,遍歷數組遞歸調用
createWatcher
即可。
選項式watch Api總結
- 對于選項式watch Api本質上還是調用的函數式
watch Api
進行實現的。這里只是做了重載,對于不同的配置傳遞不同的參數給watch
。所以接下來我們重點分析函數式watch Api
的實現。
函數式watch的實現(下面統(tǒng)稱watch)
1.watch
function watch(source, cb, options) { //cb必須是函數 if (!shared.isFunction(cb)) { console.warn(); } return doWatch(source, cb, options); }
source
:監(jiān)聽源,可以是數組(代表監(jiān)聽多個變量)。cb
:監(jiān)聽源發(fā)生改變時,調用的回調函數。options
:watch函數的可選項。- 如果傳遞的cb不是函數需要警告用戶,這可能導致錯誤。
2.doWatch
- 這個函數非常長,也是
watch
的實現核心,我們分多個部分講解。 - 大致原理:收集
source
中響應式元素包裝成getter
,在new ReactiveEffect
中傳遞調用run
方法執(zhí)行getter
就會收集到依賴,然后當觸發(fā)依賴更新的時候就會調用scheduler
,在根據flush
參數,選擇同步執(zhí)行scheduler
還是加入調度器。
function doWatch( source, //getter ()=>[監(jiān)聽的數據] cb, //回調函數 //獲取當前watch的選項 { immediate, deep, flush, onTrack, onTrigger } = shared.EMPTY_OBJ ) { //immediate和deep屬性必須有cb if (!cb) { if (immediate !== undefined) { warn( `watch() "immediate" option is only respected when using the ` + `watch(source, callback, options?) signature.` ); } if (deep !== undefined) { warn( `watch() "deep" option is only respected when using the ` + `watch(source, callback, options?) signature.` ); } } //省略第二部分代碼 }
- 第一部分的代碼主要是檢測參數。對于沒有cb參數但是又有
immediate
和deep
選項的需要警告用戶。
//獲取當前實例 const instance = getCurrentInstance(); let getter; let forceTrigger = false; //強制觸發(fā) let isMultiSource = false; //是否多個數據 //判斷監(jiān)聽的數據是否是ref if (reactivity.isRef(source)) { getter = () => source.value; forceTrigger = reactivity.isShallow(source); } //判斷數據是否是響應式 else if (reactivity.isReactive(source)) { getter = () => source; deep = true; } //判斷數據是否是數組 else if (shared.isArray(source)) { isMultiSource = true; //source中有一個是響應式的 //就需要觸發(fā) forceTrigger = source.some( (s) => reactivity.isReactive(s) || reactivity.isShallow(s) ); //()=>[proxy,()=>proxy,ref] getter = () => source.map((s) => { if (reactivity.isRef(s)) { return s.value; } else if (reactivity.isReactive(s)) { //遍歷響應式對象s 這個getter會作為ReactiveEffect的 //第一個參數,在調用run的時候遍歷所有的值 //確保能讓每一個變量都能收集到effect return traverse(s); } //調用監(jiān)聽的函數 else if (shared.isFunction(s)) { return callWithErrorHandling(s, instance, 2); } else { //提示非法source信息 warnInvalidSource(s); } }); } //省略第三部分代碼
- 如果監(jiān)聽的數據是ref類型,包裝成
getter
形式。 - 如果監(jiān)聽的數據是reactive類型,需要設置為深度監(jiān)聽。
- 如果監(jiān)聽的數據是數組,設置變量
isMultiSource=true
表示當前監(jiān)聽了多個變量,同時判斷監(jiān)聽的所有數據中是否有相應式對象,如果有就必須強制觸發(fā)。設置getter
。 - 我們可以發(fā)現所有的監(jiān)聽數據源都會被包裝成
getter
,這是因為底層都是調用reactivity庫
的watchEffect
,而第一個參數必須是函數,當調用這個函數訪問到的變量都會收集依賴。所以如果當前元素為reactive
元素的時候需要遍歷這個元素的所有值以便所有的變量都能收集到對應的依賴。
//()=>[proxy]傳入的是一個函數 else if (shared.isFunction(source)) { if (cb) { //讓getter為這個函數 getter = () => callWithErrorHandling(source, instance, 2); } else { //如果沒有回調函數 getter = () => { if (instance && instance.isUnmounted) { return; } if (cleanup) { cleanup(); } return callWithAsyncErrorHandling(source, instance, 3, [onCleanup]); }; } } //省略第四部分代碼
- 如果監(jiān)聽的數據是函數,先判斷是否有
cb
,如果有cb
則將監(jiān)聽源函數作為getter
。 - 如果沒有傳遞
cb
,那么這個函數將會作為getter
和回調函數cb。 - 我們來詳細說說
cleanup
的作用。先來看看官方的測試用例:
watch(async (onCleanup) => { const { response, cancel } = doAsyncWork(id.value) // `cancel` 會在 `id` 更改時調用 // 以便取消之前 // 未完成的請求 onCleanup(cancel) data.value = await response })
- 它被用來做副作用清除。第一次調用
getter
的時候是作為收集依賴,所以cleanup
為空不執(zhí)行,然后調用source函數
,在這個函數中會收到onCleanup
的參數,如果你在source
函數中調用了onCleanup
函數那么cleanup
將會被賦值。當id
發(fā)生改變之后再次調用getter函數
(此時作為cb),這時候cleanup
就會被調用,也就是官方說的cancle函數會在id更改時調用。 - 我們繼續(xù)第四部分代碼的分析:
//不是以上情況,讓getter為空函數 else { getter = shared.NOOP; //警告 warnInvalidSource(source); } //省略第五部分代碼
- 這表示沒有需要監(jiān)聽的數據源,將
getter
設置為空函數,同時警告用戶。
const INITIAL_WATCHER_VALUE = {} //getter作為參數傳入ReactiveEffect //調用run的時候會調用getter,確保 //所有的屬性都能夠收集到依賴 if (cb && deep) { const baseGetter = getter; getter = () => traverse(baseGetter()); } let cleanup; //調用effect.stop的時候觸發(fā)這個函數 let onCleanup = (fn) => { cleanup = effect.onStop = () => { callWithErrorHandling(fn, instance, 4); }; }; let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE; //省略第六部分代碼
- 對于含有
deep
屬性的需要深度遍歷,只要在getter中訪問了所有變量的值那么這些值都會收集到依賴。 - 接下來便是
onCleanup
的實現,大家可以按照上面我說的進行理解。 - 我們知道在watch可以監(jiān)聽多個數據,那么對應的cb回調函數的參數要收集到這些改變的值。所以如果監(jiān)聽了多個數據源那么
oldValue
會被設置為數組
否則為對象
。
//回調函數 const job = () => { if (!effect.active) { return; } //傳遞了cb函數 if (cb) { //watch([a,b],()=>{}) //newValue=[a,b] const newValue = effect.run(); //未設置deep屬性的 //舊值和新值要發(fā)生改變才會調用cb回調函數 if ( deep || forceTrigger || (isMultiSource ? newValue.some((v, i) => shared.hasChanged(v, oldValue[i])) : shared.hasChanged(newValue, oldValue)) ) { //這里的作用上面我們已經講過了,不在贅述。 if (cleanup) { cleanup(); } callWithAsyncErrorHandling(cb, instance, 3, [ newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onCleanup, ]); oldValue = newValue; } } else { //沒有cb就只調用getter函數(watchEffect) effect.run(); } }; //省略第七部分代碼
- 這個
job
代表的是要傳遞給Vue調度器的任務,所以這是在創(chuàng)建一個調度器任務。 - 同時還需要注意這個
job
是監(jiān)聽的變量發(fā)生了改變后才會調用。 - 這里的
effect
代表的是ReactiveEffect類的實例
,如果還不了解這個類的請閱讀Vue3源碼分析(2)。 - 如果沒有傳遞
cb
那么會調用effect.run()
這個函數會去執(zhí)行getter函數
。因為沒有傳遞cb
所以回調函數就是getter函數
。 - 如果存在
cb
,那么會先調用getter函數
獲取最新的value
,然后再調用cb
,所以不太建議自己將第一個參數寫成函數,這樣改變值的時候會調用getter和cb兩個函數,如果你在getter中寫了副作用那么就會多次調用。 - 同樣
cleanup
用于清除副作用這里就不再贅述了。
//只要有cb則允許遞歸 job.allowRecurse = !!cb; let scheduler; //設置了sync則同步調度,不放入queue進行異步調度(同步) if (flush === "sync") { scheduler = job; } //設置了post放到DOM渲染之后執(zhí)行(異步) else if (flush === "post") { scheduler = () => queuePostRenderEffect(job, instance && instance.suspense); } //默認值為pre,放入到queue中執(zhí)行(異步) //帶有pre的會在DOM渲染前執(zhí)行 else { job.pre = true; //給當前的job設置優(yōu)先級 if (instance) job.id = instance.uid; scheduler = () => queueJob(job); } //省略第八部分代碼
- 當監(jiān)視的數據發(fā)生改變的時候會調用
job任務
,但是job任務
是異步調用還是同步調用是可以通過flush參數
改變的。 - 當flush為sync的時候:會同步的執(zhí)行
job任務
。 - 當flush為post的時候:會將
job任務
推入后置任務隊列,也就是會等queue隊列任務執(zhí)行完成之后執(zhí)行。 - 當flush為pre的時候:會將
job任務
設置為前置任務,在調用flushPreFlushCbs
的時候執(zhí)行。執(zhí)行完成后刪除這個任務。當然如果一直不調用flushPreFlushCbs
,將會作為普通任務執(zhí)行,這時候就是異步的了。 - 最終
getter
和scheduler
都得到了。他們會作為reactiveEffect
類的兩個參數。第一個為監(jiān)聽的getter函數,在這里面訪問的值都會收集到依賴,當這些監(jiān)聽的值發(fā)生改變的時候就會調用schgeduler
。
const effect = new reactivity.ReactiveEffect(getter, scheduler); //將用戶傳遞的onTrack和onTrigger賦值到effect上 //便于在track和trigger的時候調用 effect.onTrack = onTrack; effect.onTrigger = onTrigger; //省略第九部分代碼
onTrack
:是reactivity庫實現的api
。當被追蹤的時候調用這個函數。onTrigger
:當監(jiān)視的變量改變的時候觸發(fā)的函數。- 創(chuàng)建ReactiveEffect實例對象,對變量進行監(jiān)視。
//調用了watch之后 //需要立刻執(zhí)行getter,處理不同的flush參數 if (cb) { if (immediate) { //有immediate參數立即執(zhí)行job job(); } //否則就只收集依賴調用getter函數 //并且獲取監(jiān)聽的變量 else { oldValue = effect.run(); } } //flush為post需要將收集依賴函數getter //放到postQueue中 else if (flush === "post") { queuePostRenderEffect( effect.run.bind(effect), instance && instance.suspense ); } //沒有設置則收集依賴 else { effect.run(); } //省略第十部分代碼
- 如果含有
immediate
參數則需要立刻執(zhí)行job任務
,否則調用effect.run()
方法(調用getter
)收集依賴。 - 如果
flush
設置為post
那么收集依賴的操作也需要移動到后置隊列當中。
//watch的停止函數,調用后不再依賴更新 return () => { effect.stop(); };
watch
會返回一個方法用于取消監(jiān)聽。
watch總結
- 為了兼容選項式
watch
處理了不同的配置選項最終調用函數式的watch來實現的監(jiān)視效果。 watch
擁有三個參數:source、cb、options
。source
是監(jiān)聽源,可以傳遞函數,值,數組。但是最后都是包裝成getter函數。實現的理念就是通過調用getter函數,訪問響應式變量收集依賴,當響應式數據發(fā)生改變的時候調用cb。options
中比較重要的配置是flush
,他決定了何時收集依賴和觸發(fā)依賴。當flush為post
的時候需要知道收集依賴和觸發(fā)依賴都將會推入到后置隊列當中(DOM更新后觸發(fā))。
以上就是Vue3源碼分析調度器與watch用法原理的詳細內容,更多關于Vue3調度器與watch的資料請關注腳本之家其它相關文章!
相關文章
解決vue項目中頁面調用數據 在數據加載完畢之前出現undefined問題
今天小編就為大家分享一篇解決vue項目中頁面調用數據 在數據加載完畢之前出現undefined問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11VSCode Vue開發(fā)推薦插件和VSCode快捷鍵(小結)
這篇文章主要介紹了VSCode Vue開發(fā)推薦插件和VSCode快捷鍵(小結),文中通過圖文表格介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-08-08