10

TNTWeb-the full name of Tencent News front-end team, the small partners in the group have practiced and accumulated in the front-end fields such as Web front-end, NodeJS development, UI design, and mobile APP.

At present, the team mainly supports the front-end development of Tencent News's various businesses. In addition to business development, it has also accumulated some front-end infrastructure to empower business efficiency and product innovation.

The team advocates open source and co-construction, and has a variety of technical experts. The team’s Github address: https://github.com/tnfe

The author of this article dravenwu

image.png

This article will focus on the other main folder reactivity of Vue3 to explain, that is, the part of compositionApi exposed to the outside in Vue3, which has more and more the flavor of React Hooks. The reactivity folder contains multiple files, the main functions are computed, effect, reactive, ref; other files are for it, and there is also a main entry file index. All the APIs exposed under reactivity can be seen in the figure below. We will use this document to analyze the source code of these functions in combination.

text

The text is here, and it officially begins.

computed

The meaning of computed is the same as that in Vue2, computing attributes; the usage method is similar to that in Vue2, and there are two ways to use it:

computed use

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: `
        <div>
            <h2>init count:<i>{{state.count}}</i></h2>
            <h2>computedCount:<i>{{computedCount}}</i></h2>
            <h2>computedNumber:<i>{{computedNumber}}</i></h2>
            <button @click="changeCount">changeCount</button>
        </div>
    `
})

app.mount('#demo')

In the above code, you can see the use of computed twice. The first pass is a function, and the second pass is an object containing get and set.

Computed source code analysis

Next, let's take a look at the source code of 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 new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any
}

The above is the source code of the computed entry. The writing method here is the same as that in Vue2. The parameters are judged and getters and setters are generated. The last call here is ComputedRefImpl;

// packages/reactivity/src/computed.ts
class ComputedRefImpl<T> {
  private _value!: T
  private _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)
  }
}

As above, it is the source code of ComputedRefImpl. ComputedRefImpl is a class that contains attributes such as _value, _dirty, effect, __v_isRef, ReactiveFlags.IS_READONLY, etc., as well as functions such as constructor and get and set. Those who know it all know that the constructor is called first; the effect is called to assign the effect attribute, and the isReadonly is assigned to the ReactiveFlags.IS_READONLY attribute. Regarding the effect, we will talk about this later. At this point, the execution of ComputedRefImpl is complete.

When obtaining the current computed value, as in the above use computedCount to obtain the value in the template, the get method in the above class will be called. The get method internally calls this.effect for data acquisition, and the _dirty attribute is for If the data cache does not change, the effect will not be called, and the previous value will be used to return. track is to track the track of the current get call.

When assigning a value to computed, such as computedNumber.value = 200 used above, the set method in the above class will be called, and the function passed in before is still called inside the set.

reactive

Next, an explanation of reactive

reactive use

