effect
effect 作为 reactive 的核心,主要负责收集依赖,更新依赖,其会在 mountComponent、doWatch、reactive、computed 时被调用。 实质:其实就是一个改良版的发布订阅模式。get 时通过 track 收集依赖,而 set 时通过 trigger 触发了依赖,而 effect 收集了这些依赖并进行追踪,在响应后去触发相应的依赖。effect 也正是 Vue3 响应式的核心。 参数
执行
在调用 effect 时会触发 track 开启响应式追踪,将追踪数据放入 targetMap 执行 reactive 时,通过 Proxy 类劫持对象
劫持 getter 执行 track 劫持 setter 执行 trigger 劫持的对象放在一个叫 targetMap 的 WeakMap
export interface ReactiveEffectOptions {
lazy? : boolean
computed? : boolean
scheduler? : ( job: ReactiveEffect ) => void
onTrack? : ( event: DebuggerEvent ) => void
onTrigger? : ( event: DebuggerEvent ) => void
onStop? : ( ) => void
}
export function effect< T = any> (
fn : ( ) => T ,
options? : ReactiveEffectOptions
) : ReactiveEffectRunner {
if ( ( fn as ReactiveEffectRunner) . effect) {
fn = ( fn as ReactiveEffectRunner) . effect. fn
}
const _effect = new ReactiveEffect ( fn)
if ( options) {
extend ( _effect, options)
if ( options. scope) recordEffectScope ( _effect, options. scope)
}
if ( ! options || ! options. lazy) {
_effect. run ( )
}
const runner = _effect. run . bind ( _effect) as ReactiveEffectRunner
runner. effect = _effect
return runner
}
effect的创建
首先对 effect 做了一些初始化,然后初次创建 effect 的时候,如果当前的 effect 栈(effectStack)不包含当前 effect,仅将activeEffect设为当前effect,将activeEffect压入effectStack再开始依赖收集,根据依赖数目判断是否需要清空依赖数组,这样可以避免依赖的重复收集。依赖收集后重置activeEffect。这里的effect实际上是vue中垃圾回收
export class ReactiveEffect < T = any> {
active = true
deps: Dep[ ] = [ ]
computed? : boolean
allowRecurse? : boolean
onStop? : ( ) => void
onTrack? : ( event: DebuggerEvent ) => void
onTrigger? : ( event: DebuggerEvent ) => void
constructor (
public fn : ( ) => T ,
public scheduler: EffectScheduler | null = null ,
scope? : EffectScope | null
) {
recordEffectScope ( this , scope)
}
run ( ) {
if ( ! this . active) {
return this . fn ( )
}
if ( ! effectStack. includes ( this ) ) {
try {
effectStack. push ( ( activeEffect = this ) )
enableTracking ( )
trackOpBit = 1 << ++ effectTrackDepth
if ( effectTrackDepth <= maxMarkerBits) {
initDepMarkers ( this )
} else {
cleanupEffect ( this )
}
return this . fn ( )
} finally {
if ( effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers ( this )
}
trackOpBit = 1 << -- effectTrackDepth
resetTracking ( )
effectStack. pop ( )
const n = effectStack. length
activeEffect = n > 0 ? effectStack[ n - 1 ] : undefined
}
}
}
function cleanupEffect ( effect: ReactiveEffect ) {
const { deps } = effect
if ( deps. length) {
for ( let i = 0 ; i < deps. length; i++ ) {
deps[ i] . delete ( effect)
}
deps. length = 0
}
}
export const initDepMarkers = ( { deps } : ReactiveEffect) => {
if ( deps. length) {
for ( let i = 0 ; i < deps. length; i++ ) {
deps[ i] . w |= trackOpBit
}
}
}
export const finalizeDepMarkers = ( effect: ReactiveEffect ) => {
const { deps } = effect
if ( deps. length) {
let ptr = 0
for ( let i = 0 ; i < deps. length; i++ ) {
const dep = deps[ i]
if ( wasTracked ( dep) && ! newTracked ( dep) ) {
dep. delete ( effect)
} else {
deps[ ptr++ ] = dep
}
dep. w &= ~ trackOpBit
dep. n &= ~ trackOpBit
}
deps. length = ptr
}
}
track 收集依赖(get操作)
当对一个对象进行get、has、iterate的时候,会触发该对象的track,收集依赖到targetMap。
export const enum TrackOpTypes {
GET = 'get' ,
HAS = 'has' ,
ITERATE = 'iterate'
}
let shouldTrack = true
export function track ( target: object, type: TrackOpTypes, key: unknown ) {
if ( ! isTracking ( ) ) {
return
}
let depsMap = targetMap. get ( target)
if ( ! depsMap) {
targetMap. set ( target, ( depsMap = new Map ( ) ) )
}
let dep = depsMap. get ( key)
if ( ! dep) {
depsMap. set ( key, ( dep = createDep ( ) ) )
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects ( dep, eventInfo)
}
export function isTracking ( ) {
return shouldTrack && activeEffect !== undefined
}
export function trackEffects (
dep: Dep,
debuggerEventExtraInfo? : DebuggerEventExtraInfo
) {
let shouldTrack = false
if ( effectTrackDepth <= maxMarkerBits) {
if ( ! newTracked ( dep) ) {
dep. n |= trackOpBit
shouldTrack = ! wasTracked ( dep)
}
} else {
shouldTrack = ! dep. has ( activeEffect! )
}
if ( shouldTrack) {
dep. add ( activeEffect! )
activeEffect! . deps. push ( dep)
if ( __DEV__ && activeEffect! . onTrack) {
activeEffect! . onTrack (
Object. assign (
{
effect: activeEffect!
} ,
debuggerEventExtraInfo
)
)
}
}
}
trigger 触发依赖(触发更新后执行监听函数之前触发)
对对象进行set、add、delete、clear时会触发trigger,使用target中的deps触发依赖追踪。 trigger的运行过程。其实是消费targetMap的依赖。在 trigger 方法中,拿到了之前收集到的依赖(也就是之前添加好的 effect)并添加到了任务队列中。然后遍历找到依赖后,开始触发依赖,执行任务
export const enum TriggerOpTypes {
SET = 'set' ,
ADD = 'add' ,
DELETE = 'delete' ,
CLEAR = 'clear'
}
export function trigger (
target: object,
type: TriggerOpTypes,
key? : unknown,
newValue? : unknown,
oldValue? : unknown,
oldTarget? : Map< unknown, unknown> | Set< unknown>
) {
const depsMap = targetMap. get ( target)
if ( ! depsMap) {
return
}
let deps: ( Dep | undefined ) [ ] = [ ]
if ( type === TriggerOpTypes. CLEAR ) {
deps = [ ... depsMap. values ( ) ]
} else if ( key === 'length' && isArray ( target) ) {
depsMap. forEach ( ( dep, key ) => {
if ( key === 'length' || key >= ( newValue as number) ) {
deps. push ( dep)
}
} )
} else {
if ( key !== void 0 ) {
deps. push ( depsMap. get ( key) )
}
switch ( type) {
case TriggerOpTypes. ADD :
if ( ! isArray ( target) ) {
deps. push ( depsMap. get ( ITERATE_KEY ) )
if ( isMap ( target) ) {
deps. push ( depsMap. get ( MAP_KEY_ITERATE_KEY ) )
}
} else if ( isIntegerKey ( key) ) {
deps. push ( depsMap. get ( 'length' ) )
}
break
case TriggerOpTypes. DELETE :
if ( ! isArray ( target) ) {
deps. push ( depsMap. get ( ITERATE_KEY ) )
if ( isMap ( target) ) {
deps. push ( depsMap. get ( MAP_KEY_ITERATE_KEY ) )
}
}
break
case TriggerOpTypes. SET :
if ( isMap ( target) ) {
deps. push ( depsMap. get ( ITERATE_KEY ) )
}
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
if ( deps. length === 1 ) {
if ( deps[ 0 ] ) {
if ( __DEV__) {
triggerEffects ( deps[ 0 ] , eventInfo)
} else {
triggerEffects ( deps[ 0 ] )
}
}
} else {
const effects: ReactiveEffect[ ] = [ ]
for ( const dep of deps) {
if ( dep) {
effects. push ( ... dep)
}
}
if ( __DEV__) {
triggerEffects ( createDep ( effects) , eventInfo)
} else {
triggerEffects ( createDep ( effects) )
}
}
}
export function triggerEffects (
dep: Dep | ReactiveEffect[ ] ,
debuggerEventExtraInfo? : DebuggerEventExtraInfo
) {
for ( const effect of isArray ( dep) ? dep : [ ... dep] ) {
if ( effect !== activeEffect || effect. allowRecurse) {
if ( __DEV__ && effect. onTrigger) {
effect. onTrigger ( extend ( { effect } , debuggerEventExtraInfo) )
}
if ( effect. scheduler) {
effect. scheduler ( )
} else {
effect. run ( )
}
}
}
}
总结
调用方调用effect函数,参数为函数fn,options(默认为{}); 判断是否已经是effect过的函数,如果是的话,则直接把原函数返回。 调用createReactiveEffect生成当前fn对应的effect函数,把上面的参数fn和options直接传进去;
为effect函数赋值 然后为effect函数添加属性:id, _isEffect, active, raw, deps, options,把effect返回。 判断options里面的lazy是否是false
如果不是懒处理,就直接调用下对应的effect函数,返回生成的effect函数。 如果是懒处理
首先判断了是否是active状态,如果不是,说明当前effect函数已经处于失效状态,直接返回return options.scheduler ? undefined : fn() 查看调用栈effectStack里面是否有当前effect,如果无当前effect,接着执行下面的代码。 先调用cleanup,把当前所有依赖此effect的全部清掉,deps是个数组,元素为Set,Set里面放的则是ReactiveEffect,也就是effect; 把当前effect入栈,并将当前effect置为当前活跃effect->activeEffect;后执行fn函数; finally,把effect出栈,执行完成了,把activeEffect还原到之前的状态; 其中涉及到调用轨迹栈的记录。和shouldTrack是否需要跟踪轨迹的处理。
effect.spec单元测试
it ( 'should run the passed function once (wrapped by a effect)' , ( ) => {
const fnSpy = jest. fn ( ( ) => { } )
effect ( fnSpy)
expect ( fnSpy) . toHaveBeenCalledTimes ( 1 )
} )
it ( 'should observe basic properties' , ( ) => {
let dummy
const counter = reactive ( { num: 0 } )
effect ( ( ) => ( dummy = counter. num) )
expect ( dummy) . toBe ( 0 )
counter. num = 7
expect ( dummy) . toBe ( 7 )
} )
it ( 'should handle multiple effects' , ( ) => {
let dummy1, dummy2
const counter = reactive ( { num: 0 } )
effect ( ( ) => ( dummy1 = counter. num) )
effect ( ( ) => ( dummy2 = counter. num) )
expect ( dummy1) . toBe ( 0 )
expect ( dummy2) . toBe ( 0 )
counter. num++
expect ( dummy1) . toBe ( 1 )
expect ( dummy2) . toBe ( 1 )
} )
it ( 'should observe nested properties' , ( ) => {
let dummy
const counter = reactive ( { nested: { num: 0 } } )
effect ( ( ) => ( dummy = counter. nested. num) )
expect ( dummy) . toBe ( 0 )
counter. nested. num = 8
expect ( dummy) . toBe ( 8 )
} )
it ( 'should observe delete operations' , ( ) => {
let dummy
const obj = reactive ( { prop: 'value' } )
effect ( ( ) => ( dummy = obj. prop) )
expect ( dummy) . toBe ( 'value' )
delete obj. prop
expect ( dummy) . toBe ( undefined )
} )
it ( 'should observe has operations' , ( ) => {
let dummy
const obj = reactive< { prop: string | number } > ( { prop: 'value' } )
effect ( ( ) => ( dummy = 'prop' in obj) )
expect ( dummy) . toBe ( true )
delete obj. prop
expect ( dummy) . toBe ( false )
obj. prop = 12
expect ( dummy) . toBe ( true )
} )
it ( 'should observe chained getters relying on this' , ( ) => {
const obj = reactive ( {
a: 1 ,
get b ( ) {
return this . a
}
} )
let dummy
effect ( ( ) => ( dummy = obj. b) )
expect ( dummy) . toBe ( 1 )
obj. a++
expect ( dummy) . toBe ( 2 )
} )
it ( 'should observe methods relying on this' , ( ) => {
const obj = reactive ( {
a: 1 ,
b ( ) {
return this . a
}
} )
let dummy
effect ( ( ) => ( dummy = obj. b ( ) ) )
expect ( dummy) . toBe ( 1 )
obj. a++
expect ( dummy) . toBe ( 2 )
} )
it ( 'should not observe raw mutations' , ( ) => {
let dummy
const obj = reactive< { prop? : string } > ( { } )
effect ( ( ) => ( dummy = toRaw ( obj) . prop) )
expect ( dummy) . toBe ( undefined )
obj. prop = 'value'
expect ( dummy) . toBe ( undefined )
} )
it ( 'should not be triggered by raw mutations' , ( ) => {
let dummy
const obj = reactive< { prop? : string } > ( { } )
effect ( ( ) => ( dummy = obj. prop) )
expect ( dummy) . toBe ( undefined )
toRaw ( obj) . prop = 'value'
expect ( dummy) . toBe ( undefined )
} )
it ( 'should not be triggered by inherited raw setters' , ( ) => {
let dummy, parentDummy, hiddenValue: any
const obj = reactive< { prop? : number } > ( { } )
const parent = reactive ( {
set prop ( value ) {
hiddenValue = value
} ,
get prop ( ) {
return hiddenValue
}
} )
Object. setPrototypeOf ( obj, parent)
effect ( ( ) => ( dummy = obj. prop) )
effect ( ( ) => ( parentDummy = parent. prop) )
expect ( dummy) . toBe ( undefined )
expect ( parentDummy) . toBe ( undefined )
toRaw ( obj) . prop = 4
expect ( dummy) . toBe ( undefined )
expect ( parentDummy) . toBe ( undefined )
} )
it ( 'should avoid implicit infinite recursive loops with itself' , ( ) => {
const counter = reactive ( { num: 0 } )
const counterSpy = jest. fn ( ( ) => counter. num++ )
effect ( counterSpy)
expect ( counter. num) . toBe ( 1 )
expect ( counterSpy) . toHaveBeenCalledTimes ( 1 )
counter. num = 4
expect ( counter. num) . toBe ( 5 )
expect ( counterSpy) . toHaveBeenCalledTimes ( 2 )
} )
it ( 'should avoid infinite loops with other effects' , ( ) => {
const nums = reactive ( { num1: 0 , num2: 1 } )
const spy1 = jest. fn ( ( ) => ( nums. num1 = nums. num2) )
const spy2 = jest. fn ( ( ) => ( nums. num2 = nums. num1) )
effect ( spy1)
effect ( spy2)
expect ( nums. num1) . toBe ( 1 )
expect ( nums. num2) . toBe ( 1 )
expect ( spy1) . toHaveBeenCalledTimes ( 1 )
expect ( spy2) . toHaveBeenCalledTimes ( 1 )
nums. num2 = 4
expect ( nums. num1) . toBe ( 4 )
expect ( nums. num2) . toBe ( 4 )
expect ( spy1) . toHaveBeenCalledTimes ( 2 )
expect ( spy2) . toHaveBeenCalledTimes ( 2 )
nums. num1 = 10
expect ( nums. num1) . toBe ( 10 )
expect ( nums. num2) . toBe ( 10 )
expect ( spy1) . toHaveBeenCalledTimes ( 3 )
expect ( spy2) . toHaveBeenCalledTimes ( 3 )
} )
it ( 'should discover new branches while running automatically' , ( ) => {
let dummy
const obj = reactive ( { prop: 'value' , run: false } )
const conditionalSpy = jest. fn ( ( ) => {
dummy = obj. run ? obj. prop : 'other'
} )
effect ( conditionalSpy)
expect ( dummy) . toBe ( 'other' )
expect ( conditionalSpy) . toHaveBeenCalledTimes ( 1 )
obj. prop = 'Hi'
expect ( dummy) . toBe ( 'other' )
expect ( conditionalSpy) . toHaveBeenCalledTimes ( 1 )
obj. run = true
expect ( dummy) . toBe ( 'Hi' )
expect ( conditionalSpy) . toHaveBeenCalledTimes ( 2 )
obj. prop = 'World'
expect ( dummy) . toBe ( 'World' )
expect ( conditionalSpy) . toHaveBeenCalledTimes ( 3 )
} )
it ( 'should discover new branches when running manually' , ( ) => {
let dummy
let run = false
const obj = reactive ( { prop: 'value' } )
const runner = effect ( ( ) => {
dummy = run ? obj. prop : 'other'
} )
expect ( dummy) . toBe ( 'other' )
runner ( )
expect ( dummy) . toBe ( 'other' )
run = true
runner ( )
expect ( dummy) . toBe ( 'value' )
obj. prop = 'World'
expect ( dummy) . toBe ( 'World' )
} )
it ( 'should not be triggered by mutating a property, which is used in an inactive branch' , ( ) => {
let dummy
const obj = reactive ( { prop: 'value' , run: true } )
const conditionalSpy = jest. fn ( ( ) => {
dummy = obj. run ? obj. prop : 'other'
} )
effect ( conditionalSpy)
expect ( dummy) . toBe ( 'value' )
expect ( conditionalSpy) . toHaveBeenCalledTimes ( 1 )
obj. run = false
expect ( dummy) . toBe ( 'other' )
expect ( conditionalSpy) . toHaveBeenCalledTimes ( 2 )
obj. prop = 'value2'
expect ( dummy) . toBe ( 'other' )
expect ( conditionalSpy) . toHaveBeenCalledTimes ( 2 )
} )
it ( 'scheduler' , ( ) => {
let runner: any, dummy
const scheduler = jest. fn ( _runner => {
runner = _runner
} )
const obj = reactive ( { foo: 1 } )
effect (
( ) => {
dummy = obj. foo
} ,
{ scheduler }
)
expect ( scheduler) . not. toHaveBeenCalled ( )
expect ( dummy) . toBe ( 1 )
obj. foo++
expect ( scheduler) . toHaveBeenCalledTimes ( 1 )
expect ( dummy) . toBe ( 1 )
runner ( )
expect ( dummy) . toBe ( 2 )
} )