Location>code7788 >text

The interview question that 70% of people answer incorrectly, how does vue3 ref implement responsive?

Popularity:83 ℃/2024-07-29 09:21:23

preamble

Recently in myvue source code exchange groupOne interviewer shared one of his interview questions:How does vue3's ref implement responsive?There are quite a few peeps below who answeredProxyActually, these guys only answered half of the questions correctly
wx

  • It is true that when ref receives an object it relies on theProxyGo for responsive.

  • But ref can still receivestringnumber maybeboolean Such a primitive type, when it is a primitive type, responsive is not relying on theProxyto realize it, but rather in thevalueattributegettercap (a poem)settermethod to go in the responsive implementation.

This article will take you through a debug to figure out how responsive is implemented when the ref receives an object and a primitive type, respectively. Note: The version of vue used in this article is3.4.19

Follow the public number: [Front-end Ouyang], give yourself a chance to advance vue

Look at the demo.

Same old story. Let's get a demo going.The file code is as follows:

<template>
  <div>
    <p>countThe value of the:{{ count }}</p>
    <p>The value of the:{{ }}</p>
    <button @click="count++">count++</button>
    <button @click="++">++</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const count = ref(0);

const user = ref({
  count: 0,
});
</script>

In the demo above we have two ref variables, thecountThe variable receives the primitive type, and his value is the number 0.

countvariable is rendered in the template's p tag, and in the button's click event it willcount++

userVariables receive objects, which have acountProperties.

equalis also rendered on the other p tag, and the click event on the other button will++

Next I'll walk you through the debugging process to figure it out by clicking on each of thecount++cap (a poem)++How responsive is implemented when buttons.

point of interruption (math.)

Where do you start with the first step to break the point?

Since we're trying to figure out how the ref implements responsiveness, it's of course a good idea to give the ref a breakpoint, so our first breakpoint is the one we hit in theconst count = ref(0);at the code. This line of code is runtime code and is running in the browser.

To break points in the browser, you need to open the browser's source panel in thefile before you can put a breakpoint on the code.

So here comes the second question, how to find in the source panel what we have here in theWhere's the file?

It's easy, use it like in vscode.command+p(control+p in windows) will bring up an input box. Inside the input box, typeThen click enter to open the source panel.File. As shown below:
index

We can then give theconst count = ref(0);The break point is hit at the top.

RefImplresemble

Refreshing the page at this point will leave the breakpoint atconst count = ref(0);At the code, let the breakpoint go into thereffunction. In our scenario the simplifiedrefThe function code is as follows:

function ref(value) {
  return createRef(value, false);
}

It can be seen in thereffunction is actually a direct call to thecreateReffunction.

Then walk the breakpoint into thecreateReffunction, in our scenario the simplifiedcreateRefThe function code is as follows:

function createRef(rawValue, shallow) {
  return new RefImpl(rawValue, shallow);
}

As you can see from the code above the actual call to theRefImplclass NEW an object, the first parameter passed to therawValue, that is, the value of the variable to which the ref is bound, which can be a primitive type or an object, array, etc.

Then walk the breakpoint into theRefImplclass, in our scenario the simplifiedRefImplThe class code is as follows:

class RefImpl {
  private _value: T
  private _rawValue: T

  constructor(value) {
    this._rawValue = toRaw(value);
    this._value = toReactive(value);
  }
  get value() {
    trackRefValue(this);
    return this._value;
  }
  set value(newVal) {
    newVal = toRaw(newVal);
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = toReactive(newVal);
      triggerRefValue(this, 4, newVal);
    }
  }
}

As you can see from the code aboveRefImplThe class consists of three parts:constructorConstructor,valueattributegetterMethods,valueattributesetterMethods.

RefImplclassconstructorconstructor

constructorThe code in the constructor is simple, as follows:

constructor(value) {
  this._rawValue = toRaw(value);
  this._value = toReactive(value);
}

In the constructor the first thing that happens is that thetoRaw(value)value assigned to the_rawValueattribute, thistoRawfunction is an API exposed by vue that is designed to return the original object of a Vue-created proxy. Since therefFunctions can accept not only normal objects and primitive types, but also a ref object, so this is the place to use thetoRaw(value)Get the original value and save it to_rawValuein the attribute.

Then in the constructor it will execute thetoReactive(value)function, assigning the result of its execution to the_valueProperties.toReactivefunction look at the name you should have guessed, if the received value is a primitive type, then directly return value. if the received value is not a primitive type (such as object), then return a value converted responsive object. ThistoReactivefunctions we'll talk about below.