The explanation given by the official website of reactive is: returns a reactive copy of the object. Let's first look at the use of 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: `
        <div>
            <h2>reactive count:<i>{{state.count}}</i></h2>
            <button @click="changeCount">changeCount</button>
        </div>
    `
})
app.mount('#demo')

When you click changeCount, state.count will be ++ and map to h2-dom at the same time.

Reactive source code interpretation

// @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
  )
}

As you can see from the source code above, if the target has a value and the target's [ReactiveFlags.IS_READONLY] attribute, that is, if __v_isReadonly is true, the current object will be returned directly without any processing, and subsequent changes to state.count will not Mapped to dom. If the above conditions are not met, the createReactiveObject function will be called, passing 4 parameters:

  • target is the original object;
  • The second is isReadonly, which is false;
  • The third parameter mutableHandlers is the processing function corresponding to reactive;
  • The fourth parameter is a function for processing objects of collection type.

Regarding this core function, we will explain it later.

readonly use

Now let's look at the second api under reactivity provided by Vue3: readonly.

The definition given by the official website is: gets an object (responsive or pure object) or ref and returns the read-only proxy of the original proxy. 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: `
        <div>
            <h2>readonly count:<i>{{read.count}}</i></h2>
            <button @click="changeRead">changeRead</button>
        </div>
    `
})

app.mount('#demo')

The above code is the use of readonly. I tested the result read after readonly returned, and tried to change it, and found that it cannot be changed. It is read-only. At the same time, a warning 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
  )
}

The above is the source code entry of readonly, which is the same as reactive, which is called the createReactiveObject function:

  • The first parameter is still target;
  • The second is isReadonly, which is true;
  • The third parameter readonlyHandlers is the processing function corresponding to readonly;
  • The fourth parameter is the function corresponding to readonly for processing objects of collection type.

shallowReactive use

The explanation given by the official website document: creates a responsive agent that tracks the responsiveness of its own property, but does not perform deep responsive conversion of nested objects (exposing the original value). Take a look at the use of 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: `
        <div>
            <h2>foo:<i>{{state.foo}}</i></h2>
            <h2>bar:<i>{{state.nested.bar}}</i></h2>
            <button @click="change">change</button>
        </div>
    `
})

app.mount('#demo')

The above code is basically written according to the official website, but the effect is different from the official website. It is not a shallow type, but the internal attributes are also monitored. The change of bar will also be reflected in response. Go to the dom. I don't know if my posture is wrong 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
  )
}

The above is the source entry of shallowReactive. Like reactive and readonly, they are all called createReactiveObject functions:

  • The first parameter is still target;
  • The second is isReadonly, which is false;
  • The third parameter shallowReactiveHandlers is the processing function corresponding to shallowReactive;
  • The fourth parameter is the function corresponding to shallowReactive for processing objects of collection type.

    shallowReadonly use

    The explanation given by the official website: creates an agent to make its own property read-only, but does not perform deep read-only conversion of nested objects (exposing the original value). Take a look at the use:

    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: `
          <div>
              <h2>foo:<i>{{state.foo}}</i></h2>
              <h2>bar:<i>{{state.nested.bar}}</i></h2>
              <button @click="change">change</button>
          </div>
      `
    })
    
    app.mount('#demo')

    The above code is basically written in accordance with the official website. The change of foo is not allowed. According to the official website, state.nested.bar is allowed to be changed. In the above example, it is found that the value of state.nested.bar will change. But it will not respond to 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
    )
    }

    The above is the source entry of shallowReadonly. Like reactive and readonly, they are all called createReactiveObject functions:

  • The first parameter is still target;
  • The second is isReadonly, which is true;
  • The third parameter shallowReadonlyHandlers is the processing function corresponding to shallowReadonly;
  • The fourth parameter is the function corresponding to shallowReadonly for processing objects of collection type.

isReadonly

isReadonly: Check whether the object is a read-only proxy created by readonly.

Use as follows:

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

The source code is as follows:

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

ReactiveFlags.IS_READONLY is a string with the value: __v_isReadonly, which is an attribute when attached to the object. It determines whether the __v_isReadonly attribute of the current object is true and returns.

isReactive

isReadonly: Check whether the object is a reactive proxy created by reactive.

Use as follows:

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

The source code is as follows:

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

Firstly, the above-mentioned isReadonly method is called to determine whether it is an object created by readonly; if it is, the RAW attribute of the current object is further used to call isReactive to determine; if it is not, it is determined whether __v_isReactive is true; the result of the determination is returned.

ReactiveFlags.RAW is a string with a value of: __v_raw, which is the attribute when attached to the object, which is the original object, to determine whether it is the original object of the reactive agent;
ReactiveFlags.IS_READONLY is also a string with the value: __v_isReactive, which is an attribute when attached to the object

isProxy

isProxy: Check 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

The source code is as follows:

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

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

markRaw

markRaw: Mark an object so that it will never be converted into a proxy. Return 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

As can be seen from the above usage, markRaw is only valid for the current object itself. When the marked object is used as an attribute, the large object bar can still be processed responsively, but the currently marked object foo in bar is still a non- The responsive object is always the foo object 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
  })
}

You can see the source code of markRaw above, which is to add an attribute (ReactiveFlags.SKIP, which is __v_skip) to the object to be marked, and assign it to true. All the reactive processing of the current object will be ignored.

toRaw

toRaw: Returns the original object of the reactive or readonly proxy. This is an escape port that can be used for temporary reading without incurring proxy access/tracking overhead, and it can also be used for writing without triggering changes. It is not recommended to keep a persistent reference to the original object. Please use it with caution.

Since Vue allows us to use it cautiously, let's not use it when we can't use it. This is to return the original object of the proxy.

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

console.log(toRaw(reactiveObj) === obj) // true

Source code:

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

Return the object pointed to by the ReactiveFlags.RAW (that is, __v_raw) attribute of the current object, that is, the object itself. Regarding where to assign a value to ReactiveFlags.RAW, you will see this part later.

createReactiveObject

The above reactive, readonly, shallowReactive, and shallowReadonly all use the createReactiveObject function. Now let’s take a look at the source code of this function.

function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>) {
    if (!isObject(target)) {
        if (__DEV__) {
            console.warn(`value cannot be made reactive: ${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
}

