preface

Hello, this is Derek.

Take a vacation for more than half a month, continue to come back to write the bug, the previous article talked about Vue3 source mount and patch part, Vue3 createApp and render in the mainstream program analysis and explanation.

This article will focus on Vue3’s other main folder, ReActivity, the compositionApi exposed in Vue3, which is getting a bit like React Hooks. The reactivity folder contains several files with computed, Effect, Reactive, and REF functions. The other files serve it, as well as the main entry file index. All of the APIS exposed under reactivity are shown below, and we will use them in combination for source analysis in this document.

The body of the

The text is here, the formal beginning.

computed

Computed means the same thing as Vue2, calculating attributes; The mode of use is similar to that of Vue2. There are two ways of use:

Computed using the

const {reactive, readonly, computed, ref} = Vue;

const app = Vue.createApp({});
app.component('TestComponent', {
    setup(props) {
        // reactive
        const state = reactive({
            count: 0.number: 10
        })
        // computed getter
        const computedCount = computed(() = > {
            return state.count + 10
        })
        // computed set get
        const computedNumber = computed({
            get: () = > {
                return state.number + 100
            },
            set: (value) = > {
                state.number = value - 50}})const changeCount = function(){
            state.count++;
            computedNumber.value = 200
        }
        return {
            state,
            changeCount,
            computedCount,
            computedNumber
        }
    },
    template: ` 
      

init count:{{state.count}}

computedCount:{{computedCount}}

computedNumber:{{computedNumber}}

`
}) app.mount('#demo') Copy the code

The code above shows two uses of computed, the first passing a function and the second passing an object containing get and set.

Computed source code analysis

Next, let’s look at the source code for computed:

