Fully understand Vue3's ref and related functions and calculation properties

Posted by Michael 01 on Tue, 08 Mar 2022 21:53:01 +0100

Responsiveness of foundation type - ref

coupon https://m.fenfaw.cn/

In vue3, we can realize the responsiveness of reference types through reactive. How to realize the responsiveness of basic types?

You may think of this:

const count = reactive({value: 0})
count.value += 1

This can indeed be achieved, and it is very similar to the use of Ref value. Is this how ref is implemented internally?

Let's first define two instances of ref and print them.

    const refCount = ref(0) // Foundation type
    console.log('refCount ', refCount )

    const refObject = ref({ value: 0 }) // reference type
    console.log('refObject ', refObject )

Look at the results:

We all know that reactive is implemented through the Proxy of ES6. Obviously, the ref of the basic type has nothing to do with the Proxy, while the ref of the reference type first changes the prototype into reactive, and then hangs it on the value.
In this way, it's not quite the same as our guess, so how does ref come true? We can take a look at the source code of Ref.

ref source code

The code comes from Vue global. JS, adjust the order.

  function ref(value) {
      return createRef(value);
  }
  function createRef(rawValue, shallow = false) {
      if (isRef(rawValue)) {
          return rawValue;
      }
      return new RefImpl(rawValue, shallow);
  }
  class RefImpl {
      constructor(_rawValue, _shallow = false) {
          this._rawValue = _rawValue;
          this._shallow = _shallow;
          this.__v_isRef = true;
          this._value = _shallow ? _rawValue : convert(_rawValue); // Deep ref or shallow ref
      }
      get value() {
          track(toRaw(this), "get" /* 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), "set" /* SET */, 'value', newVal);
          }
      }
  }
  const convert = (val) => isObject(val) ? reactive(val) : val;

  • ref
    This is the function we use, which uses createRef to create an instance.

  • createRef
    Make some basic judgments, then enter the topic and formally create ref. You can also create a shallowRef here.

  • RefImpl
    This is the main body. Obviously, this is the class of ES6. The constructor is an initialization function. It creates an instance according to the parameters and sets the properties of the instance. This can also correspond to the print result of ref above.
    The code of the whole class is also very simple. Set several "internal" attributes, record the required data, and then set the "external" attribute value. The operation of value is intercepted through setter and getter. trigger is the main function in set, which calls the automatic refresh function of the template.

  • convert
    Obviously, judge whether the parameter is an object. If so, it will become reactive.
    This can explain how the ref of reference type realizes responsiveness. Obviously, it becomes reactive first, and then hangs on the value (judge whether it is shallow before hanging).

Relationship between ref and reactive

By comparing the print results and analyzing the source code, it can be found that:

  • ref of the base type has nothing to do with reactive.
  • For ref of reference type, first change the object into reactive, that is, use reactive to realize the responsiveness of reference type.

This is the relationship. Don't confuse it any more.

shallowRef

Shallow response, listening only The change of value is really a simple type of response.

function shallowRef(value) {
      return createRef(value, true); // true shallow
  }

Through the source code, we can find that before hanging the reference type to value, first judge whether it is shallow. If it is shallow, it will not become reactive, but directly hang the original object on value. This is the difference between shallowRef and ref.

Let's write a few examples to see the effect:

  setup () {
     // Shallow test 
    // Foundation type
    const srefCount = shallowRef(0)
    console.log('refCount ', srefCount )

    // reference type
    const srefObject = shallowRef({ value: 0 })
    console.log('refObject ', srefObject )

    // nested object 
    const srefObjectMore = shallowRef({ info: {a: 'jyk'} })
    console.log('shallowRef ', srefObjectMore )

    // reactive shallowRef
    const ret = reactive({name: 'jyk'})
    const shallowRefRet = shallowRef(ret)
    console.log('shallowRefRet ', shallowRefRet )

    // ====================Events==================
    // Modify foundation type
    const setNumber = () => {
      srefCount.value = new Date().valueOf()
      console.log('srefCount ', srefCount )
    }

    // Modify the properties of a reference type
    const setObjectProp = () => {
      srefObject.value.value = new Date().valueOf()
      console.log('srefObject ', srefObject )
    }
 
    // Modify value of reference type
    const setObject = () => {
      srefObject.value = { value: new Date().valueOf() }
      console.log('srefObject ', srefObject )
    }

    // Modify properties of nested reference types
    const setObjectMoreProp = () => {
      srefObjectMore.value.info.a = new Date().valueOf()
      console.log('srefObjectMore ', srefObjectMore )
    }
    
    // Modify value of nested reference type
    const setObjectMore = () => {
      srefObjectMore.value = { qiantao: 1234567 }
      console.log('srefObjectMore ', srefObjectMore )
    }

    // Modify shallow ref of reactive
    const setObjectreactive = () => {
      shallowRefRet.value.name = 'Shallow reactive'
      console.log('shallowRefRet ', shallowRefRet )
    }
  }