Source code interpretation:

  • First of all, the target must be an object. If it is not an object, it will directly return the current value. Of course, Vue3 also provides a responsive method to the value: ref, which will be described later.
  • If it is judged that there is an original object and it is not a read-only or responsive object, the current object will be returned.
  • According to whether it is isReadonly, the map stored by the proxy is obtained. If it has been proxied before and already exists, the proxy that has been proxied before will be returned.
  • Judge the type of target, getTargetType will judge the target object internally, and the return is common, collection or invalid; if the type is not available (invalid), it will directly return the current object. mentioned above will be used here. There are only two types available, one is common and the other is collection;
  • The next step is the process of obtaining an agent without an agent. new Proxy , if it is a collection, use the passed collectionHandlers, otherwise (that is, common) use baseHandlers;
  • The map used by the proxy storage, stores the current proxy;
  • Return the current proxy.

Through the above explanation of reactive, readonly, shallowReactive, and shallowReadonly, you can see that for collections and common types, several different processing objects are provided, and the content contained in the objects is also different. Let's compare here:

basehandler:


As shown in the figure above, we can see the functions provided in basehandler, let's take a look at them 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
}
  • Get whether the current object has the current key => hadKey;
  • Get the current value and store it as oldValue;
  • Call Reflect.deleteProperty to delete the current key of the current object target, and the return result is whether the deletion is successful ->result;
  • If the deletion is successful and there is the current key, trigger is called to trigger the effect.
  • Returns the result of whether the deletion was successful.

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

    This function is very simple, get the target object's own attribute key; track the obtained trajectory, and then 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
    }
  • Call Reflect.has to get whether the current object has the current key;
  • If the key is not a Symbol type, or is not an attribute of the Symbol itself, call track to track the trajectory 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 = value
          return 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
    }
    }

    The function factory generates set functions according to shallow. The set function accepts 4 parameters: target is the target object; key is the set property; value is the set value; receiver is the additional parameter of Reflect (if setter is encountered, receiver is the value of this when the setter is called).

  • First get oldValue;
  • If it is not shallow responsive, that is, in the formal situation, the original object of value is obtained and assigned to value. If the target object is not an array and oldValue is a reactive type of ref type, and the new value is not a reactive type of ref type, it is oldValue assignment (ref type reactive object, need to assign value to the object).
  • The following is the deep response code logic.
  • If it is an array and the key is of a numeric type, the subscript is directly judged, otherwise it is obtained by calling hasOwn, whether it contains the current key => hadKey;
  • Call Reflect.set to set the value;
  • If the target object and the original object of the receiver are equal, then hadKey, and trigger is called to trigger the add operation; otherwise, the trigger is called to trigger the set operation.
  • Return the result of set processing, 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)) {
        const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
        return shouldUnwrap ? res.value : res
      }
    
      if (isObject(res)) {
        return isReadonly ? readonly(res) : reactive(res)
      }
    
      return res
    }
    }

    The function factory generates get functions based on shallow. The get function accepts 3 parameters: target is the target object; key is the set property; receiver is the additional parameter of Reflect (if setter is encountered, receiver is the value of this when the setter is called).

  • If the key is __v_isReactive, it will directly return !isReadonly. As can be seen from the above figure, the reactive-related call to createGetter will pass false, that is, it will directly return true;
  • If the key is __v_isReadonly, it will directly return isReadonly. Similarly, we can know from the above figure that the readonly related call to createGetter will pass true, which means it will directly return true;
  • If the key is __v_raw and the receiver is equal to the proxy of the target object stored in proxyMap, that is, the original object is obtained, the target is returned directly;
  • If it is an array, it will take a custom method, arrayInstrumentations; arrayInstrumentations is the same logic as the rewriting of the array in Vue2;
  • The following key will be judged, and if the object is a method of Symbol Set inside custom; or key is __proto__ or __v_isRef, directly to Reflect.get(target, key, receiver) acquired value returned directly;
  • If it is not read-only, call track to track get track;
  • If it is shallow, it is not deeply responsive, and directly returns the res obtained above;
  • If it is a ref object, it will call .value to get the value and return;
  • In the remaining cases, if the obtained res is an object, call readonly or reactive to obtain the value according to isReadonly and return;
  • Finally, there is a res guarantee to return;

    collectionHandler:


    Take a look at the source code of createInstrumentationGetter, the three in the above figure are all calling this method to generate the corresponding processing object.

    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
      )
    }
    }

    The createInstrumentationGetter function above returns a function based on isReadonly and shallow;

  • According to isReadonly and shallow, get the corresponding instrumentations; this object contains all the methods of the collection operation;
  • Then the following function is returned, createInstrumentationGetter is equivalent to a closure;
  • In the returned function, when the call is executed, the key will be judged first. If the private variable of Vue is accessed, that is, the above __v_isReactive, __v_isReadonly, __v_raw, etc., different returns will be directly given;
  • If it is not the above three private variables of Vue, Reflect.get will be called to get the value of the object; instrumentations, that is, the set of overridden methods, which are not in this set, will directly call the target's own method.