// @file packages/reactivity/src/computed.ts
export function computed<T> (
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () = > {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  return newComputedRefImpl( getter, setter, isFunction(getterOrOptions) || ! getterOrOptions.set )as any
}
Copy the code

Here is the source code for computed entries, written in the same way as in Vue2, which evaluates parameters and generates getters and setters. The last call is ComputedRefImpl.

// packages/reactivity/src/computed.ts
class ComputedRefImpl<T> {
  private_value! : Tprivate _dirty = true

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    this.effect = effect(getter, {
      lazy: true.scheduler: () = > {
        if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')}}})this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    if (this._dirty) {
      this._value = this.effect()
      this._dirty = false
    }
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}
Copy the code

As above, is the source code for ComputedRefImpl. ComputedRefImpl is a class that contains attributes such as _value, _dirty, effect, __v_isRef, ReactiveFlags.IS_READONLY, and functions such as constructor and get and set. If you know this, you call the constructor function first; Call effect to assign the effect property, and assign isReadonly to the reactiveFlags.is_readonly property, which we’ll talk about later. At this point, ComputedRefImpl execution is complete.

When a computed value is obtained, such as when a value is obtained from the template using computedCount, the get method in the above class is called, and this. Effect is called inside the GET method to obtain data. The _dirty attribute is used for caching data. If the dependency has not changed, effect will not be called and the previous value will be returned. Track tracks the track of the current GET call.

When a computed value is assigned, as with computedNumber.value = 200, the set method in the class above is called, and the function passed in before is called inside the set.

reactive

And then reactive

Reactive use

Reactive returns a reactive copy of an object, according to reactive’s website. So let’s look at reactive

const {reactive} = Vue;

const app = Vue.createApp({});
app.component('TestComponent', {
    setup(props) {
        // reactive
        const state = reactive({
            count: 0
        })
        const changeCount = function(){
            state.count++;
        }
        return {
            state,
            changeCount
        }
    },
    template: ` 
      

reactive count:{{state.count}}

`
}) app.mount('#demo') Copy the code

When you click on changeCount, state.count will ++ and map to h2-DOM.

Reactive

// @file packages/reactivity/src/reactive.ts
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}
Copy the code

If target has a value and its [reactiveFlags.is_readonly] attribute (__v_isReadonly) is true, the current object is returned without any action. Subsequent changes to state.count do not map to the DOM. If the above conditions are not met, the createReactiveObject function is called, passing four arguments:

  • Target is the original object;
  • The second is isReadonly, which is false;
  • The third parameter, mutableHandlers, is the handler to reactive;
  • The fourth argument is a function that handles objects of collection type.

We’ll talk about this core function in a second.

Readonly use

Now let’s look at the second API under reActivity provided by Vue3: readOnly.

The official website defines a read-only proxy that takes an object (reactive or pure) or ref and returns the original proxy. A read-only proxy is deep: any nested property accessed is also read-only

const {readonly} = Vue;

const app = Vue.createApp({});
app.component('TestComponent', {
    setup(props) {
        const read = readonly({count: 1})

        const changeRead = function(){
            read.count++;
        }
        return {
            read,
            changeRead
        }
    },
    template: ` 
      

readonly count:{{read.count}}

`
}) app.mount('#demo') Copy the code

Set operation on key “count” failed Set operation on key “count” failed: target is readonly.

Readonly source code interpretation

// @file packages/reactivity/src/reactive.ts
export function readonly<T extends object> (
  target: T
) :DeepReadonly<UnwrapNestedRefs<T>> {
  return createReactiveObject(
    target,
    true,
    readonlyHandlers,
    readonlyCollectionHandlers
  )
}
Copy the code

Reactive: createReactiveObject (createReactiveObject) reactive: createReactiveObject (reactive)

  • The first parameter is target again;
  • The second is isReadonly, which is true;
  • The third argument, readonlyHandlers, is the corresponding handler to readonly;
  • The fourth argument is the function corresponding to readonly that handles objects of collection type.

ShallowReactive use

Create a reactive proxy that tracks the responsiveness of its own property, but does not perform deep reactive conversions of nested objects (exposing raw values). Let’s look at shallowReactive

const {shallowReactive} = Vue;

const app = Vue.createApp({});
app.component('TestComponent', {
    setup(props) {
        const state = shallowReactive({
            foo: 1.nested: {
                bar: 2}})const change = function(){
            state.foo++
            state.nested.bar++
        }
        return {
            state,
            change
        }
    },
    template: ` 
      

foo:{{state.foo}}

bar:{{state.nested.bar}}

`
}) app.mount('#demo') Copy the code

Instead of being shallow, it listens for internal attributes, and changes to the BAR are reflected in the DOM in a responsive way. I don’t know if it’s my posture or a bug in Vue3.

ShallowReactive source code interpretation

// @file packages/reactivity/src/reactive.ts
export function shallowReactive<T extends object> (target: T) :T {
  return createReactiveObject(
    target,
    false,
    shallowReactiveHandlers,
    shallowCollectionHandlers
  )
}
Copy the code

ShallowReactive (createReactiveObject) ¶ shallowReactive (createReactiveObject) ¶

  • The first parameter is target again;
  • The second is isReadonly, which is false;
  • The third parameter, shallowReactiveHandlers, is the corresponding handler to shallowReactive.
  • The fourth argument is the function to which shallowReactive works on objects of collection type.

ShallowReadonly use

Create a proxy that makes its property read-only, but does not perform deep read-only conversions of nested objects (exposing raw values). Here’s how to use it:

const {shallowReadonly} = Vue;

const app = Vue.createApp({});
app.component('TestComponent', {
    setup(props) {
        const state = shallowReadonly({
            foo: 1.nested: {
                bar: 2}})const change = function(){
            state.foo++
            state.nested.bar++
        }
        return {
            state,
            change
        }
    },
    template: ` 
      

foo:{{state.foo}}

bar:{{state.nested.bar}}

`
}) app.mount('#demo') Copy the code

In this example, it is found that the value of state.neste. bar will change, but it will not be reflected in the DOM.

ShallowReadonly source code interpretation

// @file packages/reactivity/src/reactive.ts
export function shallowReadonly<T extends object> (
  target: T
) :Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
  return createReactiveObject(
    target,
    true,
    shallowReadonlyHandlers,
    readonlyCollectionHandlers
  )
}
Copy the code

ShallowReadonly is a createReactiveObject function called by reactive and readonly:

  • The first parameter is target again;
  • The second is isReadonly, which is true;
  • The third argument, shallowReadonlyHandlers, is the shallowReadonly handler;
  • The fourth argument is the function corresponding to shallowReadonly that handles objects of collection type.

isReadonly

IsReadonly: Checks whether the object is a read-only proxy created by readOnly.

Use as follows:

const only = readonly({
	count: 1
})
isOnly = isReadonly(only) // true
Copy the code

The source code is as follows:

export function isReadonly(value: unknown) :boolean {
  return!!!!! (value && (valueas Target)[ReactiveFlags.IS_READONLY])
}
Copy the code

IS_READONLY is a string with a value of __v_isReadonly, which is attached to the object as an attribute and returns if the current object’s __v_isReadonly attribute is true.

isReactive

IsReadonly: Checks whether the object is a reactive proxy created by Reactive.

Use as follows:

const tive = reactive({
	count: 1
})
isOnly = isReactive(tive) // true
Copy the code

The source code is as follows:

export function isReactive(value: unknown) :boolean {
  if (isReadonly(value)) {
    return isReactive((value as Target)[ReactiveFlags.RAW])
  }
  return!!!!! (value && (valueas Target)[ReactiveFlags.IS_REACTIVE])
}
Copy the code

First, the isReadonly method mentioned above is called to determine if the object is created by readOnly. If so, call isReactive using the RAW property of the current object. If not, check whether __v_isReactive is true. Returns the result of the judgment.

Reactiveflags. RAW is a string with the value __v_RAW. If it is attached to an object, it is the original object, and it is determined whether it is the original object of the Reactive agent. Reactiveflags. IS_READONLY is also a string with a value of __v_isReactive, which is attached to the object as an attribute

isProxy

IsProxy: Checks whether the object is a proxy created by Reactive or Readonly.

Use as follows:

const tive = reactive({
	count: 1
})
const only = readonly({
	count: 1
})
is1 = isProxy(tive) // true
is2 = isProxy(only) // true
Copy the code

The source code is as follows:

export function isProxy(value: unknown) :boolean {
  return isReactive(value) || isReadonly(value)
}
Copy the code

Call the isReadonly method mentioned above and isReactive to determine if it is a proxy object.

markRaw

MarkRaw: Marks an object so that it will never be converted to a proxy. Returns the object itself.

Use as follows:

const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

const bar = reactive({ foo })
console.log(isReactive(bar)) // true
console.log(isReactive(bar.foo)) // false
Copy the code

As you can see from the above use, markRaw only applies to the current object itself. The large object bar can still be reactive when the marked object is an attribute, but the currently marked object in bar, foo, remains a non-reactive object and is always Foo itself.

export function markRaw<T extends object> (value: T) :T {
  def(value, ReactiveFlags.SKIP, true)
  return value
}
export const def = (obj: object, key: string | symbol, value: any) = > {
  Object.defineProperty(obj, key, {
    configurable: true.enumerable: false,
    value
  })
}
Copy the code

SKIP (__v_skip) adds an attribute to the object to be marked (reactiveFlags. SKIP) and assigns it a value of true. All reactive processing to the current object will be ignored.

toRaw

ToRaw: Returns the raw object of the Reactive or Readonly agent. This is an escape port that can be used for temporary reads without proxy access/trace overhead, or for writes without triggering changes. It is not recommended to preserve persistent references to the original object. Use with caution.

Since Vue allows us to use it with caution, we should not use it when we can, which is to return the original object of the proxy.

const obj = {
    project: 'reactive'
}
const reactiveObj = reactive(obj)

console.log(toRaw(reactiveObj) === obj) // true
Copy the code

Source:

export function toRaw<T>(observed: T): T {
    return (
        (observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed
    )
}
Copy the code

Returns the object to which the current object’s reactiveFlags. RAW(that is, __v_raw) attribute points, that is, the object itself, as to where reactiveFlags. RAW is assigned, as we’ll see later.

createReactiveObject

CreateReactiveObject (REACTIVE, readonly, shallowReactive, shallowReadonly) ¶

function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>) {
    if(! isObject(target)) {if (__DEV__) {
            console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
    }
    if(target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE])) {return target
    }
    const proxyMap = isReadonly ? readonlyMap : reactiveMap
    const existingProxy = proxyMap.get(target)
    if (existingProxy) {
        return existingProxy
    }
    const targetType = getTargetType(target)
    if (targetType === TargetType.INVALID) {
        return target
    }
    const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
    )
    proxyMap.set(target, proxy)
    return proxy
}
Copy the code