Look at the results:

The responsiveness was tested:

  • The foundation type srefCount is responsive;
  • The attribute of the reference type srefObject is not responsive, but can be modified directly value is responsive.
  • The nested reference type srefObjectMore, attribute and nested attribute are not responsive, but can be modified directly value is responsive.
  • Reactively apply the shallowRef, and then modify the shallowRef value. Attribute = xxx, which can also respond, so the shallow ref is not absolute. It also depends on the internal structure.

triggerRef

Manually perform any effects associated with the shallowRef.

The Chinese version of the official website is very convoluted. In fact, it is to make the originally unresponsive part of shallowRef responsive.
shallowRef is shallow and the deep part is not responsive. What if we have to make this part responsive?
At this time, it can be implemented with triggerRef.
Well, I haven't thought of any specific application scenarios yet, because they usually use ref or reactive directly and rudely, all with responsiveness.

After testing various situations, I found that triggerRef does not support shallowReactive. I thought it could support it. (maybe there's something wrong with the test code I wrote, and the official website didn't mention shallowReactive)

Based on the above example, add trigger ref (xxx) at the appropriate position.

  setup () {
    // reference type
    const srefObject = shallowRef({ value: 0 })
    // nested object 
    const srefObjectMore = shallowRef({ value: {a: 'jyk'} })
    // reactive shallowRef
    const ret = reactive({name: 'reactive'})
    const shallowRefRet = shallowRef(ret)
 
    // Shallow reactive
    const myShallowReactive = shallowReactive({info:{name:'myShallowReactive'}})
    const setsRet = () => {
      myShallowReactive.info.name = new Date().valueOf()
      triggerRef(myShallowReactive)  // Use after modification, not supported
   }

    // ====================Events==================

    // Modify the properties of a reference type
    const setObjectProp = () => {
      srefObject.value.value = new Date().valueOf()
      triggerRef(srefObject) // Use after modification
    }
 
    // Modify value of reference type
    const setObject = () => {
      srefObject.value = { value: new Date().valueOf() }
      triggerRef(srefObject)
   }

    // Modify properties of nested reference types
    const setObjectMoreProp = () => {
      srefObjectMore.value.value.a = new Date().valueOf()
      triggerRef(srefObjectMore)
  }
    
    // Modify value of nested reference type
    const setObjectMore = () => {
      srefObjectMore.value.value = { value: new Date().valueOf() }
      triggerRef(srefObjectMore)
    }

    // Modify shallow ref of reactive
    const setObjectreactive = () => {
      shallowRefRet.value.name = 'Shallow reactive' + new Date().valueOf()
      triggerRef(shallowRefRet)
    }

    return {
      srefObject, // reference type
      srefObjectMore, // Nested reference type
      shallowRefRet, // reactive shallow ref
      myShallowReactive, // Shallow reactive
      setsRet, // Modify shallow reactive
      setObjectProp, // Modify the properties of a reference type
      setObject, // Modify value of reference type
      setObjectMoreProp, // Modify properties of nested reference types
      setObjectMore, // Modify value of nested reference type
      setObjectreactive // Try reactive shallow ref
    }
  }

In the deep part, the template will not be refreshed without using triggerRef, and the template can be refreshed with triggerRef.
In other words, why is there this function?

isRef

Pass__ v_ The isref attribute determines whether it is an instance of ref. There's nothing to say about this.
vue.global.js source code:

function isRef(r) {
     return Boolean(r && r.__v_isRef === true);
 }

unref

  • use. Syntax of value
    unref is a syntax sugar. Judge whether it is ref, and if so, take it value, if not, take the prototype.
    vue.global.js source code:
  function unref(ref) {
      return isRef(ref) ? ref.value : ref;
  }
  • Purpose of unref
    Direct object Attribute can be used, but ref needs it It is easy to use the. Ref or reactive parameter to determine whether it is easy to receive or not, especially when the. Ref or reactive parameter is passed in value, that would be too much trouble. So there is this grammar sugar.

toRef and toRefs

toRef can be used to create a ref for the property property on the source responsive object. The ref can then be passed out to maintain a responsive connection to its source property.
toRefs converts a responsive object into a normal object, where each property of the result object is a ref pointing to the corresponding property of the original object.

