一文帶你了解vue3.0響應式
我們知道Vue 2.0是利用Ojbect.defineProperty對對象的已有屬性值的讀取和修改進行劫持,但是這個API不能監(jiān)聽對象屬性的新增和刪除,此外為了深度劫持對象的內(nèi)部屬性,必須在初始化的時候?qū)?nèi)部屬性進行遞歸調(diào)用Ojbect.defineProperty,這就造成了一個性能上的消耗。為了解決這些問題,Vue 3.0利用Proxy重寫了響應式邏輯并且優(yōu)化了相關性能。
使用案例
我們先來個示例看下Vue 3.0的響應式API的寫法:

changePerson能改變響應式數(shù)據(jù)person的值,person值的變化會觸發(fā)組件重新渲染而更新DOM。
這里我們可以看到Vue 3.0的使用中,開發(fā)者利用reactive函數(shù)自己去確定哪些數(shù)據(jù)為響應式數(shù)據(jù),這樣就可以避免一些不必要的響應式的性能消耗。例如案例中我們就不需要讓nowIndex成為響應式數(shù)據(jù)。(當然Vue 2.0也可以在data函數(shù)外定義數(shù)據(jù),這樣也是非響應式數(shù)據(jù))
我們接下來看看reactive函數(shù)的實現(xiàn)原理!
reactive API相關的流程
reactive

代碼說明:
- 1.如果目標對象
target是readonly對象,直接返回目標對象,因為readonly對象不能設置成響應式對象 - 2.調(diào)用
createReactiveObject函數(shù)繼續(xù)流程。
createReactiveObject 創(chuàng)建響應式對象

代碼說明:
- 1.如果目標對象不是數(shù)據(jù)或者對象,則直接返回對象,在開發(fā)環(huán)境給出錯誤警告提示。
- 2.如果
target已經(jīng)是一個Proxy對象,則直接返回target, (target['__v_raw']設計非常巧妙:如果target是Proxy對象,target['__v_raw']觸發(fā)get方法,在緩存對象reactiveMap中查找是否target對象的Proxy對象是否等于target自身)。這里處理了一個例外,如果是給響應式對象執(zhí)行readonly函數(shù)則需要繼續(xù)。 - 3.在
reactiveMap中查找是否已經(jīng)有了對應的Proxy對象,則直接返回對應的Proxy對象。 - 4.確保只有特定的數(shù)據(jù)能變成響應式,否則直接返回
target。響應式白名單如下所示:
1.target沒有被執(zhí)行過markRaw方法,或者說target對象沒有__v_skip屬性值或者__v_skip屬性的值為false;
2.target不能是不可擴展對象,即target沒有被執(zhí)行過preventExtensions,seal和freeze這些方法;
3.target為Object或者Array;
4.target為Map,Set,WeakMap,WeakSet;
- 5.通過使用
Proxy函數(shù)劫持target對象,返回的結果即為響應式對象了。這里的處理函數(shù)會根據(jù)target對象不同而不同(這兩個函數(shù)都是參數(shù)傳入的):
1.Object或者Array的處理函數(shù)是collectionHandlers;
2.Map,Set,WeakMap,WeakSet的處理函數(shù)是baseHandlers;
- 6.將響應式對象存入
reactiveMap中緩存起來,key是target,value是proxy。
mutableHandlers 處理函數(shù)

我們知道訪問對象屬性會觸發(fā)get函數(shù),設置對象屬性會觸發(fā)set函數(shù),刪除對象屬性會觸發(fā)deleteProperty函數(shù),in操作符會觸發(fā)has函數(shù),getOwnPropertyNames會觸發(fā)ownKeys函數(shù)。我們接下來看看你這幾個函數(shù)的代碼邏輯。
get函數(shù)
由于沒有傳參,isReadonly和shallow都是默認參數(shù)false。