Source code interpretation:

  • First, target must be an object; otherwise, it returns the current value. Of course Vue3 also provides a method that responds to values: ref, which we’ll talk about later.
  • If there is an original object and it is not read-only or responsive, the current object is returned.
  • According to whether isReadonly is used, the map stored by the proxy is obtained. If the proxy has existed before, the proxy will be returned.
  • GetTargetType checks whether the target object is common, Collection, or invalid. If the type (invalid) is not available, the current object is returned. The __v_skip mentioned above is used here. There are only two types available, common and Collection;
  • What follows is the process of obtaining a proxy without having one.new Proxy, the passed collectionHandlers are used if it is collection, and baseHandlers are used otherwise (common);
  • The map used by proxy stores the current proxy.
  • Returns the current proxy.

Through above reactive, readonly, shallowReactive, shallowReadonly’s explanation, you can see for collection and common types, provides several different treatment object, the object contained in the content is also different, let’s watch the here to contrast the:

Basehandler:

As you can see in the figure above, the baseHandler provides functions that we can look at one by one.

deleteProperty

// @file packages/reactivity/src/baseHandlers.ts
function deleteProperty(target: object, key: string | symbol) :boolean {
  const hadKey = hasOwn(target, key)
  const oldValue = (target as any)[key]
  const result = Reflect.deleteProperty(target, key)
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
  }
  return result
}
Copy the code
  • If the current object has the current key => hadKey;
  • The current value store is oldValue.
  • Reflect.deleteProperty deletes the current key from target, and returns the result: delete succeeded ->result;
  • If the deletion succeeds and the current key exists, trigger is called to trigger effect.
  • Returns the result of successful deletion.