In other words, why is the explanation on the official website always so puzzling?
Let's look at examples first

 setup () {
    /**
     * Define reactive
     * Directly deconstruct attributes to see responsiveness
     * Use toRef to deconstruct and see the responsiveness
     * Use toRefs to deconstruct and see the responsiveness
     * The button only modifies reactive
     */
    const person = reactive({
      name: 'jyk',
      age: 18
    })
    console.log('person ', person )

    // Get properties directly
    const name = person.name
    console.log('name ', name )
    
    const refName = toRef(person, 'name')
    console.log('refName ', refName )

    const personToRefs = toRefs(person)
    console.log('personToRefs ', personToRefs )

    const update = () => {
      // Modify prototype
      person.name = new Date()
    }

    return {
      person, // reactive
      name, // get attribute
      refName, // Use toRef
      personToRefs,
      update // modify attribute
    }
  }

When we modify the attribute value of person, the instances of toRef and toRefs will also change automatically. The name attribute obtained directly will not change.

The purpose of toRef is to directly use the attribute name of the object and still enjoy responsiveness.
toRef is the purpose of deconstructing reactive and still enjoying responsiveness.

In fact, it turns into ref, but when we look at the print results, we will find that it is not exactly Ref.

Depending on the class name and attributes, there are also differences between toRef and ref.

Why can toRef respond

toRef is also a syntax sugar.

If you deconstruct reactive in a conventional way, you will find that although the deconstruction is successful, it also loses responsiveness (limited to the properties of the underlying type, except nested objects).

So how to achieve responsiveness after deconstruction? At this time, you need to use toRef.

Looking at the above example, when using refName, it is equivalent to using person['name '], which is responsive.

You might ask, is that it? Yes, it's that simple. If you don't believe it, let's take a look at the source code:

  function toRef(object, key) {
      return isRef(object[key])
          ? object[key]
          : new ObjectRefImpl(object, key);
  }

  class ObjectRefImpl {
      constructor(_object, _key) {
          this._object = _object;
          this._key = _key;
          this.__v_isRef = true;
      }
      get value() {
          return this._object[this._key];  // Equivalent to person['name ']
      }
      set value(newVal) {
          this._object[this._key] = newVal;
      }
  }

Look at the get part. Is it equivalent to person['name ']?

In addition, although it seems that toRef has become ref, it has only become the twin brother of ref (RefImpl), not ref (RefImpl).
ref is RefImpl and toRef is ObjectRefImpl. These are two different class es.
To ref can be regarded as a class of the same series, followed by a class of the same series.

toRefs

After knowing toRef, toRefs is easy to understand. On the surface, all attributes of reactive can be deconstructed. From the perspective of internal code, multiple toRefs are placed in an array (or object).

function toRefs(object) {
      if ( !isProxy(object)) {
          console.warn(`toRefs() expects a reactive object but received a plain one.`);
      }
      const ret = isArray(object) ? new Array(object.length) : {};
      for (const key in object) {
          ret[key] = toRef(object, key);
      }
      return ret;
  }

customRef

Customize a ref and explicitly control its dependency tracking and update trigger. It requires a factory function that takes the track and trigger functions as arguments and should return an object with get and set.

If you don't understand the above paragraph, you can skip it.

Simply put, it is to add our own code on the basis of ref's original set and get, so as to achieve a certain purpose.

In other words, the example written on the official website is really
Anyway, I didn't understand it at first, then I read it again and again, typed the code out and ran it, and checked the meaning of "debounce".
Finally, I finally understand that this is an anti shake (delayed response) code.

Generally, users will respond immediately when they enter content in the text box, but they will be a little depressed if they are in the query function.
The user enters a "1" and immediately goes to the back end to query "1",
Then the user enters a "2" and immediately goes to the back end to query "12",
Then the user enters a "3" and immediately goes to the back end to query "123".
......
This response is very fast, but it is suspected of "tossing". Can you wait for the user to enter "123" and then go to the back-end for query?

The example of the official website is to realize such a function. Let's improve the example, which is obvious.

const useDebouncedRef = (value, delay = 200) => {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track() // Trace function inside vue
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger() // vue internal automatic update function.
        }, delay) // delay time 
      }
    }
  })
}

  setup () {
    const text = useDebouncedRef('hello', 1000) // Define a v-model
    console.log('customRef', text)

    const update = () => {
      // Delayed refresh after modification
      text.value = '1111' + new Date().valueOf()
    }

    return {
      text,
      update
    }
  }

  customRef Object:{{text}} <br><br>
  <input v-model="text" type="text">
  • get
    No change, directly use the original method.

  • set
    Use setTimeout to realize the function of delayed response. Just put Vue's internal trigger() in setTimeout.

In this way, the delay time can be customized. Here is one second. The content entered by the user within one second will be updated at one time, rather than every character entered.

  • v-model="text"
    It can be used as a v-model.