代碼邏輯:
- 1.如果獲取
__v_isReactive屬性,返回true, 表示target已經(jīng)是一個響應式對象了; - 2.獲取
__v_isReadonly屬性,返回false;(readonly是響應式的另外一個API,暫不解釋) - 3.獲取
__v_raw屬性,返回target本身,這個屬性用來判斷target是否已經(jīng)是響應式對象; - 4.如果target是數(shù)組,且命中了一些屬性,例如includes, indexOf, lastIndexOf等,則執(zhí)行的是數(shù)組的這些函數(shù)方法,并對數(shù)組的每個元素執(zhí)行收集依賴track(
arr,TrackOpTypes.GET,i +''),然后通過Reflect獲取數(shù)組函數(shù)的值; - 5.Reflect求值;
- 6.判斷是否是特殊的屬性值:
symbol,__proto__,__v_isRef,__isVue, 如果是直接返回前面得到的res,不做后續(xù)處理; - 7.執(zhí)行收集依賴;
- 8.如果是ref, 如果target不是數(shù)組或者key不是整數(shù),就執(zhí)行數(shù)據(jù)拆包,這里涉及到另外一個響應式APIref, 暫不解釋;
- 9.如果res是對象,遞歸執(zhí)行reactive,把res變成響應式對象。這里是一個優(yōu)化小技巧,只有屬性值被訪問后才會被被劫持,避免了初始化就全劫持的性能消耗。
get函數(shù)的的調(diào)用時機
回答這個問題前我們需要回到前面一篇關于setup的文章—揭開Vue3.0 setup函數(shù)的神秘面紗。
- 在
setupStatefulComponent函數(shù)中會執(zhí)行setup()函數(shù),并得到執(zhí)行結果:

handleSetupResult處理結果的邏輯是間隔setupResult賦值給instance.setupState:

- 這個
instance.setupState被instance.ctx代理,所以訪問和修改instance.ctx就能直接訪問和修改instance.setupState:

- 我們以前提到過渲染生成子樹
VNode就是調(diào)用render函數(shù),我們用模板編譯看看我們例子中的render函數(shù)長啥樣子?

- 很清晰了,當渲染模板的時候,會從
ctx中取person屬性對象,其實就是取setupState的person屬性對象。當取setupState的person屬性對象的name,age,address時都會觸發(fā)get函數(shù)的調(diào)用,獲取對應的值。
總結:組件實例對象執(zhí)行render函數(shù)生成子樹VNode時,會調(diào)用響應式對象的get函數(shù)。
track 收集依賴
我們上面的get函數(shù)的代碼解釋中兩次提到了收集依賴,那什么是收集依賴呢?
要實現(xiàn)響應式,就是當數(shù)據(jù)變化后會自動實現(xiàn)一些功能,比如執(zhí)行某些函數(shù)等。因為副作用渲染函數(shù)能觸發(fā)組件的重新渲染而更新DOM,所以這里收集的依賴就是當數(shù)據(jù)變化后需要執(zhí)行的副作用渲染函數(shù)。
也就是說,當執(zhí)行get函數(shù)時就會收集對應組件的副作用渲染函數(shù)。


我們可以拿我們的例子說明最后的結果:

set函數(shù)

代碼邏輯:
- 1.如果值沒有變化,直接返回;
- 2.通過
Reflect設置新值; - 3.不是原型鏈上的屬性,如果是新增屬性執(zhí)行
add類型的trigger,如果是修改屬性執(zhí)行set類型的trigger。(如果Reflect.set原型鏈上的屬性會再次調(diào)用setter,所以不用兩次執(zhí)行trigger)。
trigger 分發(fā)依賴

trigger代碼邏輯很清晰,就是從get函數(shù)中收集來的依賴targetMap中找到對應的函數(shù),然后執(zhí)行這些副作用渲染函數(shù),更新DOM。
get和副作用渲染函數(shù)關聯(lián)
我們回過頭來再解答一個疑問:就是從get函數(shù)中收集來的副作用渲染函數(shù)是怎么確定的,即訪問person.name時如何確定關聯(lián)哪個副作用渲染函數(shù)呢?
我們接下來一步步梳理其中的邏輯:
- 組件掛載
mountComponent最后一步是執(zhí)行帶副作用的渲染函數(shù):