ownKeys

// @file packages/reactivity/src/baseHandlers.ts
function ownKeys(target: object) : (string | number | symbol) []{
  track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.ownKeys(target)
}
Copy the code

This function is very simple: it gets the target object’s own property key; Trace the retrieved trace and call reflect.ownkeys to get the result.

has

// @file packages/reactivity/src/baseHandlers.ts
function has(target: object, key: string | symbol) :boolean {
  const result = Reflect.has(target, key)
  if(! isSymbol(key) || ! builtInSymbols.has(key)) { track(target, TrackOpTypes.HAS, key) }return result
}
Copy the code
  • Calls reflect. has to get whether the current object has the current key;
  • For a key that is not of type Symbol, or a property that is not Symbol itself, the track call tracks the path of the HAS call.
  • Return the result, result.

createSetter

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ) :boolean {
    const oldValue = (target as any)[key]
    if(! shallow) { value = toRaw(value)if(! isArray(target) && isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true}}else {}

    const hadKey = isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    if (target === toRaw(receiver)) {
      if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}
Copy the code

Function factory, which generates the set function according to its shallow. The set function takes four arguments: target is the target object; Key is the set attribute; Value is the set value. Receiver is the additional argument to Reflect (if a setter is encountered, receiver is the this value when the setter is called).

  • First get oldValue;
  • If it’s non-shallow, which is the formal case, and you get the original object of value and you assign it to value, if the target object is not an array and oldValue is a reactive type of type REF, and the new value is not a reactive type of type REF, Assign oldValue (for reactive objects of type ref, assign the value of the object).
  • So here’s the deep responsive code logic.
  • If it is an array and the key is numeric, the index is determined directly. Otherwise, hasOwn is called to check whether the current key => hadKey.
  • Call reflect. set to set the value;
  • If the target object is equal to the receiver’s original object, hadKey is called to trigger the add operation. Otherwise, a call to trigger triggers the set operation.
  • Return the result of set, result.

createGetter

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    const res = Reflect.get(target, key, receiver)

    const keyIsSymbol = isSymbol(key)
    if (
      keyIsSymbol
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res
    }

    if(! isReadonly) { track(target, TrackOpTypes.GET, key) }if (shallow) {
      return res
    }

    if (isRef(res)) {
      constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)return shouldUnwrap ? res.value : res
    }

    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}
Copy the code