_rawValueattributes and_valueThe attributes are allRefImplA private property of the class for use in theRefImplused in the class, and the only ones exposed are thevalueProperties.

go throughconstructorAfter the constructor's processing, the two private properties are assigned values respectively:

  • _rawValueis the original value of the ref binding.

  • If ref is bound to a primitive type, such as the number 0, then the_valueIt is the number 0 that is stored in the attribute.

    If the ref is bound to an object, then the_valueWhat's stored in the property is the responsive object after the bound object is converted.

RefImplclassvalueattributegettermethodologies

Let's move on.valueattributegettermethod with the following code:

get value() {
  trackRefValue(this);
  return this._value;
}

When we do a read operation on the value attribute of the ref we go to thegetterMethods in.

We know that template after compilation will become render function, the execution of render function will generate virtual DOM, and then by the virtual DOM to generate the real DOM.

During the execution of the render function a change is made to thecountvariable for a read operation, so this triggers thecountvariablevalueattribute corresponding to thegetterMethods.

existgettermethod will call thetrackRefValuefunction for dependency collection, and since this is during the execution of the render function, the dependency collected is the render function.

end upgettermethod will return the_valuePrivate Attributes.

RefImplclassvalueattributesettermethodologies

Let's move on.valueattributesettermethod with the following code:

set value(newVal) {
  newVal = toRaw(newVal);
  if (hasChanged(newVal, this._rawValue)) {
    this._rawValue = newVal;
    this._value = toReactive(newVal);
    triggerRefValue(this, 4, newVal);
  }
}

When we do a write operation on the attribute of the value of the ref we go to thesettermethod, for example, by clicking on thecount++button, there will be a change to thecountvalue of+1The write operation is triggered to go to thesetterMethods in.

do sth (for sb)settermethod to make a breakpoint and click thecount++button, the breakpoint will go to thesettermethod. Initializationcounthas a value of 0. At this point, after clicking the button the newcountThe value is 1, so the value ofsettermethod received in thenewValThe value of 1. as shown below:
set

You can see the new values in the graph abovenewValhas a value of 1 and the old valuethis._rawValueand then use theif (hasChanged(newVal, this._rawValue))to determine whether the new value and the old value are equal.hasChangedThe code for this is also simple, as follows:

const hasChanged = (value, oldValue) => !(value, oldValue);

method you may usually use less, the role is also to determine whether the two values are equal. and==differThere will be no forced conversion, you can refer to the documentation on mdn for other differences.

utilizationhasChangedWhen the function determines that the new value is not equal to the old one, it will go to an if statement, which will first execute thethis._rawValue = newValSet the private attribute_rawValuevalue is updated to the latest value. The next step is to execute thethis._value = toReactive(newVal)Set the private attribute_valuevalue is updated to the latest value.

And finally, the implementationtriggerRefValuefunction triggers the collection of dependencies, and earlier we talked about the dependencies that are created during the execution of the render function as a result of a change to thecountvariable to perform a read operation. A read operation is triggered by thegettermethod in thegettermethod collects the render function as a dependency.

So at this point, executetriggerRefValuefunction takes out all the collected dependencies and executes them again. Since the render function is also a collected dependency, the render function is re-executed. Re-executing the render function takes the dependencies from thecountThe value taken out of the variable is the new value 1, followed by generating the virtual DOM, then mounting the virtual DOM onto the real DOM, and eventually on the pagecountThe value of the variable binding has been updated to 1.

See here you are not think about ref to realize responsive has finished?

Let's look at the second example in the demo.userobject, recall that in templates and scripts about theuserThe code for the object is as follows:

<template>
  <div>
    <p>The value of the:{{ }}</p>
    <button @click="++">++</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const user = ref({
  count: 0,
});
</script>

Executed in the click event of the button button is:++As we mentioned earlier, a write operation on the value attribute of a ref goes to thesettermethod. But we're ref binding an object here, and clicking the button isn't a response to theattribute, but instead performs a write operation on theattribute to perform a write operation. So clicking the button here won't go to thesettermethod, and of course the collected dependencies are not re-executed.

So when the ref is bound to an object, how do we do a responsive update when we change a property of the object?

In this case it is necessary to use theProxyNow, remember what we talked about earlier.RefImplclassconstructorConstructor? The code is as follows:

class RefImpl {
  private _value: T
  private _rawValue: T

  constructor(value) {
    this._rawValue = toRaw(value);
    this._value = toReactive(value);
  }
}

That's it, actually.toReactivefunction at work.

ProxyImplementing Responsive

