Welcome to follow my personal wechat official account “HcySunYang”. Let’s coding for fun together!

Some of the basics can be referenced as documentation. I couldn’t write any more. I had to wait nearly 20 seconds for each character I typed. Stuck to death…

The TOC:

  • Effect () and reactive ()

  • shallowReactive()

  • readonly()

  • shallowReadonly()

  • isReactive()

  • isReadonly()

  • isProxy()

  • markRaw()

    • What data can be proxied
    • The markRaw() function is used to make data unproppable
  • toRaw()

  • ReactiveFlags

  • The schedule executes the effect-scheduler

  • watchEffect()

  • Asynchronous side effects and invalidate

  • Stop a side effect

  • Difference between watchEffect() and effect()

  • Track () and the trigger ()

  • ref()

  • isRef()

  • toRef()

  • toRefs()

  • Automatically take off a ref

  • customRef()

  • shallowRef()

  • triggerRef()

  • unref()

  • Effect of Lazy ()

  • computed()

  • Other effect options onTrack and onTrigger

Effect () and reactive ()

Import {effect, reactive} from '@vue/reactivity' const obj = reactive({text: 'hello'}) effect(() => {document.body.innertext = obj.text}) SetTimeout (() => {obj.text += 'world'}, 1000)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 may have side effects, for example

Document.body. innerText = obj.text in the code above. Reactive 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' const obj = shallowReactive({foo: {bar: 1}}) effect(() => {console.log(obj.foo.bar)}) obj.foo.bar = 2 // obj.foo = {bar: 2} // validCopy 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' // reactive() 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 shallowReadonly() to define shallow read-only data const obj = shallowReadonly({foo: {bar: 1 } }) obj.foo = { bar: 2 } // Warn obj.foo.bar = 2 // OKCopy 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
  • nonObject.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:
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)}, 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){/* data */} else {/*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} 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 (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')!) // Render (null, document.querySelector('#app')! Obj.foo+ + // The side effect function will not be re-executedCopy 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')!) // 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)}) OnUnmounted (() => stop(runner)) return () => "}}) // Render (h(Comp), document.querySelector('#app')!) // 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 is'get'
  • Key: to track the target objectkeyWhat we read isfoo, sokeyfoo

Thus, we are essentially manually building a data structure:

/ / map for pseudo code: {/ 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 // Triggers the responseCopy 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: Get () {track(r, trackoptypes. get, 'value') return value}, set value(newVal: any) { if (newVal ! 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 a response is a problem with the reactivity API:

Reactive ({foo: 1}) const obj2 = {foo: 1} Obj.foo} effect(() => {console.log(obj2.foo) // here to read obj2.foo}) obj.foo = 2 // Setting obj.foo is obviously invalidCopy the code

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

const obj = reactive({ foo: 1 }) const obj2 = { foo: ToRef (obj, 'foo')} effect(() => {console.log(obj2.foo.value) // Since obj2.foo is now a ref, So to access.value}) obj.foo = 2 // validCopy 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') } const obj2 = { ... ToRefs (obj)} effect(() => {console.log(obj2.foo.value) // Since obj2.foo is now a ref, So to access.value}) obj.foo = 2 // validCopy 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') } const obj2 = { ... ToRefs (obj)} effect(() => {console.log(obj2.foo.value) // Since obj2.foo is now a ref, So to access.value}) obj.foo = 2 // validCopy 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 // InvalidCopy 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 (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 // Invalid triggerRef(refVal) // mandatory triggerCopy 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 lazy) Runner () // Execute the side effect function manuallyCopy 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 Default to true const runner = effect(() => {doubleCount = refcount.value * 2}, {lazy: True}) function getDoubleCount() {if(dirty) {runner() {dirty = 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 Default to true const runner = effect(() => {doubleCount = refcount.value * 2}, {lazy: true, scheduler: () => dirty = true}) function getDoubleCount() {if(dirty) {runner() 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() // Only when dirty = false // set to false} return doubleCount } } refDoubleCount.valueCopy 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

Welcome to follow my personal wechat official account “HcySunYang”. Let’s coding for fun together!