Function factory, which generates the get function from shallow. The get function takes three arguments: target is the target object; Key is the set attribute; Receiver is the additional argument to Reflect (if a setter is encountered, receiver is the this value when the setter is called).

  • If key is __v_isReactive, return it! IsReadonly (reactive createGetter) returns true (reactive createGetter);
  • If the key is __v_isReadonly, isReadonly is returned. In the same way, createGetter, which is associated with readonly, returns true.
  • If key is __v_RAW and receiver is equal to proxy for the target object stored in proxyMap, that is, to get the original object, target is returned directly;
  • ArrayInstrumentations (); arrayInstrumentations (); ArrayInstrumentations is the same logic as the array rewriting in Vue2;
  • The key is checked if the Symbol object is a custom method in the Set. Or the key is __proto__ or __v_isRefReflect.get(target, key, receiver)The obtained value is returned directly;
  • If it is not read-only, track is called to track the GET track.
  • If it is shallow, the RES obtained above is returned directly.
  • If it is a ref object,.value is called to get the value and return it;
  • The rest of the time, if the res is an object, isReadonly calls readonly or Reactive to get the value and return it.
  • Finally there is a res guarantee return;

CollectionHandler:

Look at createInstrumentationGetter source, three in the figure above are object of this method is called to generate the corresponding processing.

function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
  const instrumentations = shallow
    ? shallowInstrumentations
    : isReadonly
      ? readonlyInstrumentations
      : mutableInstrumentations

  return (
    target: CollectionTypes,
    key: string | symbol,
    receiver: CollectionTypes
  ) = > {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW) {
      return target
    }

    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver
    )
  }
}
Copy the code

The above createInstrumentationGetter function according to the isReadonly and shallow returns a function;

  • The corresponding instrumentations are obtained according to isReadonly and shallow; This object contains all the methods that operate on the collection;
  • And then put the function to return, createInstrumentationGetter rather then a closure;
  • When the function is called, the key will be checked first. If the Vue private variables are accessed, such as __v_isReactive, __v_isReadonly, __v_RAW, etc., different returns will be given directly.
  • If they are not the three private variables above the Vue, reflect.get is called to get the value of the object; Instrumentations, a set of rewritten methods, call target’s own methods directly if they are not in the set.

Completion of reactive

So far, these methods in the Reactive file have been reviewed, and the source code has been simply analyzed and interpreted. Interested readers can delve into the source code to see why Vue is implemented this way.

Refs

Next, we’ll start with the use and presentation of ref and its dependent methods.

ref

Take an internal value and return a responsive, mutable ref object. The ref object has a single property.value pointing to an internal value.

So let’s look at the use of ref.

const {ref} = Vue;

const app = Vue.createApp({});
app.component('TestComponent', {
    setup(props) {
        const count = ref(0)
        const obj = ref({number: 10})
        const change = function(){
            count.value++;
            obj.value.number++
        }

        return {
            count,
            obj,
            change
        }
    },
    template: `
        <div>
            <h2>count:<i>{{count}}</i></h2>
            <h2>number:<i>{{obj.number}}</i></h2>
            <button @click="change">change</button>
        </div>
    `
})
app.mount('#demo')
Copy the code

Ref accepts a value of a common type or an object. The example on Vue’s official website does not include passing objects. In fact, Vue does not advocate using ref to respond to an object. The second thing to note is that a reference to the ref object in template does not require the value attribute to obtain the value. For example, the ref object count in js requires count.

Look at the source code implementation of ref

// @file packages/reactivity/src/ref.ts
export function ref<T extends object> (
  value: T
) :T extends Ref ? T : Ref<UnwrapRef<T>>
export function ref<T> (value: T) :Ref<UnwrapRef<T>>
export function ref<T = any> () :Ref<T | undefined>
export function ref(value? : unknown) {
  return createRef(value)}function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)}class RefImpl<T> {
  private _value: T

  public readonly __v_isRef = true

  constructor(private _rawValue: T, private readonly _shallow = false) {
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }

  get value() {
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
    }
  }
}

const convert = <T extends unknown>(val: T): T => isObject(val) ? reactive(val) : val export const hasChanged = (value: any, oldValue: any): boolean => value ! == oldValue && (value === value || oldValue === oldValue)Copy the code

The above is the source of REF in Vue3 according to the running track; Ref takes any argument, returns a ref object, and calls createRef internally.

  • The createRef function evaluates the value internally and returns the current value if it is already a REF object, otherwise it calls new RefImpl to generate the ref object and return it.
  • Constructor () returns _rawValue (shallow); otherwise, convert is called. In addition to the private _value attribute, you can see that __v_isRef has a read-only attribute of true;
  • In convert, val is evaluated, and the object is called reactive, otherwise val is returned.
  • So get keeps track of the call, track; Returns the current value;
  • In set, hasChanged is called to determine if anything hasChanged, and NaN is checked because NaN is equal to nothing; The new value is set and trigger is called to trigger the set call.

