深入理解Vue3 Reactivity API
一些基礎內容,可作為文檔參考。不行了,實在寫不下去了,到後來每輸入一個字符我都要等將近20秒。 。 。 。 。卡的要死。 。 。 。 。 。
TOC:
- effect()和reactive()
- shallowReactive()
- readonly()
- shallowReadonly()
- isReactive()
- isReadonly()
- isProxy()
- markRaw()
- 哪些數據是可以被代理的
- markRaw()函數用於讓數據不可被代理
- toRaw()
- ReactiveFlags
- 調度執行effect - scheduler
- watchEffect()
- 異步副作用和invalidate
- 停止一個副作用(effect)
- watchEffect()與effect()的區別
- track()與trigger()
- ref()
- isRef()
- toRef()
- toRefs()
- 自動脫ref
- customRef()
- shallowRef()
- triggerRef()
- unref()
- Lazy的effect()
- computed()
- effect的其他選項onTrack和onTrigger
effect() 和reactive()
import{effect,reactive}from'@vue/reactivity'// 使用 reactive() 函数定义响应式数据
constobj=reactive({text:'hello'})// 使用 effect() 函数定义副作用函数
effect(()=>{document.body.innerText=obj.text})// 一秒后修改响应式数据,这会触发副作用函数重新执行
setTimeout(()=>{obj.text+=' world'},1000)
reactive()
函數接收一個對像作為參數,並返回一個代理對象。-
effect()
函數用於定義副作用,它的參數就是副作用函數,這個函數可能會產生副作用,例如上面代碼中的document.body.innerText = obj.text 。在副作用函數內的響應式數據會與副作用函數之間建立聯繫,即所謂的依賴收集,當響應式數據變化之後,會導致副作用函數重新執行。
shallowReactive()
定義淺響應數據:
import{effect,shallowReactive}from'@vue/reactivity'// 使用 shallowReactive() 函数定义浅响应式数据
constobj=shallowReactive({foo:{bar:1}})effect(()=>{console.log(obj.foo.bar)})obj.foo.bar=2// 无效
obj.foo={bar:2}// 有效
readonly()
有些數據,我們要求對用戶是只讀的,此時可以使用readonly()
函數,它的用法如下:
import{readonly}from'@vue/reactivity'// 使用 reactive() 函数定义响应式数据
constobj=readonly({text:'hello'})obj.text+=' world'// Set operation on key "text" failed: target is readonly.
shallowReadonly()
類似於淺響應,shallowReadonly()定義淺只讀數據,這意味著,深層次的對象值是可以被修改的,在Vue
內部props
就是使用shallowReadonly()
函數來定義的,用法如下:
import { effect , shallowReadonly } from '@vue/reactivity' // 使用shallowReadonly() 函数定义浅只读数据const obj = shallowReadonly ({ foo : { bar : 1 } }) obj . foo = { bar : 2 } // Warn obj . foo . bar = 2 // OK
isReactive()
判斷數據對像是否是reactive:
import{isReactive,reactive,readonly,shallowReactive,shallowReadonly}from'@vue/reactivity'constreactiveProxy=reactive({foo:{bar:1}})console.log(isReactive(reactiveProxy))// trueconsole.log(isReactive(reactiveProxy.foo))// trueconstshallowReactiveProxy=shallowReactive({foo:{bar:1}})console.log(isReactive(shallowReactiveProxy))// trueconsole.log(isReactive(shallowReactiveProxy.foo))// falseconstreadonlyProxy=readonly({foo:1})console.log(isReactive(readonlyProxy))// falseconstshallowReadonlyProxy=shallowReadonly({foo:1})console.log(isReactive(shallowReadonlyProxy))// false
isReadonly()
用於判斷數據是否是readonly:
import { isReadonly , reactive , readonly , shallowReactive , shallowReadonly } from '@vue/reactivity' console . log ( isReadonly ( readonly ({}))) // true console . log ( isReadonly ( shallowReadonly ({}))) // true console . log ( isReadonly ( reactive ({}))) // false console . log ( isReadonly ( shallowReactive ({}))) // false
isProxy()
用於判斷對像是否是代理對象(reactive 或readonly):
import{isProxy,reactive,readonly,shallowReactive,shallowReadonly}from'@vue/reactivity'console.log(isProxy(readonly({})))// trueconsole.log(isProxy(shallowReadonly({})))// trueconsole.log(isProxy(reactive({})))// trueconsole.log(isProxy(shallowReactive({})))// trueconstshallowReactiveProxy=shallowReactive({foo:{}})console.log(isProxy(shallowReactiveProxy))// trueconsole.log(isProxy(shallowReactiveProxy.foo))// falseconstshallowReadonlyProxy=shallowReadonly({foo:{}})console.log(isProxy(shallowReadonlyProxy))// trueconsole.log(isProxy(shallowReadonlyProxy.foo))// false
markRaw()
- 哪些數據是可以被代理的:
- Object 、Array、Map、Set、WeakMap、WeakSet
- 非
Object.isFrozen
:
const obj = { foo : 1 } Object . freeze ( obj ) // Object.isFrozen(obj) ==> true // proxyObj === obj const proxyObj = reactiev ( obj )
- 非VNode,Vue3的VNode對象帶有
__v_skip: true
標識,用於跳過代理(實際上,只要帶有__v_skip
屬性並且值為true
的對象,都不會是被代理),例如:
// obj 是原始数据对象
constobj=reactive({foo:0,__v_skip:true})
- markRaw()函數用於讓數據不可被代理:
實際上markRaw
函數所做的事情,就是在數據對像上定義__v_skip
屬性,從而跳過代理:
import{markRaw}from'@vue/reactivity'constobj={foo:1}markRaw(obj)// { foo: 1, __v_skip: true }
toRaw()
接收代理對像作為參數,並獲取原始對象:
import { toRaw , reactive , readonly } from '@vue/reactivity' const obj1 = {} const reactiveProxy = reactive ( obj1 ) console . log ( toRaw ( reactiveProxy ) === obj1 ) // true const obj2 = {} const readonlyProxy = readonly ( obj2 ) console . log ( toRaw ( readonlyProxy ) === obj2 ) // true
如果參數是非代理對象,則直接該值:
import { toRaw } from '@vue/reactivity' const obj1 = {} console . log ( toRaw ( obj1 ) === obj1 ) // true console . log ( toRaw ( 1 ) === 1 ) // true console . log ( toRaw ( 'hello' ) === 'hello' ) // true
ReactiveFlags
ReactiveFlags
是一個枚舉值:
它的定義如下:
exportconstenumReactiveFlags{skip='__v_skip',isReactive='__v_isReactive',isReadonly='__v_isReadonly',raw='__v_raw',reactive='__v_reactive',readonly='__v_readonly'}
它有什麼用呢?舉個例子,我們要定義一個不可被代理的對象:
import{ReactiveFlags,reactive,isReactive}from'@vue/reactivity'constobj={[ReactiveFlags.skip]:true}constproxyObj=reactive(obj)console.log(isReactive(proxyObj))// false
實際上markRaw()
函數就是使用類似的方式實現的。所以我們不必像如上代碼那麼做,但是在一些高級場景或許會用到這些值。
下面簡單介紹一下ReactiveFlags
中各個值得作用:
- 代理對象會通過
ReactiveFlags.raw
引用原始對象 - 原始對象會通過
ReactiveFlags.reactive
或ReactiveFlags.readonly
引用代理對象 - 代理對像根據它是
reactive
或readonly
的,將ReactiveFlags.isReactive
或ReactiveFlags.isReadonly
屬性值設置為true
。
調度執行effect - scheduler
來看下面的例子:
constobj=reactive({count:1})effect(()=>{console.log(obj.count)})obj.count++obj.count++obj.count++
定義響應式對象obj
,並在effect
內讀取它的值,這樣effect
與數據之間就會建立“聯繫”,接著我們連續三次修改obj.count
的值,會發現console.log
語句共打印四次(包括首次執行)。
想像一下,假如我們只需要把數據的最終的狀態應用到副作用中,而不是每次變化都重新執行一次副作用函數,這將對性能有所提升。實際上我們可以為effect
傳遞第二個參數作為選項,可以指定“調度器”。所謂調度器就是用來指定如何運行副作用函數的:
constobj=reactive({count:1})effect(()=>{console.log(obj.count)},{//指定調度器為queueJobscheduler:queueJob})//調度器實現constqueue:Function[]=[]letisFlushing=falsefunctionqueueJob(job:()=>void){if(!queue.includes(job))queue.push(job)if(!isFlushing){isFlushing=truePromise.resolve().then(()=>{letfnwhile(fn=queue.shift()){fn()}})}}obj.count++obj.count++obj.count++
我們指定effect
的調度器為queueJob
, job
實際上就是副作用函數,我們將副作用函數緩衝到queue
隊列中,並在microtask
中刷新隊列,由於隊列不會重複緩衝相同的job
,因此最終只會執行一次副作用函數。
這實際上就是watchEffect()
函數的實現思路。
watchEffect()
watchEffect()
函數並不在@vue/reactivity
中提供,而是在@vue/runtime-core
中提供,與watch()
函數一起對外暴露。
constobj=reactive({foo:1})watchEffect(()=>{console.log(obj.foo)})obj.foo++obj.foo++obj.foo++
這與我們上面剛剛實現的自定義調度器的effect
的效果實際上是一樣的。
異步副作用和invalidate
異步副作用是很常見的,例如請求API 接口:
watchEffect(async()=>{constdata=awaitfetch(obj.foo)})
當obj.foo
變化後,意味著將會再次發送請求,那麼之前的請求怎麼辦呢?是否應該將之前的請求標記為invalidate
?
實際上,副作用函數接收一個函數作為參數:
watchEffect(async(onInvalidate)=>{constdata=awaitfetch(obj.foo)})
我們可以調用它來註冊一個回調函數,這個回調函數會在副作用無效時執行:
watchEffect(async(onInvalidate)=>{letvalidate=trueonInvalidate(()=>{validate=false})constdata=awaitfetch(obj.foo)if(validate){/* 正常使用 data */}else{/* 说明当前副作用已经无效了,抛弃即可 */}})
如果不拋棄無效的副作用,那麼就會產生竟態問題。實際上,我們很容易就能通過封裝effect() 函數支持註冊“無效回調”的功能:
import { effect } from '@vue/reactivity' function watchEffect ( fn : ( onInvalidate : ( fn : () => void ) => void ) => void ) { let cleanup : Function function onInvalidate ( fn : Function ) { cleanup = fn } // 封装一下effect // 在执行副作用函数之前,先使上一次无作用无效effect (() => { cleanup && cleanup () fn ( onInvalidate ) }) }
如果我們再加上調用器,那實際上就非常接近watchEffect
的真實實現了。
什麼時候需要invalidate 掉一個副作用函數呢?
- 在組件中定義的effect,需要在組件卸載時將其invalidate
- 在數據變化導致effect 重新執行時,需要invalidate 掉上一次的effect 執行
- 用戶手動stop 一個effect 時
停止一個副作用(effect)
@vue/reactivity
提供了stop
函數用來停止一個副作用:
import{stop,reactive,effect}from'@vue/reactivity'constobj=reactive({foo:1})construnner=effect(()=>{console.log(obj.foo)})// 停止一个副作用
stop(runner)obj.foo++obj.foo++
effect()
函數會返回一個值,這個值其實就是effect
本身,我們通常命名它為runner
。
把這個runner
傳遞給stop()
函數,就可以停止掉這個effect
。後續對數據的變更不會觸發副作用函數的重新執行。
watchEffect() 與effect() 的區別
effect()
函數來自於@vue/reactivity
,而watchEffect()
函數來自於@vue/runtime-core
。它們的區別在於: effect()
是非常底層的實現, watchEffect()
是基於effect()
的封裝, watchEffect()
會維護與組件實例以及組件狀態(是否被卸載等)的關係,如果一個組件被卸載,那麼watchEffect()
也將被stop
,但effect()
則不會。舉個例子:
- watchEffect():
constobj=reactive({foo:1})constComp=defineComponent({setup(){watchEffect(()=>{console.log(obj.foo)})return()=>''}})//掛載組件render(h(Comp),document.querySelector('#app')!)//卸載組件render(null,document.querySelector('#app')!)obj.foo++//副作用函數不會重新執行
我們先掛載了組件,接著又卸載了組件,最後修改obj.foo
的值,並不會導致watchEffect
的副作用函數重新執行。
- effect()
constobj=reactive({foo:1})constComp=defineComponent({setup(){effect(()=>{console.log(obj.foo)})return()=>''}})//渲染組件render(h(Comp),document.querySelector('#app')!)//卸載組件render(null,document.querySelector('#app')!)obj.foo++
但effect()
的副作用函數仍然會被執行,但我們可以藉助onUnmounted
API解決這個問題:
constobj=reactive({foo:1})constComp=defineComponent({setup(){construnner=effect(()=>{console.log(obj.foo)})//組件卸載時,stop掉effectonUnmounted(()=>stop(runner))return()=>''}})//渲染組件render(h(Comp),document.querySelector('#app')!)//卸載組件render(null,document.querySelector('#app')!)obj.foo++
當然,在普通開發中不推薦直接用effect()
啦,使用watchEffect()
就好了。
track() 與trigger()
track()
和trigger()
是依賴收集的核心, track()
用來跟踪收集依賴(收集effect
), trigger()
用來觸發響應(執行effect
),它們需要配合effect()
函數使用:
constobj={foo:1}effect(()=>{console.log(obj.foo)track(obj,TrackOpTypes.GET,'foo')})obj.foo=2trigger(obj,TriggerOpTypes.SET,'foo')
如上代碼所示, obj
是一個普通的對象,注意它並非是響應式對象。接著使用effect()
函數定義了一個副作用函數,讀取並打印obj.foo
的值,由於obj
是一個普通對象,因此它並沒有收集依賴的能力,為了收集到依賴,我們需要手動調用track()
函數, track()
函數接收三個參數:
- target:要跟踪的目標對象,這裡就是
obj
- 跟踪操作的類型:
obj.foo
是讀取對象的值,因此是'get'
- key:要跟踪目標對象的
key
,我們讀取的是foo
,因此key
是foo
這樣,我們本質上是手動建立一種數據結構:
// 伪代码
map:{[target]:{[key]:[effect1,effect2....]}}
簡單的理解, effect
與對象和具體操作的key
,是以這種映射關係建立關聯的:
[target]
----> key1
----> [effect1, effect2...]
[target]
----> key2
----> [effect1, effect3...]
[target2]
----> key1
----> [effect5, effect6...]
既然effect
與目標對象target
已經建立了聯繫,那麼當然就可以想辦法通過target
----> key
進而取到effect
,然後執行它們,而這就是trigger()
函數做的事情,所以在調用trigger
函數時我們要指定目標對象和相應的key
值:
trigger(obj,TriggerOpTypes.SET,'foo')
這大概就是依賴收集的原理,但是這個過程是可以自動完成的,而不需要開發者手動調用track()
和trigger()
函數,想要自定完成依賴收集,那麼就需要攔截諸如:設置、讀取等對值得操作方法才行。至於實現方式,無論是Object.defineProperty
還是Proxy
那就是具體的技術形式了。
ref()
reactive()
函數可以代理一個對象,但不能代理基本類型值,例如字符串、數字、 boolean
等,這是js
語言的限制,因此我們需要使用ref()
函數來間接對基本類型值進行處理:
constrefVal=ref(0)refVal.value// 0
ref
是響應式的:
constrefVal=ref(0)effect(()=>{console.log(refVal.value)})refVal.value=1// 触发响应
已經了解了track()
和trigger()
函數的你,仔細思考一下,實現ref()
函數是不是非常簡單呢:
functionmyRef(val:any){letvalue=valconstr={isRef:true,//隨便加個標識以示區分getvalue(){//收集依賴track(r,TrackOpTypes.GET,'value')returnvalue},setvalue(newVal:any){if(newVal!==value){value=newVal//觸發響應trigger(r,TriggerOpTypes.SET,'value')}}}returnr}
現在去試試我們的myRef()
函數吧:
constrefVal=myRef(0)effect(()=>{console.log(refVal.value)})refVal.value=1
一切OK。
isRef()
我們在實現myRef()
函數時,可以看到為ref
對象添加了一個標識isRef: true
。因此我們可以封裝一個函數isRef()
函數來判斷一個值是不是ref
:
functionisRef(val){returnval.isRef}
實際上在Vue
3中使用的標識是__v_isRef
,這無關緊要嘛。
toRef()
丟失響應,式reactivity api 的一個問題:
const obj = reactive ({ foo : 1 }) // obj 是响应式数据const obj2 = { foo : obj . foo } effect (() => { console . log ( obj2 . foo ) // 这里读取obj2.foo }) obj . foo = 2 // 设置obj.foo 显然无效
為了解決這個問題,我們可以使用toRef()
函數:
const obj = reactive ({ foo : 1 }) const obj2 = { foo : toRef ( obj , 'foo' ) } // 修改了这里effect (() => { console . log ( obj2 . foo . value ) // 由于obj2.foo 现在是一个ref,因此要访问.value }) obj . foo = 2 // 有效
toRef()
函數用來把一個響應式對象的的某個key
值轉換成ref
,它的實現本身很簡單:
functiontoRef(target,key){return{isRef:true,getvalue(){returntarget[key]},setvalue(newVal){target[key]=newVal}}}
可以看到toRef()
函數比ref()
函數要簡單的多,這是因為target
本身就是響應的,因此無需手動track()
和trigger()
。
toRefs()
toRef()
的一個問題是定義起來極其麻煩,一次只能轉換一個key
,因此我們可以封裝一個函數,直接把一個響應式對象的所有key
都轉成ref
,這就是toRefs()
functiontoRefs(target){constret:any={}for(constkeyintarget){ret[key]=toRef(target,key)}returnret}
這樣我們就可以修改前例的代碼為:
const obj = reactive ({ foo : 1 }) // const obj2 = { foo: toRef(obj, 'foo') } const obj2 = { ... toRefs ( obj ) } // 代替上面注释这句代码effect (() => { console . log ( obj2 . foo . value ) // 由于obj2.foo 现在是一个ref,因此要访问.value }) obj . foo = 2 // 有效
自動脫ref
但是我們發現,問題雖然解決了,但是帶來了新的問題,即我們需要通過.value
訪問值才行,這就帶來了另外一個問題:我們怎麼知道一個值是不是ref
,需不需要通過.value
來訪問呢?因為上面的例子可能會給讀者帶來疑惑,我們為什麼把問題弄得這麼複雜?為什麼弄了obj
和obj2
這兩個變量,只用obj
不就沒問題了嘛?這是因為在Vue
中,我們要暴露數據到渲染環境,怎麼暴露呢?
constComp={setup(){constobj=reactive({foo:1})return{...obj}}}
這就會導致丟失響應,因此我們需要toRef()
還是和toRefs()
函數。然而這帶來了新的問題,我們在setup
中暴露出去的數據,是要在渲染環境中使用的:
<h1>{{ obj.foo }}</h1>
這裡我們應該用obj.foo
還是obj.foo.value
呢?這時就需要你明確知道在setup
中暴露出去的值,哪些是ref
哪些不是ref
。因此為了減輕心智負擔,乾脆,在渲染環境都不需要.value
去取值,即使是ref
也不需要,這就極大的減少的心智負擔,這就是自動脫`Ref`功能。而實現自動脱ref
也很簡單,回看一下剛才的代碼:
const obj = reactive ({ foo : 1 }) // const obj2 = { foo: toRef(obj, 'foo') } const obj2 = { ... toRefs ( obj ) } // 代替上面注释这句代码effect (() => { console . log ( obj2 . foo . value ) // 由于obj2.foo 现在是一个ref,因此要访问.value }) obj . foo = 2 // 有效
為了自動擺脫ref
,我們可以:
const obj = reactive ({ foo : 1 }) // const obj2 = { foo: toRef(obj, 'foo') } const obj2 = reactive ({ ... toRefs ( obj ) }) // 让obj2 也是reactive effect (() => { console . log ( obj2 . foo ) // 即使obj2.foo 是ref,我们也不需要.value 来取值}) obj . foo = 2 // 有效
我們只需要讓obj2
也是reactive
的即可,這樣,即使obj2.foo
是ref
,我們也不需要通過.value
取值,其實現也很簡單,當我們在對像上讀取屬性時,如果發現其值是ref
,那麼直接返回.value
就可以了:
get ( target , key , receiver ) { // ... const res = Reflect . get ( target , key , receiver ) if ( isRef ( res )) return res . value // ... }
但對於由ref
組成的數組,在渲染環境仍然需要.value
訪問。
customRef()
customRef()
實際上就是手動track
和trigger
的典型例子,參考上文中的“track() 和trigger()”
一節。它的源碼也極其簡單,大家可以自行查看。
shallowRef()
通常我們使用ref()
函數時,目的是為了引用原始類型值,例如: ref(false)
。但我們仍然可以引用非基本類型值,例如一個對象:
constrefObj=ref({foo:1})
此時, refObj.value
是一個對象,這個對象依然是響應的,例如如下代碼會觸發響應:
refObj.value.foo=2
shallowRef()
顧名思義,它只代理ref
對象本身,也就是說只有.value
是被代理的,而.value
所引用的對象並沒有被代理:
constrefObj=shallowRef({foo:1})refObj.value.foo=3// 无效
triggerRef()
我們上面講過了ref()
函數的實現,在trigger
一個ref
的時候,它的操作類型都是SET
,並且操作的key
都是value
:
trigger(r,TriggerOpTypes.SET,'value')
這裡唯一不同的就是r
,也就是ref
本身。換句話說,如果一個ref
被track
了,那麼我們可以手動調用trigger
函數任意去觸發響應:
const refVal = ref ( 0 ) effect (() => { refVal . value }) // 任意次的trigger trigger ( refVal , TriggerOpTypes . SET , 'value' ) trigger ( refVal , TriggerOpTypes . SET , 'value' ) trigger ( refVal , TriggerOpTypes . SET , 'value' )
而triggerRef()
函數實際上就是封裝了這一操作:
exportfunctiontriggerRef(ref:Ref){trigger(ref,TriggerOpTypes.SET,'value',__DEV__?{newValue:ref.value}:void0)}
那它有什麼用呢?上面我們講過了, shallowRef()
函數不會代理.value
所引用的對象,因此我們修改對象值的時候不會觸發響應,這時我們可以通過triggerRef()
函數強制觸發響應:
constrefVal=shallowRef({foo:1})effect(()=>{console.log(refVal.value.foo)})refVal.value.foo=2// 无效
triggerRef(refVal)// 强制 trigger
unref()
unref()
函數很簡單:
exportfunctionunref<T>(ref:T):TextendsRef<inferV>?V:T{returnisRef(ref)?(ref.valueasany):ref}
給它一個值,如果這個值是ref
就返回.value
,否則原樣返回。
Lazy 的effect()
effect()
用來運行副作用函數,默認是立即執行的,但它可以是lazy
的,這時我們可以手動執行它:
const runner = effect ( () => { console . log ( 'xxx' ) }, { lazy : true } // 指定lazy ) runner () // 手动执行副作用函数
它有什麼用呢?實際上computed()
就是一個lazy
的effect
。
computed()
我們先來看看computed()
怎麼用:
constrefCount=ref(0)constrefDoubleCount=computed(()=>refCount.value*2)
當我們通過refDoubleCount.value
取值時,如果refCount
的值沒變,那麼表達式refCount.value * 2
只會計算一次。這也是computed()
優於methods
的地方。
上面說了computed
就是一個lazy
的effect
,接下來我們證明這個說法。
我們可以把問題簡化一下,首先我們有一refCount
和doubleCount
,其中doubleCount
是根據refCount.value * 2
計算得來的,如下:
constrefCount=ref(1)letdoubleCount=0functiongetDoubleCount(){doubleCount=refCount.value*2returndoubleCount}
這樣,我們就可以通過執行getDoubleCount()
函數來取值,這段代碼實際上我們可以改寫一下,例如:
constrefCount=ref(1)letdoubleCount=0construnner=effect(()=>{doubleCount=refCount.value*2},{lazy:true})functiongetDoubleCount(){runner()returndoubleCount}
我們定義了一個lazy
的effect
,然後在getDoubleCount()
函數中手動執行runner()
來計算值。不過無論怎麼改,都存在一個問題:即使refCount
的值沒變,表達式refCount.value * 2
都會執行計算。
實際上,我們可以通過一個標誌變量dirty
來避免這個問題:
const refCount = ref ( 1 ) let doubleCount = 0 let dirty = true // 定义标志变量,默认为true const runner = effect (() => { doubleCount = refCount . value * 2 }, { lazy : true }) function getDoubleCount () { if ( dirty ) { runner () // 只有dirty 的时候才执行计算dirty = false // 设置为false } return doubleCount }
如上代碼所示,我們增加了dirty
變量,默認為true
,代表臟值,需要計算,所以只有dirty
為true
的時候才執行runner()
,緊接著將dirty
設置為false
,這樣就避免了冗餘的計算量。
但問題是,現在我們修改refCount
的值,並在此執行getDoubleCount()
函數,得到的仍然是上一次的值,這是不正確的,因為refCount
已經變化了,這是因為dirty
一直是false
的緣故,因此問題的解決辦法也很簡單,當refCount
值變化之後,我們將dirty
再次設置為true
就可以了:
constrefCount=ref(1)letdoubleCount=0letdirty=true//定義標誌變量,默認為trueconstrunner=effect(()=>{doubleCount=refCount.value*2},{lazy:true,scheduler:()=>dirty=true})//將dirty設置為truefunctiongetDoubleCount(){if(dirty){runner()//只有dirty的時候才執行計算dirty=false//設置為false}returndoubleCount}
如上代碼所示,當refCount
變化之後,我們知道副作用函數會進行調度執行,因此我們提供調度器,在調度器中僅僅將dirty
設置為true
即可。
那其實我們可以把getDoubleCount
函數封裝為一個getter
:
const refDoubleCount = { get value () { if ( dirty ) { runner () // 只有dirty 的时候才执行计算dirty = false // 设置为false } return doubleCount } } refDoubleCount . value
這其實機上計算屬性的思路了。
effect() 的其他選項
- onTrack()
const obj = reactive ({ foo : 0 }) effect (() => { obj . foo }, { onTrack ({ effect , target , type , key }) { // ... } })
參數介紹:
- effect:track 誰?
- target:誰track 的?
- type:因為啥track?
- key:哪個key track 的?
- onTrigger()
const map = reactive ( new Map ()) map . set ( 'foo' , 1 ) effect (() => { for ( let item of map ){} }, { onTrigger ({ effect , target , type , key , newValue , oldValue }) { // ... } }) map . set ( 'bar' , 2 )
- effect:trigger 誰?
- target:誰trigger 的?
- type:因為啥trigger
- key:哪個key trigger 的?可能是undefined,例如:map.clear() 的時候。
- newValue 和oldValue:新舊值
- onStop()
const runner = effect (() => { // ... }, { onStop () { console . log ( 'stop...' ) } }) stop ( runner )