setupRenderEffect先定義了一個componentUpdateFn組件渲染函數(shù),然后將這個componentUpdateFn封裝在了ReactiveEffect中,并將ReactiveEffect對象的run方法賦值給組件對象的update屬性,然后執(zhí)行update方法,其實就是執(zhí)行ReactiveEffect對象的run方法。

ReactiveEffect的run方法持有了傳入的函數(shù),當前場景為componentUpdateFn組件渲染函數(shù),并且利用了兩個全局的變量effectStack和activeEffect。- 在執(zhí)行run方法時先將
componentUpdateFn賦值給activeEffect,并且壓入effectStack棧中,然后執(zhí)行componentUpdateFn方法。當執(zhí)行完成后componentUpdateFn出棧,并且賦值activeEffect為新的棧頂?shù)暮瘮?shù)。

componentUpdateFn執(zhí)行的時候會調(diào)用renderComponentRoot,本質(zhì)是執(zhí)行組件實例對象的render方法。

- 目前為止就到了本文的內(nèi)容了,render方法中如果訪問相應式數(shù)據(jù)就會觸發(fā)get函數(shù),get中收集的就是

這里設計一個棧的結構,主要是為了解決effect嵌套的問題。
副作用渲染函數(shù)的執(zhí)行過濾
如果仔細思考下可能會有一個疑問?name,age,address都修改了,然后他們都關聯(lián)了同一個渲染函數(shù),理論上同時修改這三個值會觸發(fā)三次組件重新渲染呢,這明顯是不合理的。那Vue是如何控制只執(zhí)行一次呢?
- 我們需要再次回到
ReactiveEffect封裝componentUpdateFn渲染函數(shù)的地方,我們先看一眼第二個參數(shù)scheduler:

- 派發(fā)依賴的時候如果有
scheduler則會執(zhí)行scheduler:

queueJob的執(zhí)行邏輯是如果任務在隊列中就過濾掉不執(zhí)行。

結尾
本文詳細介紹了Vue3.0的相應式原理:利用Proxy劫持對象,訪問對象的時候會觸發(fā)get方法,此時會進行依賴的收集;當修改對象數(shù)據(jù)的時候會觸發(fā)set方法,此時會派發(fā)依賴,即調(diào)用組件的副作用渲染函數(shù)(其實不限于), 這樣組件就能重新渲染,DOM更新。
到此這篇關于一文帶你了解vue3.0響應式的文章就介紹到這了,更多相關vue3.0響應式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Vue2+SpringBoot實現(xiàn)數(shù)據(jù)導出到csv文件并下載的使用示例
本文主要介紹了Vue2+SpringBoot實現(xiàn)數(shù)據(jù)導出到csv文件并下載,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-10-10
詳解Vue的數(shù)據(jù)及事件綁定和filter過濾器
這篇文章主要為大家介紹了Vue的數(shù)據(jù)及事件綁定和filter過濾器,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-01-01
Elemenu中el-table中使用el-popover選中關閉無效解決辦法(最新推薦)
這篇文章主要介紹了Elemenu中el-table中使用el-popover選中關閉無效解決辦法(最新推薦),因為在el-table-column里,因為是多行,使用trigger="manual"?時,用v-model="visible"來控制時,控件找不到這個值,才換成trigger="click",需要的朋友可以參考下2024-03-03
Vue3報錯‘defineProps‘?is?not?defined的解決方法
最近工作中遇到vue3中使用defineProps中報錯,飄紅,所以這篇文章主要給大家介紹了關于Vue3報錯‘defineProps‘?is?not?defined的解決方法,需要的朋友可以參考下2023-01-01
vue-router 源碼之實現(xiàn)一個簡單的 vue-router
這篇文章主要介紹了vue-router 源碼之實現(xiàn)一個簡單的 vue-router,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-07-07

