一文帶你深入剖析vue3的響應式_第1頁
一文帶你深入剖析vue3的響應式_第2頁
一文帶你深入剖析vue3的響應式_第3頁
一文帶你深入剖析vue3的響應式_第4頁
一文帶你深入剖析vue3的響應式_第5頁
已閱讀5頁,還剩17頁未讀, 繼續免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

第一文帶你深入剖析vue3的響應式緩存功能,如果值沒有變化,就會返回上一次的執行結果在實現這兩個核心功能之前,我們先來改造一下之前實現的effect函數。

怎么能使effect函數變成懶執行呢,比如計算屬性的這種功能,我們不想要他立即執行,而是希望在它需要的時候才執行。

這時候我們可以在effect函數中傳遞第二個參數,一個對象,用來設置一些額外的功能。

functioneffect(fn,options={}){//修改

leteffectFn=function(){

activeEffect=effectFn

effectStack.push(effectFn)

fn()

effectStack.pop()

activeEffect=effectStack[effectStack.length-1]

//只有當非lazy的時候才直接執行

if(!options.lazy){

effectFn()

//將依賴函數組為返回值進行返回

returneffectFn//新增

}

這時,如果傳遞了lazy屬性,那么該effect將不會立即執行,需要手動進行執行:

consteffectFn=effect(()={

console.log(obj.foo)

},{lazy:true})

//手動執行

effectFn()

但是如果我們想要獲取手動執行后的值呢,這時只需要在effect函數中將其返回即可。

functioneffect(fn,options={}){

leteffectFn=function(){

activeEffect=effectFn

effectStack.push(effectFn)

//保存返回值

constres=fn()//新增

effectStack.pop()

activeEffect=effectStack[effectStack.length-1]

returnres//新增

//只有當非lazy的時候才直接執行

if(!options.lazy){

effectFn()

//將依賴函數組為返回值進行返回

returneffectFn

}

接下來開始實現computed函數:

functioncomputed(getter){

//創建一個可手動調用的依賴函數

consteffectFn=effect(getter,{

lazy:true

//當對象被訪問的時候才調用依賴函數

constobj={

getvalue(){

returneffectFn()

returnobj

}

但是此時還做不到對值進行緩存和對比,增加兩個變量,一個存儲執行的值,另一個為一個開關,表示是否可以重新執行依賴函數:

functioncomputed(getter){

//定義value保存執行結果

//isRun表示是否需要執行依賴函數

letvalue,isRun=true;//新增

consteffectFn=effect(getter,{

lazy:true

constobj={

getvalue(){

//增加判斷,isRun為true時才會重新執行

if(isRun){//新增

//保存執行結果

value=effectFn()//新增

//執行完畢后再次重置執行開關

isRun=false//新增

returnvalue

returnobj

}

但是上面的實現還有一個問題,就是好像isRun執行一次后好像永遠都不會變成true了,我們的本意是在數據發生變動的時候需要再次觸發依賴函數,也就是將isRun變為true,實現這種效果,需要我們為options再傳遞一個函數,用于用戶自定義的調度執行。

functioneffect(fn,options={}){

leteffectFn=function(){

activeEffect=effectFn

effectStack.push(effectFn)

constres=fn()

effectStack.pop()

activeEffect=effectStack[effectStack.length-1]

returnres

//掛載用戶自定義的調度執行器

effectFn.options=options//新增

if(!options.lazy){

effectFn()

returneffectFn

}

接下來需要修改一下trigger如果傳遞了scheduler這個函數,那么只執行scheduler這個函數而不執行依賴函數:

functiontrigger(target,key){

letdepsMap=store.get(target)

if(!depsMap)return

consteffects=depsMap.get(key)

leteffectsToRun=newSet()

effectseffects.forEach(effectFn={

if(effectFn!==activeEffect){

effectsToRun.add(effectFn)

effectsToRun.forEach(effect={

//如果存在調度器scheduler,那么直接調用該調度器,并將依賴函數進行傳遞

if(effectFn.options.scheduler){//新增

effectFn.options.scheduler(effect)//新增

}else{

effect()

}

那么在computed中就可以實現重置執行開關isRun的操作了:

functioncomputed(getter){

//定義value保存執行結果

//isRun表示是否需要執行依賴函數

letvalue,isRun=true;//新增

consteffectFn=effect(getter,{

lazy:true,

scheduler(){

if(!isRun){

isRun=true

constobj={

getvalue(){

//增加判斷,isRun為true時才會重新執行

if(isRun){//新增

//保存執行結果

value=effectFn()//新增

//執行完畢后再次重置執行開關

isRun=false//新增

returnvalue

returnobj

}

當computed傳入的依賴函數中的值發生改變時,會觸發響應式對象的trigger函數,而計算屬性創建響應式對象時傳入了scheduler,所以當數據改變時,只會執行scheduler函數,在scheduler函數內我們將執行開關重置為true,再下次訪問數據觸發get函數時,就會重新執行依賴函數。這也就實現了當數據發生改變時,會再次觸發依賴函數的功能了。

為了避免計算屬性被另外一個依賴函數調用而失去響應,我們還需要為計算屬性單獨進行綁定響應式的功能,形成一個effect嵌套。

functioncomputed(getter){

letvalue,isRun=true;

consteffectFn=effect(getter,{

lazy:true,

scheduler(){

if(!isRun){

isRun=true

//當計算屬性依賴的響應式數據發生變化時,手動調用trigger函數觸發響應

trigger(obj,value)//新增

constobj={

getvalue(){

if(isRun){

value=effectFn()

isRun=false

//當讀取value時,手動調用track函數進行追蹤

track(obj,value)

returnvalue

returnobj

}

五.watch

先來看一下watch函數的用法,它的用法也非常簡單:

watch(obj,()={

console.log(改變了)

//修改數據,觸發watch函數

obj.age++

watch接受兩個參數,第一個參數為綁定的響應式數據,第二個參數為依賴函數,我們依然可以沿用之前的思路來進行處理,利用effect以及scheduler來改變觸發執行時機。

functionwatch(source,fn){

effect(

//遞歸讀取對象中的每一項,變為響應式數據,綁定依賴函數

()=bindData(source),

scheduler(){

//當數據發生改變時,調用依賴函數

fn()

//readData保存已讀取過的數據,防止重復讀取

functionbindData(value,readData=newSet()){

//此處只考慮對象的情況,如果值已被讀取/值不存在/值不為對象,那么直接返回

if(typeofvalue!==object||value==null||readData.has(value))return

//保存已讀取對象

readData.add(value)

//遍歷對象

for(constkeyinvalue){

//遞歸進行讀取

bindData(value[key],readData)

returnvalue

}

watch函數還有另外一種用法,就是除了接收對象,還可以接受一個getter函數,例如:

watch(

()=obj.age,

()={

console.log(改變了)

)

這種情況下只需要將用戶傳入的getter將我們自定義的bindData替代即可:

functionwatch(source,fn){

letgetter=typeofsource===functionsource:(()=bindData(source))

effect(

//執行getter

()=getter(),

scheduler(){

//當數據發生改變時,調用依賴函數

fn()

}

其實watch函數還有一個很重要的功能:就是在用戶傳遞的依賴函數中可以獲取新值和舊值,但是我們目前還做不到這一點。實現這個功能我們可以配置前文中的lazy屬性來實現。來回顧一下lazy屬性:設置了lazy之后一開始不會執行依賴函數,手動執行時會返回執行結果:

functionwatch(source,fn){

letgetter=typeofsource===functionsource:(()=bindData(source))

//定義新值與舊值

letnewVal,oldVal;//新增

consteffectFn=effect(

//執行getter

()=getter(),

lazy:true,

scheduler(){

//在scheduler重新執行依賴函數,得到新值

newVal=effectFn()//新增

fn(newVal,oldVal)//新增

//執行完畢后更新舊值

oldVal=newVal//新增

//手動調用依賴函數,取得舊值

oldVal=effectFn()//新增

}

此外,watch函數還有一個功能,就是可以自定義執行時機,比如immediate屬性,他會在創建時立即執行一次:

watch(obj,()={

console.log(改變了)

immediate:true

})

我們可以把scheduler封裝為一個函數,以便在不同的時機去調用他:

functionwatch(source,fn,options={}){

letgetter=typeofsource===functionsource:(()=bindData(source))

letnewVal,oldVal;

construn=()={//新增

newVal=effectFn()

fn(newVal,oldVal)

oldVal=newVal

consteffectFn=effect(

()=getter(),

lazy:true,

//使用run來執行依賴函數

scheduler:run//修改

//當immediate為true時,立即執行一次依賴函數

if(options.immediate){//新增

run()//新增

}else{

oldVal=effectFn()

}

watch函數還支持其他的執行調用時機,這里只實現了immediate。

六.淺響應與深響應

深響應和淺響應的區別:

constobj=reatcive({foo:{bar:1}})

effect(()={

console.log(obj.foo.bar)

//修改obj.foo.bar的值,并不能觸發響應

obj.foo.bar=2

因為之前實現的攔截,無論對于什么類型的數據都是直接進行返回的,如果實現深響應,那么首先應該判斷是否為對象類型的值,如果是對象類型的值,應當遞歸調用reactive方法進行轉換。

//接收第二個參數,標記為是否為淺響應

functioncreateReactive(obj,isShallow=false){

returnnewProxy(obj,{

get(target,key,receiver){

//訪問raw時,返回原對象

if(key===raw)returntarget

track(target,key)

constres=Reflect.get(target,key,receiver)

//如果是淺響應,直接返回值

if(isShallow){

returnres

//判斷res是否為對象并且不為null,循環調用reatcive

if(typeofres===objectres!==null){

returnreatcive(res)

returnres

//...省略其他

})

將創建響應式對象的方法抽離出去,通過傳遞isShallow參數來決定是否創建深響應/淺響應對象。

//深響應

functionreactive(obj){

returncreateReactive(obj)

//淺響應

functionshallowReactive(obj){

returncreateReactive(obj,true)

}

七.淺只讀與深只讀

有時候我們并不需要對值進行修改,也就是需要值為只讀的,這個操作也分為深只讀和淺只讀,首先需要在createReactive函數中增加一個參數isReadOnly,代表是否為只讀屬性。

//淺只讀

functionshallowReadOnly(obj){

returncreateReactive(obj,true,true)

//深只讀

functionreadOnly(obj){

returncreateReactive(obj,false,true)

}

set(target,key,newValue,receiver){

//是否為只讀屬性,如果是則打印警告信息并直接返回

if(isReadOnly){

console.log(`屬性${key}是只讀的`)

returnfalse

constoldVal=target[key]

consttype=Ototype.hasOwnProperty.call(target,key)triggerType.SET:triggerType.ADD

constres=Reflect.set(target,key,newValue,receiver)

if(target===receiver.raw){

if(oldVal!==newValue(oldVal===oldVal||newValue===newValue)){

trigger(target,key,type)

returnres

}

如果為只讀屬性,那么也不需要為其建立響應聯系如果為只讀屬性,那么在進行深層次遍歷的時候,需要調用readOnly函數對值進行包裝

functioncreateReactive(obj,isShallow=false,isReadOnly=false){

returnnewProxy(obj,{

get(target,key,receiver){

//訪問raw時,返回原對象

if(key===raw)returntarget

//只有在非只讀的時候才需要建立響應聯系

if(!isReadOnly){

track(target,key)

constres=Reflect.get(target,key,receiver)

//如果是淺響應,直接返回值

if(isShallow){

returnres

//判斷res是否為對象并且不為null,循環調用creative

if(typeofres===objectres!==null){

//如果數據為只讀,則調用readOnly對值進行包裝

returnisReadOnlyreadOnly(res):creative(res)

returnres

}

八.處理數組

數組的索引與length

如果操作數組時,設置的索引值大于數組當前的長度,那么要更新數組的length屬性,所以當通過索引設置元素值時,可能會隱式的修改length的屬性值,因此再j進行觸發響應時,也應該觸發與length屬性相關聯的副作用函數重新執行。

constarr=reactive([foo])//數組原來的長度為1

effect(()={

console.log(arr.length)//1

//設置索引為1的值,會導致數組長度變為2

arr[1]=bar

在判斷操作類型時,新增對數組類型的判斷,如果代理目標是數組,那么對于操作類型的判斷作出處理:

如果設置的索引值小于數組的長度,就視為SET操作,因為他不會改變數組長度,如果設置的索引值大于當前數組的長度,那么應該被視為ADD操作。

//定義常量,便于修改

consttriggerType={

ADD:add,

SET:set

set(target,key,newValue,receiver){

if(isReadOnly){

console.log(`屬性${key}是只讀的`)

returnfalse

constoldVal=target[key]

//如果目標對象是數組,檢測被設置的索引值是否小于數組長度

consttype=Array.isArray(target)(Number(key)target.lengthtriggerType.ADD:triggerType.SET)

constres=Reflect.set(target,key,newValue,receiver)

trigger(target,key,type)

returnres

},

functiontrigger(target,key,type){

constdepsMap=store.get(target)

if(!depsMap)return

consteffects=depsMap.get(key)

leteffectsToRun=newSet()

effectseffects.forEach(effectFn={

if(effectFn!==activeEffect){

effectsToRun.add(effectFn)

//當操作類型是ADD并且目標對象時數組時,應該取出執行那些與length屬性相關的副作用函數

if(Array.isArray(target)type===triggerType.ADD){

//取出與length相關的副作用函數

constlengthEffects=deps.get(length)

lengthEffectslengthEffects.forEach(effectFn={

if(effectFn!==activeEffect){

effectsToRun.add(effectFn)

effectsToRun.forEach(effect={

if(effectFn.options.scheduler){

effectFn.options.scheduler(effect)

}else{

effect()

}

還有一點:其實修改數組的length屬性也會隱式的影響數組元素:

constarr=reactive([foo])

effect(()={

//訪問數組的第0個元素

console.log(arrr[0])//foo

//將數組的長度修改為0,導致第0個元素被刪除,因此應該觸發響應

arr.length=0

如上所示,在副作用函數內部訪問了第0個元素,然后將數組的length屬性修改為0,這回隱式的影響數組元素,及所有的元素都會被刪除,所以應該觸發副作用函數重新執行。

然而并非所有的對length屬性值的修改都會影響數組中的已有元素,如果設置的length屬性為100,這并不會影響第0個元素,當修改屬性值時,只有那些索引值大于等于新的length屬性值的元素才需要觸發響應。

調用trigger函數時傳入新值:

set(target,key,newValue,receiver){

if(isReadOnly){

console.log(`屬性${key}是只讀的`)

returnfalse

constoldVal=target[key]

//如果目標對象是數組,檢測被設置的索引值是否小于數組長度

consttype=Array.isArray(target)(Number(key)target.lengthtriggerType.ADD:triggerType.SET)

constres=Reflect.set(target,key,newValue,receiver)

//將新的值進行傳遞,及觸發響應的新值

trigger(target,key,type,newValue)//新增

returnres

}

判斷新的下標值與需要操作的新的下標值進行判斷,因為數組的key為下標,所以副作用函數搜集器是以下標作為key值的,當length發生變動時,只需要將新值與每個下標的key判斷,大于等于新的length值的需要重新執行副作用函數。

如上圖所示,Map為根據數組的key,也就是id組成的Map結構,他們的每一個key都對應一個Set,用于保存這個key下面的所有的依賴函數。

當length屬性發生變動時,應當取出所有key值大于等于length值的所有依賴函數進行執行。

functiontrigger(target,key,type,newValue){

constdepsMap=store.get(target)

if(!depsMap)return

consteffects=depsMap.get(key)

leteffectsToRun=newSet()

effectseffects.forEach(effectFn={

if(effectFn!==activeEffect){

effectsToRun.add(effectFn)

//如果操作目標是數組,并且修改了數組的length屬性

if(Array.isArray(target)key===length){

//對于索引值大于或等于新的length元素

//需要把所有相關聯的副作用函數取出并添加到effectToRun中待執行

depsMap.forEach((effects,key)={

//key與newValue均為數組下標,因為數組中key為index

if(key=newValue){

effects.forEach(effectFn={

if(effectFn!==activeEffect){

effectsToRun.add(effectFn)

//...省略

}

本文的實現數組這種數據結構只考慮了針對長度發生變化的情況。

九.ref

由于Proxy的代理目標是非原始值,所以沒有任何手段去攔截對原始值的操作:

letstr=hi

//無法攔截對值的修改

str=pino

解決方法是:使用一個非原始值去包裹

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論