Source code of customRef

Let's take a look at the implementation of the internal source code of customRef.

  function customRef(factory) {
      return new CustomRefImpl(factory);
  }
  class CustomRefImpl {
      constructor(factory) {
          this.__v_isRef = true;
          const { get, set } = factory(() => track(this, "get" /* GET */, 'value'), () => trigger(this, "set" /* SET */, 'value'));
          this._get = get;
          this._set = set;
      }
      get value() {
          return this._get();
      }
      set value(newVal) {
          this._set(newVal);
      }
  }

It's very simple. It's like this first, then like that, and finally it's done.
Well, it is to deconstruct the factory parameter, divide it into set and get, make it into internal functions, and then call it in the internal set and get.

Custom ref instance - write your own calculation attribute.

When it comes to computing attributes, we think of Vue's computed. How can we implement the function of computing attributes by using custom ref? (Note: for practice only)

We can achieve this:

const myComputed = (_get, _set) => {
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        if (typeof _get === 'function') {
          return _get()
        } else {
          console.warn(`Not set get method`)
        }
      },
      set(newValue) {
        if (typeof _set === 'function') {
          _set(newValue)
          trigger()
        } else {
          console.warn(`Not set set method`)
        }
      }
    }
  })
}

setup () {
    const refCount = ref(0)

    const myCom = myComputed(() => refCount.value + 1)
    // const myCom = myComputed(() => refCount.value, (val) => { refCount.value = val})

    const update = () => {
      // Modify prototype
      refCount.value = 3
    }


    const setRef = () => {
      // Direct assignment
      refCount.value += 1
    }

    return {
      refCount, // Foundation type
      myCom, // reference type
      update, // modify attribute
      setRef // Direct setting
    }
  }

  <div>
      Show custom customRef Implement calculated properties <br>
      ref Object:{{refCount}} <br><br>
      Custom calculation property object:{{myCom}} <br><br>
      <input v-model="myCom" type="text">
      <el-button @click="update" type="primary">modify attribute</el-button><br><br>
    </div>
  • myComputed
    First, define a function to receive two parameters, a get and a set.

  • customRef
    Returns an instance of customRef with internal settings of get and set.

  • Call method
    When calling, you can only pass in the get function, or you can pass in the get and set functions.
    Modify refcount Value, the myCom of v-model will also change.

  • practicability
    So what is the effect of this method?
    When making a component, the property props of the component cannot be directly used in the v-model of the internal component, because props is read-only, so what should I do?

You can set a ref inside the component and listen to props, or use computed.
In addition to the above methods, you can also use the methods here. Just change refCount into props attribute, and then use smit to submit in set.

computed

After writing your own calculation properties, let's take a look at the calculation properties provided by Vue.
The code comes from Vue global. JS, adjust the order.

  function computed(getterOrOptions) {
      let getter;
      let setter;
      if (isFunction(getterOrOptions)) {
          getter = getterOrOptions;
          setter =  () => {
                  console.warn('Write operation failed: computed value is readonly');
              }
              ;
      }
      else {
          getter = getterOrOptions.get;
          setter = getterOrOptions.set;
      }
      return new ComputedRefImpl(getter, setter, isFunction(getterOrOptions) || !getterOrOptions.set);
  }
  class ComputedRefImpl {
      constructor(getter, _setter, isReadonly) {
          this._setter = _setter;
          this._dirty = true;
          this.__v_isRef = true;
          this.effect = effect(getter, {
              lazy: true,
              scheduler: () => {
                  if (!this._dirty) {
                      this._dirty = true;
                      trigger(toRaw(this), "set" /* SET */, 'value');
                  }
              }
          });
          this["__v_isReadonly" /* IS_READONLY */] = isReadonly;
      }
      get value() {
          if (this._dirty) {
              this._value = this.effect();
              this._dirty = false;
          }
          track(toRaw(this), "get" /* GET */, 'value');
          return this._value;
      }
      set value(newValue) {
          this._setter(newValue);
      }
  }
  • computed
    Expose the method we use to define a computational attribute. There is only one parameter, which can be a function or an object. The internal will make a judgment and then split it.

  • ComputedRefImpl
    Does it look familiar? This is the same ref series, all in RefImpl style, and the internal code structure is also very similar.
    This is the main class of computed. It also defines the internal properties first, and then sets the get and set of value. In get and set, call the functions set externally.

Source code:

https://gitee.com/naturefw/nf-vue-cdn/tree/master/cdn/project-compositionapi

Online presentation:

https://naturefw.gitee.io/nf-vue-cdn/cdn/project-compositionapi/