深入理解Vue3 Reactivity API

blank

深入理解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是一個枚舉值:

blank

它的定義如下:

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.reactiveReactiveFlags.readonly引用代理對象
  • 代理對像根據它是reactivereadonly的,將ReactiveFlags.isReactiveReactiveFlags.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的調度器為queueJobjob實際上就是副作用函數,我們將副作用函數緩衝到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 ,因此keyfoo

這樣,我們本質上是手動建立一種數據結構:

// 伪代码
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來訪問呢?因為上面的例子可能會給讀者帶來疑惑,我們為什麼把問題弄得這麼複雜?為什麼弄了objobj2這兩個變量,只用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.fooref ,我們也不需要通過.value取值,其實現也很簡單,當我們在對像上讀取屬性時,如果發現其值是ref ,那麼直接返回.value就可以了:

get ( target , key , receiver ) { // ... const res = Reflect . get ( target , key , receiver ) if ( isRef ( res )) return res . value // ... }

但對於由ref組成的數組,在渲染環境仍然需要.value訪問。

customRef()

customRef()實際上就是手動tracktrigger的典型例子,參考上文中的“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本身。換句話說,如果一個reftrack了,那麼我們可以手動調用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()就是一個lazyeffect

computed()

我們先來看看computed()怎麼用:

constrefCount=ref(0)constrefDoubleCount=computed(()=>refCount.value*2)

當我們通過refDoubleCount.value取值時,如果refCount的值沒變,那麼表達式refCount.value * 2只會計算一次。這也是computed()優於methods的地方。

上面說了computed就是一個lazyeffect ,接下來我們證明這個說法。

我們可以把問題簡化一下,首先我們有一refCountdoubleCount ,其中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}

我們定義了一個lazyeffect ,然後在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 ,代表臟值,需要計算,所以只有dirtytrue的時候才執行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 )

What do you think?

Written by marketer

blank

React 16.8.6 版本存在內存洩露

blank

小程序長列表渲染優化另一種解決方案