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