Vue3.0 pre-alpha version was officially released on October 5, 2019, followed by more than 500 PRs and 1000 commits over the next few months, and finally Vue3.0 alpha.1 was released on January 4, 2020. the core code of Vue3.0 is basically complete, and the main work left so far is server-side rendering, which the Vue team is actively working on. The Vue team is also actively working on it. The responsive API code is basically stable and will not change much (the reactivity package in packages), so I will analyze the responsive principle of Vue3.0 from the source code.
Responsive API for Vue 3.0
|
|
Vue3.0 uses the setup
function as the entry point for the entire component, uses the directly imported onXXX
function to register lifecycle hooks, and uses the variables from the return
in the page. There are some changes in the whole writeup compared to Vue 2.0, so we won’t discuss the lifecycle code related to the component here (it belongs to the packages/runtime-core
package).
The most important thing in responsive is the remaining 4 APIs. ref
and reactive
are both methods to convert incoming parameters into responsive objects, the difference is that ref
converts basic data types (string, number, bool, etc.) into responsive data, while reactive
converts other data types into responsive data. To ensure that the basic data is responsive, ref
wraps the basic data in a layer, so in the code above, you need to use count.value
to get the value of count
, but in the template, it is automatically unwrapped (unwrap), so you can use count
directly. computed
has the same role as vue2.0 and represents the computed property. watch
is used to listen for state in the internal logic and will be executed once for each dependency change.
One of the features of Vue is data-driven view updates. By responsive, we mean that when data changes, the part of the view that needs to change is automatically updated. Unlike Vue 2.0, Vue 3.0 uses a monorepo structure that pulls the responsive code into a separate package - reactivity
- which means you can reference this package separately in non-Vue projects to use responsive data.
Source code structure
Let’s explain the structure of the source code to make it clearer.
|
|
In fact, the code logic is basically in baseHandlers.ts
, collectionHandlers.ts
, computed.ts
, effect.ts
, reactive.ts
, ref.ts
which are 6 files. index
is the file that exposes the API to the public, lock
contains the variables and methods that can modify the global responsiveness, and operations
are the constants corresponding to the types of dependency changes triggered by method hijacking. Of course, there are some utility functions used here, all of which come from packages/shared
, and I will analyze them directly in the code when I explain them later.
Data hijacking
Vue2.0 uses Object.defineProperty for data hijacking, which actually proxies each property of the object during initialization, while Vue3.0 uses a combination of Proxy and Reflect to proxy the entire object directly. The difference is that in Vue 3.0 you no longer need to add and delete properties responsively via the vm.$set
and vm.$delete
methods, you don’t need to override the array’s native methods, and listening to the array doesn’t break the JS engine’s rendering, which results in better performance.
Data Interception for Proxy
|
|
The specific usage of Proxy and Reflect can be seen in Mr. Nguyen’s ES6 Primer. Why doesn’t the get function in Proxy return the intercepted property directly, but instead calls the Reflect API?
First, Proxy supports 13 methods for interception, and Reflect has the same 13 methods. These methods include some language-internal methods (such as Object.defineProperty
) that sometimes throw errors during use, and Reflect returns false
in this case.
The Proxy object can easily call the corresponding Reflect method to complete the default behavior as a basis for modifying the behavior. That is, no matter how Proxy modifies the default behavior, you can always get the default behavior on Reflect.
Vue3.0 Responsive Schematic
I combed through the flow of data proxying, method hijacking, dependency collection and triggering in Vue 3.0 and drew the following schematic. It may be a little bit roundabout at first, but you will have a clearer understanding when you look at this diagram after reading the source code parsing behind it.
Reactive
Although Proxy can directly proxy objects, it can only proxy one layer of properties, and you need to manually recursively implement it for deeper detection inside objects. Of course, recursive Proxy has performance pitfalls. How does Vue3.0 avoid excess performance loss?
Vue3.0 caches the set of mapping relationships between the original data and the proxy data to prevent the same data from being repeatedly proxied. Also, when generating responsive data using reactive
, it is not recursive, and only when the trap of get
is triggered by accessing the responsive data, it nests recursive properties for proxy hijacking (unlike Vue2.0 where dependency collection is done at initialization, which will be explained in detail in the handler function).
|
|
Why do we use WeakMap
and WeakSet
to hold mapped data collections? Because WeakMap
and WeakSet
hold objects that are weakly referenced, and the garbage collection mechanism will release the memory occupied by the referenced object as soon as all other references to the object are cleared. Therefore, using these two data structures can better reduce memory overhead, in addition to having a higher search query efficiency.
Vue 3.0 uses three main responsive methods, reactive
, readonly
and shallowReadonly
.
|
|
Here you can see that all three methods actually call the createReactiveObject
method, and we’ll take a look at this method later. reactive
returns responsive data, readonly
returns read-only responsive data, so what does the shallowReadonly
method do? It returns an object with only the outermost read-only responsive data, and it does not recursively proxy the internal data responsively. It is mainly used for props proxy objects created in stateful components.
Let’s start with a few of the tool functions that will be used in the later methods.
|
|
In fact, I have written a lot of TS code in my projects, but after reading the source code, I realized that my TS is only half-assed, and I learned a lot from it.
The isObject
function uses the top-level type unknown
for the input, not any
, to avoid arbitrary operations on the input (which cannot be assigned by types other than unknown
and any
). Also, the function return type uses a type predicate, Record<any, any>
instead of object
, because TS allows access to arbitrary properties of objects of type Record<any, any>
without errors.
canObserve
specifies which data is observable. Non-Vue components, non-dom nodes, of the type defined by isObservableType
and not unresponsive data.
|
|
Although the code is not much, we still have several questions.
-
what does
void 0
mean? Actually it isundefined
. The source code usesvoid 0
instead ofundefined
, first of all becauseundefined
is rewritten under local scope, whilevoid
is not rewritten andvoid 0
has fewer bytes. In fact, many JavaScript compression tools replaceundefined
withvoid 0
in the compression process. -
Why do we use different handler functions for different data types? Vue 3.0 uses two files,
baseHandlers
andcollectionHandlers
, to handle handler functions. ThecollectionHandlers
file is dedicated to handling collection type data (Map, Set, WeakMap, WeakSet), why do collection type data need separate handler functions? Because these collection types use the so-called “internal slots” to access properties directly through the built-in method (this), not through[[Get]]/[[Set]]
, which the Proxy cannot intercept. This is because after using Proxy to proxy a collection type, this=Proxy, not the original object, will not be accessible. So we need to do a layer of function hijacking and just modify the pointing of this to the original mapping.
baseHandlers
baseHandlers.ts
exposes a total of three methods, mutableHandlers
, readonlyHandlers
and shallowReadonlyHandlers
. This corresponds to the three responsive methods mentioned above, reactive
, readonly
and shallowReadonly
.
|
|
In fact, you can see that the three exposed handler methods actually only hijack the get
, set
, has
, ownKeys
and deleteProperty
methods. get
and set
are easy to understand, has
actually hijacks the propKey in proxy
operation, ownKeys
can hijack Object.getOwnPropertyNames(proxy)
, Object.getOwnPropertySymbols(proxy )
, Object.keys(proxy)
, for... .in
method, deleteProperty
hijacks the delete proxy[propKey]
method.
Get
Let’s look at the get hijacking method createGetter
:
|
|
The processing functions for get interceptions are still relatively clear.
When I first saw the arrayIdentityInstrumentations
function, I didn’t understand what its role was, so I had to go to the repository and check the corresponding commit. From that single test, we found that since the three methods includes
, indexOf
, lastIndexOf
all use strict equality to determine the relationship of the found elements, if the responsive data (array) is pushed into a reference type of data, using the above three methods will find no match to the added data. Therefore, it is possible to get the correct matching relationship without proxying these three methods.
After that, the built-in Symbol property of ES6 does not collect dependencies, the data of shallowReadonly is only responsively proxied to the outermost layer, and the data of Ref is not recursively processed because it is a wrapper for basic type data and there is no nested data inside, so the rest needs to be manually recursively proxied. Vue 3.0 collects dependencies through the track
function, which will be analyzed when we talk about the effect
file.
Set
Let’s look at the set hijacking method createSetter
:
|
|
The source code of createSetter
is quite easy to understand with the official comments, and Vue 3.0 implements dependency triggering through trigger
(which will also be analyzed in the effect
file). Beyond that, there are of course a few confusing points.
-
why is there
(value === value || oldValue === oldValue)
logic in thehasChanged
function? BecauseNaN ! == NaN
, so this logic is added to exclude the interference of NaN. -
why
isRef(oldValue) && !isRef(value)
does not need to trigger the dependency? Because theRef
data structure itself has the logic to hijack theset
function (which triggers the dependency), so there is no need to trigger the dependency again. -
target === toRaw(receiver)
What does this logic mean?
// don’t trigger if target is something up in the prototype chain of original
There is this comment on the source code that means don’t do anything to trigger the listener function if it is a data operation in the prototype chain of original data. Still don’t quite understand, so I commented out this line, ran through the single test, and got this test case, I finally understood it. The receiver
is generally an object that has been proxied by the Proxy, but the handler’s set method may also be called indirectly in the prototype chain or in some other way (so not necessarily by the proxy itself).
|
|
If child and parent are 2 Proxy proxies, target
is not equal to toRaw(receiver)
for child. A set operation on child should not change the data on parent, so no listener function will be triggered for data operations on the original data prototype chain.
deleteProperty, has, ownKeys
There are three more functions to hijack. The source code is very simple, add a few lines of comments, no more analysis.
|
|
collectionHandlers
As already mentioned, the four data types Map, Set, WeakMap, WeakSet Proxy can not properly intercept all properties. For example, the proxy set, delete and other methods will directly report an error, but of course the get method of access can still be intercepted normally. Therefore, we can implement a new object, which has all the APIs corresponding to the set data type, and just hijack the proxy to this new object by get.
|
|
The next step is to look at the internal implementation of the two new objects mutableInstrumentations
and readonlyInstrumentations
.
|
|
You can see that in the new object is a proxy for get
, size
, has
, add
, set
, delete
, clear
, forEach
and some methods related to iterators (keys, values, entries, Symbol.iterator).
Let’s start with a few instrumental functions.
|
|
The first two methods are described in the previous section, toReactive
converts raw data into mutable responsive data, toReadonly
converts raw data into read-only responsive data, and getProto
reads the object’s proto property (to get the prototype object).
|
|
Although the collectionHandlers.ts
file is much longer than baseHandlers.ts
, it is much easier to understand the code of baseHandlers.ts
before looking at the code of collectionHandlers.ts
. collectionHandlers.ts
creates a new object to hijack all the collection type data, so inside the hijack function you always get the original data and the prototype method of the original data first, and then bind that method to the original data to call it.
Reactive Summary
reactive
implements data proxying through ES6’s Proxy
and Reflect
APIs to convert to responsive data, which naturally has the advantage of better performance and ease of use, but the disadvantage of not supporting browsers below IE11. It also avoids performance problems caused by nested recursive new Proxy
through lazy access
, unlike Vue2.0 where all dependencies are collected at initialization.
Ref
As already mentioned, reactive
cannot convert basic data types, and Ref
solves the problem of basic data types not being converted to responsive data by a layer of wrapping.
Let’s look at the data type of Ref
.
Although there is a private property in Ref
that determines whether the target object is a Ref
structure, looking up the symbol property on an arbitrary object is much slower than a normal property, so isRef
actually determines whether it is Ref
data by the _isRef
property, which is added when Ref
is generated. The value
property of Ref
is the “unwrapped” type, which is actually determined by the recursive infer to achieve this.
This is a brief look at infer
, because the unwrapper type is used throughout the Reactive
and Ref
files, and it can be difficult to read the source code without knowing something about it.
|
|
In this conditional statement T extends (param: infer P) => any ? P : T
, infer P
represents the function parameter to be inferred.
The whole statement means that if T can be assigned to (param: infer P) => any
, the result is P in type (param: infer P) => any
, otherwise it returns T.
Next, see an example.
In TypeScript 2.8 and later, after the introduction of the infer
feature, there are also many built-in mapping types related to infer
, such as ReturnType
, ConstructorParameters
, InstanceType
and so on. Next, let’s see how the unwrapped types look like.
|
|
We know from the code that the value
structure of Ref
can be of any type, but it must not be nested by a Ref
type, be it Ref<Ref<T>>
or Array<Ref>
, { [key]: Ref }
and so on.
After that, let’s see how to generate Ref
data.
|
|
When generating Ref
, the attribute _isRef
is added to the isRef
function to identify it. Ref
also internally intercepts the get and set functions, which corresponds to the createGetter
function in Reactive.ts
, which does not collect dependencies on data of type Ref
and returns its value directly.
There are also 2 Ref
related methods.
|
|
Convert a reactive object to a plain object, where each property on the resulting object is a ref pointing to the corresponding property in the original object.
As you can see from the official documentation, toRefs
is used to convert responsive data into normal objects, but the properties of the resulting objects are of type Ref and still have responsiveness.
Why does the Ref
object returned in the toProxyRef
function have no collection dependencies or trigger dependencies?
As mentioned before, no dependencies are collected or triggered for Ref
types in reactive
, so doesn’t the value returned by toRefs
have responsiveness? Look at the following example.
In fact, x and y are already proxied to a’s x and y properties, so when accessing x and y or changing their values, a’s set value()
and get value()
functions will be triggered to collect and trigger the dependencies. Therefore, vue3 deliberately removes the trigger
and track
functions from the Ref data returned by toRefs
to prevent duplicate collection and triggering of dependencies.
Effect
Both computed
and watch
are wrapped based on effect
. In this file, we focus on collecting and triggering dependencies.
Let’s look at the type declaration first.
|
|
Then look directly at the code for effect
.
|
|
Here you have seen the role of several properties in the previous effect
configuration item, and then you have to see how to create the listener function.
|
|
If the effect
is active, it will be put in the global effectStack
, and if the value of the responsive data is changed or accessed during the execution of the original function, it will be triggered and collected by trigger
and track
.
There is a question here, every time the effect
function is executed, it is first pushed into the effectStack
and then popped after execution, so under what circumstances does effectStack.includes(effect) === true
?
From this single test, we know that it happens when there is a cyclic dependency on the execution function of effect
, so we need to make sure that the dependency collection and triggering still works when there is a cyclic dependency.
Let’s take a look at how the trigger
and track
functions are implemented.
Track
track
is the dependency collection function.
|
|
targetMap
is where the dependencies are stored globally, in a three-level nested structure of raw data -> properties -> dependency collections.
Why do we need deps
inside each effect
to store dependencies when we already have targetMap
as the global structure for storing dependencies?
The answer is given in the createReactiveEffect
function earlier. Each time the function is executed, when the dependency does not exist in the effectStack
, the cleanup
function is executed to clear the dependency mapping from the targetMap
using the internal deps
. The same is true when the stop
function is executed.
So here’s another question, why do we need to clear our own dependencies before each function execution? From this single test we know that when there is a conditional branch within a function, each execution may cause the dependency data to be different, so we need to collect the dependencies again before each execution.
Trigger
trigger
is the function whose dependency is triggered.
|
|
trigger
is a process that executes dependencies by maintaining a queue of effects
and computedRunners
when responsive data changes, and then calling scheduleRun
afterwards.
Effect Summary
effect
describes how to collect dependencies, manage dependencies and trigger dependencies.
Each time a listener is executed, it is placed in the effectStack
queue, cached as activeEffect
, popped out of the effectStack
queue when execution is complete, and the activeEffect
value is changed to the last listener in the effectStack
queue. The listener function is put into the dependency of the accessed responsive data by the track
function when it is executed for the first time and is saved in the targetMap
collection. When the responsive data is modified, it is triggered by the trigger
function, and the corresponding dependencies are taken out of the targetMap
and put into the computedRunners
and effects
dependency execution queues according to the listener’s category. After that, the listeners in the computedRunners
and effects
queues are executed in this order.
Computed
Let’s start with a few instrumental functions and type declarations
|
|
As you can see from the type declaration ComputedRef
is a read-only Ref
that has effect
.
|
|
As you can see from the source code, the value that goes into computedRef
is read-only, but we can also manually change the value of the computed property after passing in custom set and get functions using computed
. (This is consistent with vue2.0, which also provides a custom set method to change the computed property)
By using the dirty flag in the source code, we can avoid the problem of repeatedly triggering dependencies on computed properties. This is because the getter function of computed
is triggered when a computed property and a computed property that depends on it are accessed at the same time, which would lead to multiple executions of trigger
if the dirty flag bit did not exist. You can see this single test.
Summary
The reason we didn’t explain the watch
API here is that it is actually part of the runtime code (packages/runtime-core/src/apiWatch.ts
), but it is actually underpinned by effect
, so we can get an idea of what watch
does and how it works. Of course, those who want to understand the watch
API thoroughly are advised to look at the runtime code, which is still more extended than effect
.
After reading through the article, you can go back to the schematic above to get a deeper understanding.
In fact, the whole process of reading the source code was not easy and took a lot of effort. This includes a lot of advanced usage of TS, figuring out the author’s intention, and so on. At the same time, I also read some excellent source code analysis articles on the Internet, and sometimes when I can’t understand a certain piece of logic, I can see other people’s analysis and ideas and immediately be enlightened. Of course, there will be a lot of wrong interpretation, you have to determine their own context with the code.
Read the source code also has a lot of skills, the first is to understand the author’s comments, and then to make reasonable use of the single test, such as a piece of logic commented out, run a single test to see which sample hung, you can go to “guess” the role of the commented out logic. Vue’s projects have always had good commit specifications, making the code highly readable, which is great. I have also promoted Angular’s commit specification - commitizen in my team before. If a commit is merged in through a PR, you can also go to the repository and look through the PR, where the author of the PR will clearly state what the PR does.
Vue3.0 is rewritten using TypeScript, replacing Object.defineProperty
with Proxy
for data detection, improving responsive performance and allowing users to manipulate data more freely. At the same time, the Virtual DOM is refactored to adopt the idea of “combining motion and static”, which greatly improves the update performance of vdom.
A few months have passed since I started reading the source code to finish this article, and Vue3 has entered the subsequent optimization and finishing work. I am very happy to finally finish this article, of course, there is a lot of content belongs to the author’s speculation, if you have questions about any of the code analysis of this module in the statement or have a correction, you are very welcome to contact me, I will correct as soon as possible!