reactive end

So far, we have sorted out these methods in the reactive file, and simply analyzed and interpreted the source code. Interested readers can go into the source code to study why this is implemented in Vue.

Refs

Next, we will begin to use and explain the ref and its subsidiary methods.

ref

First, let's explain ref. The explanation given on the official website is: accepts an internal value and returns a responsive and variable ref object. The ref object has a single property .value that points to an internal value.

Let's first 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')

The above is the use of ref. You can see that ref accepts a common type of value or an object. The examples given on the Vue official website do not include passing objects. In fact, this means that Vue does not advocate using ref to respond to an object. , If it is responsive to the object, Vue still advocates the use of the above reactive implementation; the second point to note is that template does not need to add the value attribute to get the value, as the above ref object count is in js Count.value is needed in, but only count in template is ;

Let's take a 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)

The above is the source code part of ref in Vue3 according to the running track; according to the declaration of ref, you can see that ref accepts any parameters, the return type is Ref object, and the internal call is createRef;

  • The createRef function will first judge the value, if it is already a ref object, it will directly return the current value, otherwise it will call new RefImpl to generate a ref object and return.
  • The constructor will determine whether it is a shallow response _shallow, if it is shallow, return _rawValue directly, otherwise call convert to return; you can see that in addition to the private attribute _value, there is also a read-only attribute of __v_isRef that is true;
  • In convert, val will be judged, and the object will call reactive. Otherwise, val will be returned directly. Here you can see why the ref can also accept the object as a parameter.
  • In get, it will track the call track, track; return the current value;
  • In the set, hasChanged is called to determine whether there is a change. Here, NaN is checked, because NaN is not equal to anything; set a new value and call trigger to trigger the set call.

    isRef

    isRef is obviously the judging whether it is a ref object . Use as follows:

    const count = ref(0)
    const is = isRef(count)
    const is2 = isRef(10)

    Looking at the source code, the source code is also 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)
    }

    The read-only attribute in RefImpl is used here, just to judge whether __v_isRef is true.

    shallowRef

    The explanation given by the official website: creates a ref, which tracks its own .value changes, but does not make its value responsive.
    The source code of shallowRef is as follows:

    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)
    }

    The calling process of shallowRef and ref is the same, but there is an extra parameter, which causes _shallow to be true. When called in RefImpl, the current value is directly returned without going to the convert function.

    unRef

    Official website explanation: If the parameter is ref, it returns the internal value, otherwise it returns the parameter itself. The source code of

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

    Indeed, as stated on the official website, just one line of code, the ref object returns its value, otherwise it directly returns ref.

    triggerRef

    Explanation given on the official website: manually execute any effect associated with shallowRef. , rather vague, the more 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'

    The source code is as follows:

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

toRef

The explanation given by the official website is: can be used to create a ref for the property attribute on the source responsive object. Then the ref can be passed out to maintain a responsive connection to its source property. simple description of 161764ef9848dd is to add a reference to an attribute of the object. This reference can be used at will, and the response type remains unchanged. Take a look at the source code:


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
  }
}