It's the same routine, this time we give the bound object a name ofuserThe ref to hit a breakpoint, refresh the page code stays in the breakpoint. It's still the same process as before eventually the breakpoint goes to theRefImplclass constructor, when the code executes into thethis._value = toReactive(value)When the breakpoints walk into thetoReactivefunction. The code is as follows:

const toReactive = (value) => (isObject(value) ? reactive(value) : value);

existtoReactivefunction determines if the currentvalueis an object, returnreactive(value)Otherwise, the value is returned directly.reactivefunction you should be familiar with, which returns a responsive proxy for an object. Since thereactivedoes not accept primitive types like number, that's why it is judged herevalueWhether it is an object.

We then walk the breakpoint into thereactivefunction to see how he returns a responsive object, in our scenario the simplifiedreactiveThe function code is as follows:

function reactive(target) {
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  );
}

As you can see from the code above in thereactivefunction is a direct return of thecreateReactiveObjectfunction is called with the third argumentmutableHandlers. As you might have guessed from the name, he's a processor object for a Proxy object, more on that later.

Then walk the breakpoint into thecreateReactiveObjectfunction, the simplified code in our scenario is as follows:

function createReactiveObject(
  target,
  isReadonly2,
  baseHandlers,
  collectionHandlers,
  proxyMap
) {
  const proxy = new Proxy(target, baseHandlers);
  return proxy;
}

In the code above we finally see the big-nameProxyHere's a newProxyobject. the first parameter passed in when new is thetargetThis one.targetis the object we passed in all the way to the ref binding. The second parameter isbaseHandlers, which is a processor object of a Proxy object. ThisbaseHandlersis the invocationcreateReactiveObjectThe third parameter that is passed in during themutableHandlersObject.

Here the object of the Proxy proxy is finally returned, and the ref binding in our demo is to an object nameduserobject, after the layers of RETURN of the function described earlier, theis the value returned by return hereproxyObject.

When we are interested inThe get intercept of the Proxy here is triggered when a read operation is performed on a property of the responsive object.

When we are interested inThe set intercept of the Proxy here is triggered when a write operation is performed on a property of the responsive object.

getcap (a poem)setThe code for the interception is in themutableHandlersin the object.

Proxy(used form a nominal expression)setcap (a poem)getinterdiction

Use a search in the source codemutableHandlersobject, see his code as follows:

const mutableHandlers = new MutableReactiveHandler();

As you can see from the code abovemutableHandlersObjects are created using theMutableReactiveHandlerAn object that the class NEW.

Let's move on.MutableReactiveHandlerclass, the simplified code in our scenario is as follows:

class MutableReactiveHandler extends BaseReactiveHandler {
  set(target, key, value, receiver) {
    let oldValue = target[key];

    const result = (target, key, value, receiver);
    if (target === toRaw(receiver)) {
      if (hasChanged(value, oldValue)) {
        trigger(target, "set", key, value, oldValue);
      }
    }
    return result;
  }
}

In the code above we see thesetIntercepted, but didn't seegetInterception.

MutableReactiveHandlerclass is the class that inherits theBaseReactiveHandlerclass, let's take a look at theBaseReactiveHandlerclass, in our scenario the simplifiedBaseReactiveHandlerThe class code is as follows:

class BaseReactiveHandler {
  get(target, key, receiver) {
    const res = (target, key, receiver);
    track(target, "get", key);
    return res;
  }
}

existBaseReactiveHandlerclass we find thegetinterception, when we do a read operation on a property of an object returned by the Proxy proxy it goes to thegetInterception in progress.

After the aforementioned layers of returnThe value of theproxyresponsive object, while we use the template inRender it to a p tag, read it in templateThe actual reading of theThe value of the

The same template is compiled into a render function, which generates a virtual DOM and then converts the virtual DOM into a real DOM to render to the browser. During the execution of the render function, theperforms a read operation, so it triggers theBaseReactiveHandlerHere.getInterception.

existgetThe interception will execute thetrack(target, "get", key)function, the current render function will be used as a dependency for collection after execution. Now that the dependency collection part is done, the rest is the dependency triggering part.

Let's move on.MutableReactiveHandlerHe's inherited it.BaseReactiveHandlerThe InBaseReactiveHandlerThere's agetIntercepts, while in theMutableReactiveHandlerThere's asetInterception.

When we click on the++When the button is pressed, thePerform a write operation. Since there is no limit to the number of writes that can be performed on thecountattribute does a write operation, so it goes to thesetInterception in progress.setThe interception code is as follows:

