This article was adapted from @ZhihuHcySunYang

Effect () and reactive ()

 import { effect, reactive } from '@vue/reactivity'
  // Use reactive() to define reactive data
  const obj = reactive({ text: 'hello' })
  // Use the effect() function to define the side effect function
  effect(() = > {
       document.body.innerText = obj.text
  })

  // Modify the reactive data after a second, which triggers the side effect function to be re-executed
  setTimeout(() = > {
    obj.text += ' world'
  }, 1
Copy the code
  • reactive()The function takes an object as an argument and returns a proxy object.
  • effect()The function is used to define side effects, and its argument is the side effect function, which can have side effects, such as in the code abovedocument.body.innerText = obj.textReactive data within the side effect function is linked to the side effect function, known as dependency collection, which causes the side effect function to be re-executed when the reactive data changes.

shallowReactive()

Define shallow response data:

import { effect, shallowReactive } from '@vue/reactivity'
  // use the shallowReactive() function to define shallowReactive data
  const obj = shallowReactive({ foo: { bar: 1 } })

  effect(() = > {
    console.log(obj.foo.bar)
  })

  obj.foo.bar = 2  / / is invalid
  obj.foo = { bar: 2 }  / / effective
Copy the code

readonly()

The readonly() function can be used for data that is intended to be read-only to the user:

import { readonly } from '@vue/reactivity'
// Use reactive() to define reactive data
const obj = readonly({ text: 'hello' })
obj.text += ' world' // Set operation on key "text" failed: target is readonly. 
Copy the code

shallowReadonly()

Similar to shallow responses, shallowReadonly() defines shallow read-only data, which means that the underlying object values can be modified. In Vue, this is defined using the shallowReadonly() function:

import { effect, shallowReadonly } from '@vue/reactivity'
// Use the shallowReadonly() function to define shallow read-only data
const obj = shallowReadonly({ foo: { bar: 1 } })

obj.foo = { bar: 2 }  // Warn
obj.foo.bar = 2 // OK
Copy the code

isReactive()

Check whether the data object is reactive:

import { isReactive, reactive, readonly, shallowReactive, shallowReadonly } from '@vue/reactivity'

const reactiveProxy = reactive({ foo: { bar: 1}})console.log(isReactive(reactiveProxy)) // true
console.log(isReactive(reactiveProxy.foo)) // true

const shallowReactiveProxy = shallowReactive({ foo: { bar: 1}})console.log(isReactive(shallowReactiveProxy)) // true
console.log(isReactive(shallowReactiveProxy.foo)) // false

const readonlyProxy = readonly({ foo: 1 })
console.log(isReactive(readonlyProxy)) // false

const shallowReadonlyProxy = shallowReadonly({ foo: 1 })
console.log(isReactive(shallowReadonlyProxy)) // false
Copy the code

isReadonly()

To check whether the data is 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
Copy the code

isProxy()

Check whether an object is a proxy object (reactive or readonly) :

import { isProxy, reactive, readonly, shallowReactive, shallowReadonly } from '@vue/reactivity'

console.log(isProxy(readonly({}))) // true
console.log(isProxy(shallowReadonly({}))) // true
console.log(isProxy(reactive({}))) // true
console.log(isProxy(shallowReactive({}))) // true

const shallowReactiveProxy = shallowReactive({ foo: {}})console.log(isProxy(shallowReactiveProxy))  // true
console.log(isProxy(shallowReactiveProxy.foo))  // false

const shallowReadonlyProxy = shallowReadonly({ foo: {}})console.log(isProxy(shallowReadonlyProxy))  // true
console.log(isProxy(shallowReadonlyProxy.foo))  // false
Copy the code

markRaw()

  • ** Which data can be proxied: **
    • Object, Array, Map, Set, WeakMap, WeakSet
    • The Object. IsFrozen:
const obj = { foo: 1 }
Object.freeze(obj)

// Object.isFrozen(obj) ==> true
// proxyObj === obj
const proxyObj = reactiev(obj)
Copy the code
  • Non-vnode, the VNode object of Vue3 has__v_skip: trueFlag, used to skip the proxy (in fact, as long as there is __v_skipProperty and the value istrueWill not be proxied), for example:
// obj is the raw data object
const obj = reactive({
  foo: 0.__v_skip: true
})
Copy the code
  • The ** markRaw() function is used to make data unproppable: **

What the markRaw function does, in effect, is to define the __v_skip attribute on the data object, thereby skipping the proxy:

import { markRaw } from '@vue/reactivity'
const obj = { foo: 1 }
markRaw(obj) // { foo: 1, __v_skip: true }
Copy the code

toRaw()

Receives the proxy object as a parameter and gets the original object:

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
Copy the code

If the argument is a non-proxy object, the value is directly:

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
Copy the code

ReactiveFlags

ReactiveFlags is an enumerated value:It is defined as follows:

export const enum ReactiveFlags {
  skip = '__v_skip',
  isReactive = '__v_isReactive',
  isReadonly = '__v_isReadonly',
  raw = '__v_raw',
  reactive = '__v_reactive',
  readonly = '__v_readonly'
}
Copy the code

What does it do? For example, we want to define an object that cannot be proxied:

import { ReactiveFlags, reactive, isReactive } from '@vue/reactivity'

const obj = {
  [ReactiveFlags.skip]: true
}

const proxyObj = reactive(obj)

console.log(isReactive(proxyObj)) // false
Copy the code

In fact, the markRaw() function is implemented in a similar way. So we don’t have to do this in the code above, but we might use these values in some advanced scenarios.

Here is a brief overview of the various values in ReactiveFlags:

  • The proxy object passesReactiveFlags.rawReference to the original object
  • The original object will pass throughReactiveFlags.reactiveReactiveFlags.readonlyReference proxy object
  • The proxy object according to which it isreactivereadonlyAnd willReactiveFlags.isReactiveReactiveFlags.isReadonlyProperty value set totrue

The schedule executes the effect-scheduler

Consider the following example:

const obj = reactive({ count: 1 })
effect(() = > {
  console.log(obj.count)
})

obj.count++
obj.count++
obj.count++
Copy the code

We define the response object obj and read its value in effect so that effect and the data are “related.” Then we change the value of obj. Count three times in a row and see console.log printed four times (including the first execution).

Imagine if we only needed to apply the final state of the data to the side effects, instead of re-executing the side effects function every time it changed, which would improve performance. We can actually pass effect a second parameter as an option, specifying “scheduler.” The scheduler is used to specify how to run side effects functions:

const obj = reactive({ count: 1 })
effect(() = > {
  console.log(obj.count)
}, {
  // Specify queueJob as the scheduler
  scheduler: queueJob
})

// Scheduler implementation
const queue: Function[] = []
let isFlushing = false
function queueJob(job: () => void) {
  if(! queue.includes(job)) queue.push(job)if(! isFlushing) { isFlushing =true
    Promise.resolve().then(() = > {
      let fn
      while(fn = queue.shift()) {
        fn()
      }
    })
  }
}

obj.count++
obj.count++
obj.count++
Copy the code

We specify the scheduler for Effect as queueJob, which is essentially the side effect function. We buffer the side effect function into a queue and flush the queue in microTask. Since the queue does not buffer the same job twice, the side effect function is executed only once.

This is actually how the watchEffect() function works.

watchEffect()

The watchEffect() function is not provided in @vue/reactivity, but in @vue/ Runtime-core, which is exposed along with the watch() function.

const obj = reactive({ foo: 1 })
watchEffect(() = > {
  console.log(obj.foo)
})

obj.foo++
obj.foo++
obj.foo++
Copy the code

This is actually the same effect as the custom scheduler effect we just implemented above.

Asynchronous side effects and invalidate

Asynchronous side effects are common, such as the request API interface:

watchEffect(async() = > {const data = await fetch(obj.foo)
})
Copy the code

When obj.foo changes, meaning that the request will be sent again, what about the previous request? Should the previous request be marked as invalidate?

In effect, the side effect function takes a function as an argument:

watchEffect(async (onInvalidate) => {
    const data = await fetch(obj.foo)
})
Copy the code

We can call it to register a callback function that will be executed if the side effect is invalid:

watchEffect(async (onInvalidate) => {
    let validate = true
    onInvalidate(() = > {
        validate = false
    })
    const data = await fetch(obj.foo)
    if (validate){
        /* Normally use data */
    } else {
        /* Indicates that the current side effect is invalid}})Copy the code

If you do not discard invalid side effects, you will have a race state problem. In fact, we can easily support registering “invalid callbacks” by wrapping the effect() function:

import { effect } from '@vue/reactivity'

function watchEffect(fn: (onInvalidate: (fn: () => void) = >void) = >void) {
  let cleanup: Function
  function onInvalidate(fn: Function) {
    cleanup = fn
  }
  // Encapsulate effect
  // Nullify the last null effect before executing the side effect function
  effect(() = > {
    cleanup && cleanup()
    fn(onInvalidate)
  })
}
Copy the code

If we add the invoker, we’re actually pretty close to a real watchEffect implementation.

When do I need to invalidate a side effect function?

  • An effect defined in a component needs to be invalidate when the component is uninstalled
  • If a data change causes an effect to be re-executed, invalidate the previous effect execution
  • When the user manually stops an effect

Stop a side effect

@vue/reactivity provides the stop function to stop a side effect:

import { stop, reactive, effect } from '@vue/reactivity'
const obj = reactive({ foo: 1 })

const runner = effect(() = > {
  console.log(obj.foo)
})
// Stop a side effect
stop(runner)

obj.foo++
obj.foo++
Copy the code

The effect() function returns a value that is actually effect itself, which we usually call runner. Passing the runner to the stop() function stops the effect. Subsequent changes to the data do not trigger re-execution of the side effect function.

Difference between watchEffect() and effect()

The effect() function comes from @vue/reactivity, and the watchEffect() function comes from @vue/ Runtime-core. The difference is: Effect() is a very low-level implementation. WatchEffect () is a wrapper based on effect(). WatchEffect () maintains relationships with component instances and component states (whether or not a component is unloaded, etc.). WatchEffect () will also be stopped, but effect() will not. Here’s an example:

  • WatchEffect () :
const obj = reactive({ foo: 1 })

const Comp = defineComponent({
  setup() {
    watchEffect(() = > {
      console.log(obj.foo)
    })

    return () = > ' '}})// Mount the component
render(h(Comp), document.querySelector('#app')!// Uninstall the component
render(null.document.querySelector('#app')! obj.foo++// The side effect function will not be re-executed
Copy the code

Mounting, unmounting, and changing the value of obj.foo does not cause the watchEffect side effect function to be re-executed.

  • effect()
const obj = reactive({ foo: 1 })

const Comp = defineComponent({
  setup() {
    effect(() = > {
      console.log(obj.foo)
    })

    return () = > ' '}})// Render component
render(h(Comp), document.querySelector('#app')!// Uninstall the component
render(null.document.querySelector('#app')! obj.foo++Copy the code

The effect() side effect function is still executed, but the onUnmounted API can fix this:

const obj = reactive({ foo: 1 })

const Comp = defineComponent({
  setup() {
    const runner = effect(() = > {
      console.log(obj.foo)
    })
    // Stop effect when the component is uninstalled
    onUnmounted(() = > stop(runner))

    return () = > ' '}})// Render component
render(h(Comp), document.querySelector('#app')!// Uninstall the component
render(null.document.querySelector('#app')! obj.foo++Copy the code

Of course, it’s not recommended to use effect() directly in normal development. Instead, use watchEffect().

Track () and the trigger ()

Track () and trigger() are the core of dependency collection. Track () is used to track dependencies and trigger() is used to trigger responses. They need to be used in conjunction with the effect() function:

const obj = { foo: 1 }
effect(() = > {
  console.log(obj.foo)
  track(obj, TrackOpTypes.GET, 'foo')
})

obj.foo = 2
trigger(obj, TriggerOpTypes.SET, 'foo')
Copy the code

As shown in the code above, obj is a normal object; note that it is not a reactive object. We then use effect() to define a side effect function that reads and prints the value of obj.foo. Since obj is an ordinary object, it has no dependency collection capability. To collect dependencies, we need to manually call track(), which takes three arguments:

  • Target: The target object to trackobj
  • Type of trace operation:obj.fooIs the value of the read object, so isget
  • Key: to track the target objectkeyWhat we read isfoo, sokeyfoo

Thus, we are essentially manually building a data structure:

/ / pseudo code
map: { [target]: { [key]: [effect1, effect2....] }}Copy the code

An effect is associated with the key of an object and a specific operation in this mapping:

[target]---->key1---->[effect1, effect2...]

[target]---->key2---->[effect1, effect3...]

[target2]---->key1---->[effect5, effect6...]

Now that effect is associated with the target object target, you can of course find a way to fetch effect via target —-> key and execute them, which is what the trigger() function does, So when we call trigger we specify the target object and the corresponding key:

trigger(obj, TriggerOpTypes.SET, 'foo')
Copy the code

This is probably how dependency collection works, but it can be done automatically, without the need for developers to manually call track() and trigger() functions. To complete dependency collection, you need to intercept methods such as set and read. As for the implementation method, whether Object. DefineProperty or Proxy that is the specific technical form.

ref()

Reactive () can be used to delegate values of primitive types, such as strings, numbers, and Boolean. This is a limitation of the JS language, so we need to use ref() to delegate values of primitive types indirectly:

const refVal = ref(0)
refVal.value  / / 0
Copy the code

Ref is reactive:

const refVal = ref(0)
effect(() = > {
   console.log(refVal.value)
})

refVal.value = 1  // Trigger the response
Copy the code

Now that you know the track() and trigger() functions, think about how easy it is to implement ref() :

function myRef(val: any) {
  let value = val

  const r = {
    isRef: true.// Add a random identifier to distinguish between them
    get value() {
      // Collect dependencies
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal: any) {
      if(newVal ! == value) { value = newVal// Trigger the response
        trigger(r, TriggerOpTypes.SET, 'value')}}}return r
}
Copy the code

Now try our myRef() function:

const refVal = myRef(0)

effect(() = > {
  console.log(refVal.value)
})

refVal.value = 1
Copy the code

All OK.

isRef()

When we implement myRef(), we see that we have added an identifier isRef: true to the ref object. So we can wrap a function isRef() to determine if a value isRef:

function isRef(val) {
    return val.isRef
}
Copy the code

The fact that the identity used in Vue3 is __v_isRef doesn’t matter.

toRef()

Missing responses is a problem with the ReActivity API:

const obj = reactive({ foo: 1 }) // obj is responsive data
const obj2 = { foo: obj.foo }

effect(() = > {
  console.log(obj2.foo) // Read obj2.foo here
})

obj.foo = 2  // Setting obj.foo is obviously not working
Copy the code

To solve this problem, we can use the toRef() function:

const obj = reactive({ foo: 1 })
const obj2 = { foo: toRef(obj, 'foo')}// Change this

effect(() = > {
  console.log(obj2.foo.value)  // Since obj2.foo is now a ref, access.value
})

obj.foo = 2 / / effective
Copy the code

The toRef() function converts a key value of a responsive object toRef. The implementation itself is simple:

function toRef(target, key) {
    return {
        isRef: true.get value() {
            return target[key]
        },
        set value(newVal){
            target[key] = newVal
        }
    }
}
Copy the code

You can see that the toRef() function is much simpler than the ref() function because the target itself is responsive, so there is no need to manually track() and trigger().

toRefs()

One problem with toRefs() is that it is extremely difficult to define and can only convert one key at a time. Therefore, we can encapsulate a function that directly converts all keys of a reactive object into refs. This is called toRefs()

function toRefs(target){
    const ret: any = {}
    for (const key in target) {
        ret[key] = toRef(target, key)
    }
    return ret
} 
Copy the code

So we can change the code from the previous example to:

const obj = reactive({ foo: 1 })
// const obj2 = { foo: toRef(obj, 'foo') }
constobj2 = { ... toRefs(obj) }// Comment this code instead

effect(() = > {
  console.log(obj2.foo.value)  // Since obj2.foo is now a ref, access.value
})

obj.foo = 2 / / effective
Copy the code

Automatically take off a ref

However, we found that the problem was solved, but it created a new problem: we need to access the value through.value, which brings up another problem: how do we know if a value is ref, and whether it needs to be accessed through.value? Because the above example may leave the reader wondering, why do we make things so complicated? Obj and obj2 are two variables. Obj is the only one that works. This is because in Vue, we expose the data to the render environment. How?

const Comp = {
    setup() {
        const obj = reactive({ foo: 1 })
        return { ...obj }
     }
}
Copy the code

This will cause the response to be lost, so we need toRef() or toRefs(). However, this introduces a new problem. The data we expose in setup is to be used in the rendering environment:

<h1>{{ obj.foo }}</h1>
Copy the code

Should we use obj.foo or obj.foo.value here? You need to know exactly what values are exposed in setup, which are ref and which are not. Therefore, in order to reduce the mental burden, simply do not need.value in the rendering environment, even ref is not needed, this greatly reduces the mental burden, this is the automatic unref function. It is also very easy to implement automatic unref. Look back at the code:

const obj = reactive({ foo: 1 })
// const obj2 = { foo: toRef(obj, 'foo') }
constobj2 = { ... toRefs(obj) }// Comment this code instead

effect(() = > {
  console.log(obj2.foo.value)  // Since obj2.foo is now a ref, access.value
})

obj.foo = 2 / / effective
Copy the code

To get rid of ref automatically, we can:

const obj = reactive({ foo: 1 }) // const obj2 = { foo: toRef(obj, 'foo') } const obj2 = reactive({ ... ToRefs (obj)}) reactive effect(() => {console.log(obj2.foo); We also don't need.value for the value}) obj.foo = 2 // validCopy the code

Obj2. Foo is ref, so we don’t need to use. Value. If obj2. Foo is ref, we can use.

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

But for arrays of refs,.value access is still required in the rendering environment.

customRef()

CustomRef () is actually a typical example of manual track and trigger, see the “track() and trigger()” section above. Its source code is also extremely simple, we can view.

shallowRef()

Usually when we use ref(), we want to reference a primitive value, such as ref(false). But we can still refer to values of non-primitive types, such as an object:

const refObj = ref({ foo: 1 })
Copy the code

At this point, refobj. Value is an object that is still responsive, such as the following code that triggers the response:

refObj.value.foo = 2
Copy the code

ShallowRef (), as its name implies, proxies only the ref object itself, that is, only.value is proxied, and the object referenced by.value is not proxied:

const refObj = shallowRef({ foo: 1 })

refObj.value.foo = 3 / / is invalid
Copy the code

triggerRef()

When trigger a ref(), its operation type is SET and the key of the operation is value:

trigger(r, TriggerOpTypes.SET, 'value')
Copy the code

The only thing that’s different here is r, which is ref itself. In other words, if a ref is tracked, we can manually call the trigger function to trigger the response at will:

const refVal = ref(0)
effect(() = > {
    refVal.value
})

// Trigger any time
trigger(refVal, TriggerOpTypes.SET, 'value')
trigger(refVal, TriggerOpTypes.SET, 'value')
trigger(refVal, TriggerOpTypes.SET, 'value')
Copy the code

The triggerRef() function essentially encapsulates this operation:

export function triggerRef(ref: Ref) {
  trigger(
    ref,
    TriggerOpTypes.SET,
    'value',
    __DEV__ ? { newValue: ref.value } : void 0)}Copy the code

So what does it do? As mentioned above, shallowRef() does not delegate the object referenced by.value. Therefore, changing the value of the object does not trigger the response. In this case, we can use triggerRef() to force the response:

const refVal = shallowRef({ foo: 1 })
effect(() = > {
    console.log(refVal.value.foo)
})

refVal.value.foo = 2 / / is invalid
triggerRef(refVal)  / / the trigger
Copy the code

unref()

The unref() function is simple:

export function unref<T> (ref: T) :T extends Ref<infer V>?V : T {
  return isRef(ref) ? (ref.value as any) : ref
}
Copy the code

Give it a value, return.value if the value is ref, otherwise return as is.

Effect of Lazy ()

Effect () is used to run the side effect function, which is executed immediately by default, but can be lazy, in which case we can execute it manually:

const runner = effect(
    () = > { console.log('xxx')}, {lazy: true }  / / specify the lazy
)

runner() // Execute the side effect function manually
Copy the code

What does it do? Actually, computed() is a lazy effect.

computed()

Let’s look at how computed() works first:

const refCount = ref(0)
const refDoubleCount = computed(() => refCount.value * 2)
Copy the code

When we pass refDoublecount. value, the expression refcount. value * 2 is evaluated only once if the refCount value is unchanged. This is also why computed() is better than methods.

It says that computed is a lazy effect, and we’re going to prove that.

Value * 2 = refcount.value * 2 = refcount.value * 2 = refcount.value * 2 = refcount.value * 2 = refcount.value * 2 = refcount.value * 2 = refcount.value * 2 = refcount.value * 2 = refcount.value * 2 = refcount.value * 2

const refCount = ref(1)
let doubleCount = 0

function getDoubleCount() {
    doubleCount = refCount.value * 2
    return doubleCount
}
Copy the code

In this case, we can evaluate it by calling getDoubleCount(). We can actually rewrite this code, for example:

const refCount = ref(1)
let doubleCount = 0

const runner = effect(() = > {
    doubleCount = refCount.value * 2
}, { lazy: true })

function getDoubleCount() {
    runner()
    return doubleCount
}
Copy the code

We define a lazy effect and then manually execute runner() in the getDoubleCount() function to calculate the value. However, there is a problem with any change: the expression refcount.value * 2 will perform the calculation even if the value of refCount does not change.

In fact, we can avoid this problem with a flag variable dirty:

const refCount = ref(1)
let doubleCount = 0
let dirty = true // Define flag variables. Default is true

const runner = effect(() = > {
    doubleCount = refCount.value * 2
}, { lazy: true })

function getDoubleCount() {
    if(dirty) {
        runner()  // Perform calculations only when dirty
        dirty = false // Set it to false
    }
    return doubleCount
}
Copy the code

As shown in the code above, we added the dirty variable, which defaults to true to represent the dirty value and needs to be evaluated, so runner() is executed only when dirty is true, followed by setting dirty to false to avoid redundant computation.

The problem is, now that we modify the refCount value and execute getDoubleCount() here, we still get the same value as last time, which is incorrect, because the refCount has changed, and this is because dirty has always been false, The solution to the problem is simple: When the refCount value changes, we can set dirty to True again:

const refCount = ref(1)
let doubleCount = 0
let dirty = true // Define flag variables. Default is true

const runner = effect(() = > {
    doubleCount = refCount.value * 2
}, { lazy: true.scheduler: () = > dirty = true })  // Set dirty to true

function getDoubleCount() {
    if(dirty) {
        runner()  // Perform calculations only when dirty
        dirty = false // Set it to false
    }
    return doubleCount
}
Copy the code

As shown in the code above, when the refCount changes, we know that the side effect function will schedule execution, so we provide a scheduler where we simply set dirty to true.

We can actually wrap getDoubleCount as a getter:

const refDoubleCount = {
  get value() {
    if(dirty) {
        runner()  // Perform calculations only when dirty
        dirty = false // Set it to false
    }
    return doubleCount
  }
}

refDoubleCount.value
Copy the code

This is actually the idea of computing properties on the machine.

Other options for effect()

onTrack()

const obj = reactive({ foo: 0 })
effect(() = > {
  obj.foo
}, {
  onTrack({ effect, target, type, key }) {
    // ...}})Copy the code

Parameter Description:

  • Effect: Track who?
  • Target: Who tracks it?
  • Type: because of what track?
  • -Penny: Which 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)
Copy the code
  • -Blair: Who’s this trigger?
  • -Blair: Who’s the target?
  • Type: because of what trigger
  • Key: Which key trigger? This may be undefined, as in map.clear().
  • NewValue and oldValue: new and old values

onStop()

const runner = effect(() = > {
  // ...
}, {
  onStop() {
    console.log('stop... ')
  }
})

stop(runner)
Copy the code