isRef

IsRef is the obvious way to determine if it’s a REF object. Use as follows:

const count = ref(0)
const is = isRef(count)
const is2 = isRef(10)
Copy the code

Take a look at the source code, the source code is very simple:

export function isRef<T> (r: Ref<T> | unknown) :r is Ref<T>
export function isRef(r: any) :r is Ref {
  return Boolean(r && r.__v_isRef === true)}Copy the code

This is where the read-only attribute in RefImpl is used to check whether __v_isRef is true.

shallowRef

Create a ref that tracks its own.value changes, but does not make its value responsive. ShallowRef: shallowRef:

export function shallowRef<T extends object> (
  value: T
) :T extends Ref ? T : Ref<T>
export function shallowRef<T> (value: T) :Ref<T>
export function shallowRef<T = any> () :Ref<T | undefined>
export function shallowRef(value? : unknown) {
  return createRef(value, true)}Copy the code

ShallowRef is called in the same way as ref, but with an extra argument that causes _shallow to be true. When called in RefImpl, the current value is returned instead of the convert function.

unRef

If ref is used, the internal value is returned; otherwise, the parameter itself is returned. The source code is as follows:

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

The ref object returns its value on a single line of code, otherwise it returns ref directly.

triggerRef

Manually execute any effects associated with shallowRef. , relatively vague, popular point is to manually trigger an effect call; Take a look at the use:

const count = ref(0)
const change = function(){
    count.value++;
    triggerRef(count)
}
const shallow = shallowRef({
    greet: 'Hello, world'
})
watchEffect(() = > {
    console.log(count.value)
    console.log(shallow.value.greet)
})
shallow.value.greet = 'Hello, universe'
Copy the code

The source code is as follows:

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

toRef

This can be used to create a ref for a property attribute on a source responsive object. You can then pass the REF out, preserving a reactive connection to its source property. A simple description is to add a reference to a property of an object, and this reference can be used arbitrarily, without changing the response. Take a look at the source:


export function toRef<T extends object.K extends keyof T> (
  object: T,
  key: K
) :Ref<T[K] >{
  return isRef(object[key])
    ? object[key]
    : (new ObjectRefImpl(object, key) as any)}class ObjectRefImpl<T extends object.K extends keyof T> {
  public readonly __v_isRef = true

  constructor(private readonly _object: T, private readonly _key: K) {}

  get value() {
    return this._object[this._key]
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }
}
Copy the code

This part of the code is simpler and easier to read, with a read-only __v_isRef attribute, as with RefImpl above.

toRefs

ToRefs converts a reactive object into a normal object, where each property of the resulting object is a ref that points to the corresponding property of the original object. The popular way to describe it is to turn every property of a reactive object into a REF object. Take a look at the source:

