preamble
Recently in myvue source code exchange group
One interviewer shared one of his interview questions:How does vue3's ref implement responsive?There are quite a few peeps below who answeredProxy
,Actually, these guys only answered half of the questions correctly。
-
It is true that when ref receives an object it relies on the
Proxy
Go for responsive. -
But ref can still receive
string
、number
maybeboolean
Such a primitive type, when it is a primitive type, responsive is not relying on theProxy
to realize it, but rather in thevalue
attributegetter
cap (a poem)setter
method 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, thecount
The variable receives the primitive type, and his value is the number 0.
count
variable is rendered in the template's p tag, and in the button's click event it willcount++
。
user
Variables receive objects, which have acount
Properties.
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:
We can then give theconst count = ref(0);
The break point is hit at the top.
RefImpl
resemble
Refreshing the page at this point will leave the breakpoint atconst count = ref(0);
At the code, let the breakpoint go into theref
function. In our scenario the simplifiedref
The function code is as follows:
function ref(value) {
return createRef(value, false);
}
It can be seen in theref
function is actually a direct call to thecreateRef
function.
Then walk the breakpoint into thecreateRef
function, in our scenario the simplifiedcreateRef
The 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 theRefImpl
class 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 theRefImpl
class, in our scenario the simplifiedRefImpl
The 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 aboveRefImpl
The class consists of three parts:constructor
Constructor,value
attributegetter
Methods,value
attributesetter
Methods.
RefImpl
classconstructor
constructor
constructor
The 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_rawValue
attribute, thistoRaw
function is an API exposed by vue that is designed to return the original object of a Vue-created proxy. Since theref
Functions 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_rawValue
in the attribute.
Then in the constructor it will execute thetoReactive(value)
function, assigning the result of its execution to the_value
Properties.toReactive
function 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. ThistoReactive
functions we'll talk about below.
_rawValue
attributes and_value
The attributes are allRefImpl
A private property of the class for use in theRefImpl
used in the class, and the only ones exposed are thevalue
Properties.
go throughconstructor
After the constructor's processing, the two private properties are assigned values respectively:
-
_rawValue
is the original value of the ref binding. -
If ref is bound to a primitive type, such as the number 0, then the
_value
It is the number 0 that is stored in the attribute.If the ref is bound to an object, then the
_value
What's stored in the property is the responsive object after the bound object is converted.
RefImpl
classvalue
attributegetter
methodologies
Let's move on.value
attributegetter
method 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 thegetter
Methods 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 thecount
variable for a read operation, so this triggers thecount
variablevalue
attribute corresponding to thegetter
Methods.
existgetter
method will call thetrackRefValue
function for dependency collection, and since this is during the execution of the render function, the dependency collected is the render function.
end upgetter
method will return the_value
Private Attributes.
RefImpl
classvalue
attributesetter
methodologies
Let's move on.value
attributesetter
method 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 thesetter
method, for example, by clicking on thecount++
button, there will be a change to thecount
value of+1
The write operation is triggered to go to thesetter
Methods in.
do sth (for sb)setter
method to make a breakpoint and click thecount++
button, the breakpoint will go to thesetter
method. Initializationcount
has a value of 0. At this point, after clicking the button the newcount
The value is 1, so the value ofsetter
method received in thenewVal
The value of 1. as shown below:
You can see the new values in the graph abovenewVal
has a value of 1 and the old valuethis._rawValue
and then use theif (hasChanged(newVal, this._rawValue))
to determine whether the new value and the old value are equal.hasChanged
The 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.
utilizationhasChanged
When 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 = newVal
Set the private attribute_rawValue
value is updated to the latest value. The next step is to execute thethis._value = toReactive(newVal)
Set the private attribute_value
value is updated to the latest value.
And finally, the implementationtriggerRefValue
function 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 thecount
variable to perform a read operation. A read operation is triggered by thegetter
method in thegetter
method collects the render function as a dependency.
So at this point, executetriggerRefValue
function 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 thecount
The 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 pagecount
The 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.user
object, recall that in templates and scripts about theuser
The 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 thesetter
method. 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 the
attribute to perform a write operation. So clicking the button here won't go to the
setter
method, 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 theProxy
Now, remember what we talked about earlier.RefImpl
classconstructor
Constructor? 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.toReactive
function at work.
Proxy
Implementing Responsive
It's the same routine, this time we give the bound object a name ofuser
The 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 theRefImpl
class constructor, when the code executes into thethis._value = toReactive(value)
When the breakpoints walk into thetoReactive
function. The code is as follows:
const toReactive = (value) => (isObject(value) ? reactive(value) : value);
existtoReactive
function determines if the currentvalue
is an object, returnreactive(value)
Otherwise, the value is returned directly.reactive
function you should be familiar with, which returns a responsive proxy for an object. Since thereactive
does not accept primitive types like number, that's why it is judged herevalue
Whether it is an object.
We then walk the breakpoint into thereactive
function to see how he returns a responsive object, in our scenario the simplifiedreactive
The function code is as follows:
function reactive(target) {
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
);
}
As you can see from the code above in thereactive
function is a direct return of thecreateReactiveObject
function 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 thecreateReactiveObject
function, 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-nameProxy
Here's a newProxy
object. the first parameter passed in when new is thetarget
This one.target
is 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. ThisbaseHandlers
is the invocationcreateReactiveObject
The third parameter that is passed in during themutableHandlers
Object.
Here the object of the Proxy proxy is finally returned, and the ref binding in our demo is to an object nameduser
object, after the layers of RETURN of the function described earlier, theis the value returned by return here
proxy
Object.
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.
get
cap (a poem)set
The code for the interception is in themutableHandlers
in the object.
Proxy
(used form a nominal expression)set
cap (a poem)get
interdiction
Use a search in the source codemutableHandlers
object, see his code as follows:
const mutableHandlers = new MutableReactiveHandler();
As you can see from the code abovemutableHandlers
Objects are created using theMutableReactiveHandler
An object that the class NEW.
Let's move on.MutableReactiveHandler
class, 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 theset
Intercepted, but didn't seeget
Interception.
MutableReactiveHandler
class is the class that inherits theBaseReactiveHandler
class, let's take a look at theBaseReactiveHandler
class, in our scenario the simplifiedBaseReactiveHandler
The class code is as follows:
class BaseReactiveHandler {
get(target, key, receiver) {
const res = (target, key, receiver);
track(target, "get", key);
return res;
}
}
existBaseReactiveHandler
class we find theget
interception, when we do a read operation on a property of an object returned by the Proxy proxy it goes to theget
Interception in progress.
After the aforementioned layers of returnThe value of the
proxy
responsive object, while we use the template inRender it to a p tag, read it in template
The actual reading of the
The 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 the
BaseReactiveHandler
Here.get
Interception.
existget
The 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.MutableReactiveHandler
He's inherited it.BaseReactiveHandler
The InBaseReactiveHandler
There's aget
Intercepts, while in theMutableReactiveHandler
There's aset
Interception.
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 the
count
attribute does a write operation, so it goes to theset
Interception in progress.set
The 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.set
Intercepts the incoming 4 parameters, the first of which is thetarget
which is the original object before our proxy. The second parameter iskey
, the property that performs the write operation, in our casekey
is the value of the stringcount
. The third parameter is the new attribute value.
Fourth parameterreceiver
It 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.set
in the interceptor function, firstlet oldValue = target[key]
Get the old attribute values and use the(target, key, value, receiver)
existProxy
It's usually paired withReflect
To carry out the use of theProxy
(used form a nominal expression)get
Use in Interceptin
Proxy
(used form a nominal expression)set
Use 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] = value
We 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 the
It is sufficient to RETURN the result of the
There is also the added benefit that if not used in conjunction with a possiblethis
The problem of pointing the wrong way.
We've talked about this before.receiver
may 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 onetrigger
function 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 page
The 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 theRefImpl
class to implement it, instead of uniformly using theProxy
To represent a person who hasvalue
What 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 theRefImpl
class 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)proxy
targetvalue
attribute is removed. Instead of using theRefImpl
A class approach to implementation would not be able to use thedelete
The method of going to thevalue
attribute is removed.
summarize
In this article we talked aboutref
is 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 the
RefImpl
classvalue
attributegetter
cap (a poem)setter
method to go in the responsive implementation.When we perform a read operation on the value attribute of a ref it triggers the value's
getter
method 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 calls
reactive
method converts the value attribute ofref into a value attribute defined by theProxy
Implemented 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.
Proxy
of 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 uniformlyProxy
Going to represent someone who hasvalue
attribute of a normal object to implement responsive, but instead to get an extraRefImpl
Class.
Because if you use theProxy
To deproxy a normal object with a value attribute, you can use thedelete
commander-in-chief (military)proxy
targetvalue
attribute is removed. Instead of using theRefImpl
A class approach to implementation would not be able to use thedelete
The method of going to thevalue
attribute is removed.
Follow the public number: [Front-end Ouyang], give yourself a chance to advance vue