This part of the code is relatively simple and easier to read. The same as the above RefImpl is the addition of a read-only __v_isRef attribute.

toRefs

The official website's explanation for toRefs is: converts a responsive object into a normal object, where each property of the result object is a ref that points to the corresponding property of the original object. popular point description of 161764ef98492b is to turn every attribute of the responsive object into a ref object. Take a look at the source code:

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
}

It is especially required here to be a responsive object, and a non-responsive object will also print a warning. The for loop calls the toRef function mentioned above, turning every attribute in the object into a ref object.

customRef

The explanation given on the official website is: creates a custom ref and explicitly controls its dependency tracking and update triggers. It needs a factory function look at the source code 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
}

Correspondingly, when used, it accepts a factory. The factory is a function with parameters track and trigger. At the same time, the return of the factory must contain two functions, one for get and one for set. The track is the track of the effect, and the trigger is also the trigger of the effect; let's see 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: `
        <div>
            <input v-model="text" />
        </div>
    `
})

app.mount('#demo')

The above is an example of the use of customRef, which is the same as the example on the official website. It can achieve anti-shake, and it can also explicitly control when to call the track to track and when to call the trigger to trigger the change.

End of Refs

Above we have done the interpretation of the source code of several methods in refs and how some APIs are used. Why Vue3 provides two reactive solutions: reactive and Refs. This is actually related to the code style. Some students are accustomed to using objects, and some students are accustomed to using variables. Vue3 provides both solutions. Which one you want to use.

effect

In fact, you can see that this method is used in many places above, including effect, track, trigger, etc., which are all methods provided in effect. The methods provided in effect are internal methods of Vue and are not exposed to the outside. Let's take a look at the source code of this part one by one,

isEffect

isEffect is a function for judging whether it has side effects. Take a look at the source code:

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

You can see that the above judgment is to judge the _isEffect of the function, which is very simple.

effect

As the core part of Vue2 and Vue3, effect has this concept. The most important thing is to look at the source code of this part:

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
}

The above is the source code of the effect part. Go down step by step in the order of execution.

  • The caller calls the effect function, the parameter is the function fn, options (default is {});
  • Determine whether the function has been effected, and if so, return the original function directly.
  • Call createReactiveEffect to generate the effect function corresponding to the current fn, and pass in the above parameters fn and options directly;
  • Determine whether lazy in options is false, if it is not lazy, call the corresponding effect function directly;
  • Return the generated effect function.

Next, look at the calling process of the createReactiveEffect function.

  • Assign a value to the effect function. For the time being, don't consider what the reactiveEffect function is doing inside. Just understand that a function is created and assigned to the effect variable.
  • Then add attributes to the effect function: id, _isEffect, active, raw, deps, options
  • Returned the effect.

Let's go back to the above non-lazy case, call effect, and then the reactiveEffect function will be executed.

  • Firstly, it is judged whether it is in the active state. If it is not, it means that the current effect function is already in the invalid state, and it returns directly to return options.scheduler ? undefined : fn() .
  • Check if there is a current effect in the call stack effectStack, if there is no current effect, then execute the following code.
  • First call cleanup to clear all the currently dependent on this effect, deps is an array, the element is Set, and what is placed in the Set is ReactiveEffect, which is the effect;
  • Put the current effect on the stack, and set the current effect as the current active effect->activeEffect; then execute the fn function;
  • Finally, the effect is popped from the stack, the execution is completed, and the activeEffect is restored to the previous state;
  • Which involves the recording of the call trace stack. And shouldTrack need to track the processing of the track.

stop

The stop method is used to stop the current effect. Belongs to 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
  }
}
  • Calling cleanup to clear it is the same as calling cleanup above.
  • Execute the current effect.options.onnStop hook function.
  • Set the active state of the current effect to false.

Conclusion

This article mainly focuses on the corresponding use and source code interpretation of the compositionApi part provided for you to use in the reactivity folder. What you are interested in is to read the source code of this part. After all, this is a new function of Vue3. One step closer to react...

Everyone is welcome to discuss Vue3. The newly released version brings new things, and it will definitely bring unexpected surprises (bugs). Let us discover it and solve it. It is also an improvement and also prevents ourselves from stepping on pits. A good way.

image.png


TNTWEB
3.8k 声望8.5k 粉丝

腾讯新闻前端团队