export function toRefs<T extends object> (object: T) :ToRefs<T> {
  if(__DEV__ && ! isProxy(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`)}const ret: any = isArray(object)?new Array(object.length) : {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  return ret
}
Copy the code

In particular, a responsive object is required, and non-responsive objects will print warnings. The for loop calls the toRef function described above, turning every attribute inside the object into a REF object.

customRef

Create a custom REF with explicit control over its dependency tracking and update trigger. It requires a factory function to look at the source of customRef:

class CustomRefImpl<T> {
  private readonly _get: ReturnType<CustomRefFactory<T>>['get']
  private readonly _set: ReturnType<CustomRefFactory<T>>['set']

  public readonly __v_isRef = true

  constructor(factory: CustomRefFactory<T>) {
    const { get, set } = factory(
      () = > track(this, TrackOpTypes.GET, 'value'),
      () = > trigger(this, TriggerOpTypes.SET, 'value'))this._get = get
    this._set = set
  }

  get value() {
    return this._get()
  }

  set value(newVal) {
    this._set(newVal)
  }
}

export function customRef<T> (factory: CustomRefFactory<T>) :Ref<T> {
  return new CustomRefImpl(factory) as any
}
Copy the code

Correspondingly, when used, a factory is accepted. Factory is a function with track and trigger parameters. Meanwhile, the return of Factory must contain two functions, one is GET and the other is set. A track is an effect track and trigger is an effect trigger. Here’s how to use it:

const {customRef} = Vue;

const app = Vue.createApp({});
function useDebouncedRef(value, delay = 200) {
    let timeout
    return customRef((track, trigger) = > {
        return {
            get() {
                track()
                return value
            },
            set(newValue) {
                clearTimeout(timeout)
                timeout = setTimeout(() = > {
                    value = newValue
                    trigger()
                }, delay)
            }
        }
    })
}

app.component('TestComponent', {
    setup(props) {
        return {
            text: useDebouncedRef('hello')}},template: ` 
      
`
}) app.mount('#demo') Copy the code

The above is the example of customRef, which is the same as the example on the official website. It can realize shaking prevention, and also can explicitly control when to call track to track and when to call trigger to trigger changes.

Completion of Refs

Above we have done the source code interpretation of several methods in Refs and how some of the API is used, about why Vue3 provides two responsive solutions: Reactive and Refs, it depends on the code style. Some students are used to using objects, while others are used to using variables. Vue3 provides both solutions.

effect

In fact, it can be seen that this method is used in many places above, including Effect, track, trigger and other methods provided in Effect. The methods provided in Effect belong to the internal methods of Vue and are not exposed to the public. Let’s take a look at this part of the source code,

isEffect

IsEffect is a function to determine whether there are side effects. Take a look at the source:

export function isEffect(fn: any) :fn is ReactiveEffect {
  return fn && fn._isEffect === true
}
Copy the code

As you can see above, it is very simple to determine _isEffect of the function.

effect

Effect, as a core part of Vue2 and Vue3, has this concept.

export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if(! options.lazy) { effect() }return effect
}

function createReactiveEffect<T = any> (fn: () => T, options: ReactiveEffectOptions) :ReactiveEffect<T> {
  const effect = function reactiveEffect() :unknown {
    if(! effect.active) {return options.scheduler ? undefined : fn()
    }
    if(! effectStack.includes(effect)) { cleanup(effect)try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]}}}as ReactiveEffect
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

let shouldTrack = true
const trackStack: boolean[] = []

export function pauseTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = false
}

export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}

export function resetTracking() {
  const last = trackStack.pop()
  shouldTrack = last === undefined ? true : last
}
Copy the code

Above is the source code for the Effect section. Work your way down the execution sequence.

  • The caller calls the effect function with fn, options(default {});
  • Check if it is already an effect function. If so, return the original function.
  • Call createReactiveEffect to generate the effect function corresponding to the current FN, and pass fn and options directly into it.
  • Check whether the lazy in options is false. If not, call the effect function directly.
  • Returns the generated effect function.

Let’s look at how the createReactiveEffect function is called.

  • To assign a value to an effect function, forget about what is going on inside the reactiveEffect function, just know that a function is created and assigned to the effect variable.
  • Then add attributes to the effect function: ID, _isEffect, active, RAW, DEps, options
  • Effect is returned.

Let’s go back to the above non-lazy case and call effect, at which point the reactiveEffect function will be executed.

  • First, check whether the state is active. If not, the current effect function is invalid and return directlyreturn options.scheduler ? undefined : fn().
  • Check the call stack effectStack to see if the current effect is present. If not, execute the following code.
  • Deps is an array with a Set element, and the Set contains a ReactiveEffect (effect).
  • Push current effect to the stack and set current effect to current activeEffect ->activeEffect; After the execution of fn function;
  • Finally, effect out of the stack, the execution is complete, restore the activeEffect to the previous state;
  • It involves the record of the call trace stack. ShouldTrack and shouldTrack should be used to track the trace.

stop

The stop method is used to stop effect. Vue3 internal method, look at the source code:

export function stop(effect: ReactiveEffect) {
  if (effect.active) {
    cleanup(effect)
    if (effect.options.onStop) {
      effect.options.onStop()
    }
    effect.active = false}}Copy the code
  • The call cleanup cleans up, just like the call above.
  • Execute the current effect.options.onnStop hook function.
  • Set the current effect active state to false.

“Said

Hello, this is Derek.

This article mainly around the reactivity folder provided for everyone to use the compositionApi part of the corresponding use and source interpretation, everyone is interested in reading this part of the source code, after all, this is Vue3 new function, more and more react step……

Welcome everyone to discuss Vue3, the new version has brought new at the same time, it will certainly bring unexpected surprises (bugs), let us find it, solve it, is also a progress, but also a good way to prevent themselves from stepping on the pit.