class MutableReactiveHandler extends BaseReactiveHandler {
  set(target, key, value, receiver) {
    let oldValue = target[key];

    const result = (target, key, value, receiver);
    if (target === toRaw(receiver)) {
      if (hasChanged(value, oldValue)) {
        trigger(target, "set", key, value, oldValue);
      }
    }
    return result;
  }
}

Let's take a look.setIntercepts the incoming 4 parameters, the first of which is thetargetwhich is the original object before our proxy. The second parameter iskey, the property that performs the write operation, in our casekeyis the value of the stringcount. The third parameter is the new attribute value.

Fourth parameterreceiverIt is generally the proxy responsive object returned by the Proxy. Why does it say generally here? Take a look at MDN's explanation above and you should be able to understand:

Suppose there is a piece of code that executes = "jen", obj is not a proxy and does not contain aname attribute, but it has a proxy in its prototype chain, so that proxy'sset() processor will be called, and at that point, theobj will be passed in as the receiver parameter.

Let's move on.setin the interceptor function, firstlet oldValue = target[key]Get the old attribute values and use the(target, key, value, receiver)

existProxyIt's usually paired withReflectTo carry out the use of theProxy(used form a nominal expression)getUse in InterceptinProxy(used form a nominal expression)setUse in Intercept

This has a couple of advantages, in the set intercept we have to return a boolean value indicating whether the property assignment was successful or not. If we use the traditionalobj[key] = valueWe don't know if the assignment succeeded or not.A result is returned indicating whether or not the assignment to the object's properties was successful. In the set intercept it is straightforward to set theIt is sufficient to RETURN the result of the

There is also the added benefit that if not used in conjunction with a possiblethisThe problem of pointing the wrong way.

We've talked about this before.receivermay not be a proxy-responsive object returned by the Proxy, so here you need to use theif (target === toRaw(receiver))Make a judgment.

The next step is to use theif (hasChanged(value, oldValue))Performs a judgment on whether the new value and the old value are equal, and if they are not, performs thetrigger(target, "set", key, value, oldValue)

this onetriggerfunction is for dependency triggering, it will take out all the collected dependencies and execute them once, and since the render function is also a collected dependency, the render function will be re-executed. The re-execution of the render function starts with theThe value taken out of the attribute is the new value 1, followed by generating the virtual DOM, then mounting the virtual DOM onto the real DOM, and finally on the pageThe value of the property binding has been updated to 1.

This is how Proxy is used to implement responsive when the ref binding is to an object.

Seeing this some of you may have a question as to why ref uses theRefImplclass to implement it, instead of uniformly using theProxyTo represent a person who hasvalueWhat about ordinary objects with attributes? Such as the following:

const proxy = new Proxy(
  {
    value: target,
  },
  baseHandlers
);

If this is done above then there is no need to use theRefImplclass now, all unified into Proxy to use responsive now.

One problem with the above approach, however, is that the user can use thedelete commander-in-chief (military)proxytargetvalueattribute is removed. Instead of using theRefImplA class approach to implementation would not be able to use thedeleteThe method of going to thevalueattribute is removed.

summarize

In this article we talked aboutrefis how to realize the responsive, mainly divided into two cases: ref receives the primitive type of number, ref receives the non-primitive type of object.

  • When ref receives a primitive type such as number it relies on theRefImplclassvalueattributegettercap (a poem)settermethod to go in the responsive implementation.

    When we perform a read operation on the value attribute of a ref it triggers the value'sgettermethod for dependency collection.

    When we write to the value attribute of a ref a dependency trigger is performed and the render function is re-executed for responsive purposes.

  • When ref receives a non-primitive type such as object, it callsreactivemethod converts the value attribute ofref into a value attribute defined by theProxyImplemented responsive objects.

    This is triggered when we perform a read operation on one of the attributes of the value object of the ref.Proxy's get intercept for dependency collection.

    This is triggered when we write to one of the attributes of the value object of the ref.Proxyof the set intercept for dependency triggering, and then re-execute the render function for responsive purposes.

Finally, we talked about why refs are not used uniformlyProxyGoing to represent someone who hasvalueattribute of a normal object to implement responsive, but instead to get an extraRefImplClass.

Because if you use theProxyTo deproxy a normal object with a value attribute, you can use thedelete commander-in-chief (military)proxytargetvalueattribute is removed. Instead of using theRefImplA class approach to implementation would not be able to use thedeleteThe method of going to thevalueattribute is removed.

Follow the public number: [Front-end Ouyang], give yourself a chance to advance vue