Recently Vue officially opened 3.x source code, currently in the Pre Alpha stage, out of interest, I took time to Vue 3.x source code data responsive part to do a brief reading. In this paper, by analyzing the principle of Vue 3.x Reactive API, it is easier to understand the difference between Vue 3.x and Vue 2.x reactive principle.
Before the open source of Vue 3.x, the author has written about the principle of Vue Composition API responsive-wrapped object. The implementation of Vue 3.x Reactive API is similar to this. Students who are interested in Vue 3.
Before reading this article, if you don’t know enough about the following points, you should know the following points:
- Proxy
- WeakMap
- WeakSet
- Reflect
- Vue Composition API
I have written about it before, and you can also combine it with related articles:
- ES6 syntax you may have overlooked – reflection and proxy
- Vue 3.0 update, Composition API
- Vue 3.0 preview, experience Vue Function API
- Vue Composition API responsively wraps object principles
Set up Vue 3.x running environment
Vue 3.x rollup: vue 3.x rollup: vue 3.x rollup: vue 3.x rollup: vue 3.x rollup Generate a file called vue.global.js for developers to reference. For easy debugging, we execute vue-next/scripts/dev.js, and enable rollup watch mode to debug, modify, and output the source code.
In project directory to create a new test. The HTML, references to build packages in the project directory/vue/dist/vue. Global. Js, perform in the project directory NPM run dev, wrote one of the most simple vue 3 x the demo, opened by the browser can run directly, Using this demo, we have built the basic Vue 3.x runtime environment, and now we can start debugging the source code.
<html>
<head>
<title>vue-demo</title>
</head>
<body>
<div id="app"></div>
<script src="./packages/vue/dist/vue.global.js"></script>
<script>
const { createComponent, createApp, reactive, toRefs } = Vue;
const component = createComponent({
template: `
{{ count }}
`,
setup(props) {
const data = reactive({
count: 0});const addHandler = (a)= > {
data.count++;
};
return{... toRefs(data), addHandler, }; }}); createApp().mount(component,document.querySelector('#app'));
</script>
</body>
</html>
Copy the code
Reactive source code
Open the vue – next/packages/reactivity/SRC/reactive. Ts, first of all, you can find the reactive functions are as follows:
export function reactive(target: object) {
// If it is a proxy for a readOnly object, the object is unobservable and the proxy for the readOnly object is returned
if (readonlyToRaw.has(target)) {
return target
}
// If the object is a readonly original object, then the object is also not observable. The proxy of the readOnly object is returned directly
if (readonlyValues.has(target)) {
return readonly(target)
}
Call createReactiveObject to create a Reactive object
return createReactiveObject(
target, // Target object
rawToReactive, // Primitive object maps to responsive object WeakMap
reactiveToRaw, // Reactive objects map to WeakMap of the original object
mutableHandlers, // Proxy handlers for responsive data, typically Object and Array
mutableCollectionHandlers // The proxy handler of the responsive Set is generally Set, Map, WeakMap, WeakSet)}Copy the code
Reactive is a readonly object. If the target object is a readonly object or the proxy object returned by calling ue. Readonly, it is not a readonly object. The readOnly responsive proxy object is returned directly. CreateReactiveObject is then called to create the reactive object.
The five parameters passed by createReactiveObject are: Target Object, original Object mapping responsive Object WeakMap, responsive Object mapping original Object WeakMap, responsive data proxy handler, generally Object and Array, responsive Set proxy handler, generally Set, Map, WeakMap, WeakSet. We can turn to the vue – next/packages/reactivity/SRC/reactive. Ts top, can see defines the following constants:
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()
// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>()
const collectionTypes = new Set<Function> ([Set.Map.WeakMap.WeakSet])
Copy the code
It can be seen that the following four Weakmaps are pre-stored in Reactive: RawToReactive, reactiveToRaw, rawToReadonly, and readonlyToRaw map raw objects to reactive objects and readOnly proxy objects to raw objects respectively. In addition, readonlyValues and nonReactiveValues are defined, which are the collections of readOnly proxy objects and those that call ue. MarkNonReactive and mark them as not corresponding objects respectively. CollectionTypes is the Set of Set, Map, WeakMap and WeakSet
The reason why WeakMap is used for mutual mapping is that WeakMap’s key is weakly referenced. In addition, compared with Map, the algorithm complexity of assignment and search operation of WeakMap is lower than that of Map. For specific reasons, please refer to relevant documents.
Let’s look at createReactiveObject:
function createReactiveObject(target: unknown, toProxy: WeakMap
, toRaw: WeakMap
, baseHandlers: ProxyHandler
, collectionHandlers: ProxyHandler
,>
,>) {
// If it is not an object, return it directly, and the development environment will warn you
if(! isObject(target)) {if (__DEV__) {
console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
}
ToProxy is rawToReactive. WeakMap is used to map reactive proxies
let observed = toProxy.get(target)
if(observed ! = =void 0) {
return observed
}
// The target object is already a responsive Proxy, so it directly returns a responsive Proxy. ToRaw is a reactiveToRaw WeakMap, which is used to map the original object
if (toRaw.has(target)) {
return target
}
// The target object is unobservable and returns the target object directly
if(! canObserve(target)) {return target
}
// Here is the core logic for creating a responsive proxy
// The responsive Object handler of Set, Map, WeakMap, WeakSet is different from the responsive Object handler of Object and Array
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
/ / create the Proxy
observed = new Proxy(target, handlers)
// Update the mapping between rawToReactive and reactiveToRaw
toProxy.set(target, observed)
toRaw.set(observed, target)
Reactive (targetMap) reactive (targetMap) reactive (targetMap) reactive
if(! targetMap.has(target)) { targetMap.set(target,new Map()}return observed
}
Copy the code
Looking at the code above, we know that createReactiveObject is used to create reactive proxy objects:
- In the first place to judge
target
If it is not an object, return it directly. The development environment will give a warning - Then determine whether the target object is already observable. If so, return the created reactive Proxy directly.
toProxy
israwToReactive
thisWeakMap
Is used to map reactive proxies - Then determine whether the target object is already a reactive Proxy. If so, return a reactive Proxy directly.
toRaw
isreactiveToRaw
thisWeakMap
Is used to map the original object - Then create a responsive proxy for
Set
,Map
,WeakMap
,WeakSet
The responder object handler andObject
andArray
The responder object handler is different from the responder object handler - The last update
rawToReactive
andreactiveToRaw
mapping
Reactive proxy traps
Object and Array proxies
Below the center of gravity to the analysis of mutableCollectionHandlers and mutableHandlers, firstly analysis the vue – next/packages/reactivity/SRC/baseHandlers ts, This handler is used to create responsive proxies of type Object and type Array:
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
Copy the code
As we know, the most important are proxy get traps and set traps. Let’s start with get traps:
function createGetter(isReadonly: boolean) {
return function get(target: object, key: string | symbol, receiver: object) {
// Reflect gets the original get behavior
const res = Reflect.get(target, key, receiver)
// If it is a built-in method, no additional proxy is required
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
// If it is a ref object, proxy to ref.value
if (isRef(res)) {
return res.value
}
// track is used to collect dependencies
track(target, OperationTypes.GET, key)
// If it is a nested object, it needs to be handled separately
// If it is a primitive type, return the value directly
return isObject(res)
// createGetter is used to create a responsive object. IsReadonly is passed in as false
// For nested objects, call Reactive recursively to get the result
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
Copy the code
- Get traps pass first
Reflect.get
Get the original GET behavior - Then determine that if it is a built-in method, no additional proxy is required
- Then determine if it is a ref object, proxy to ref. Value
- Then through
track
To collect dependencies - I got it at last
res
If the result is an object type, call Reactive (RES) again to get the result to avoid cyclic references
Here’s the set trap:
function set(target: object, key: string | symbol, value: unknown, receiver: object) :boolean {
// Get the original oldValue
value = toRaw(value)
const oldValue = (target as any)[key]
// If the original value is a ref object and the new assignment is not a ref object, modify the value attribute of the ref wrapper object directly
if(isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true
}
// Whether the original object has the newly assigned key
const hadKey = hasOwn(target, key)
// Reflect gets the original set behavior
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// Manipulate the data in the prototype chain without doing anything to trigger the listener
if (target === toRaw(receiver)) {
/* istanbul ignore else */
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
// Without this key, the attribute is added
// The original attribute is assigned otherwise
// trigger is used to notify DEPS of updates to objects that depend on this state
if(! hadKey) { trigger(target, OperationTypes.ADD, key, extraInfo) }else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
if(! hadKey) { trigger(target, OperationTypes.ADD, key) }else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
}
}
return result
}
Copy the code
- The set trap gets the original value first
oldValue
- And then to determine if the original value is zero
ref
Object, new assignment is notref
Object, directly modifiedref
Object wrappervalue
attribute - Then through
Reflect
Get the original set behavior, if the original object has the newly assigned key, no key, then add the property, otherwise assign the original property - Perform the corresponding modification and add attribute operation through the call
trigger
noticedeps
Update, notifying objects that depend on this state of updates
Set, Map, WeakMap, WeakSet agent
MutableHandlers is analyzed, the following to analyze mutableCollectionHandlers, open the vue – next/packages/reactivity/SRC/collectionHandlers ts, WeakMap, WeakMap, WeakSet responsive Proxy this handler is used to create Set, Map, WeakMap, WeakSet responsive Proxy:
// The method call to listen on
const mutableInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key, toReactive)
},
get size(this: IterableCollections) {
return size(this)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false)}// ...
function createInstrumentationGetter(
instrumentations: Record<string, Function>
) {
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) =>
// If the 'get', 'has',' add ', 'set', 'delete', 'clear', 'forEach' method calls, or obtain 'size', then call the relevant mutableInstrumentations method instead
Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(mutableInstrumentations)
}
Copy the code
Look at the code, we see only a get mutableCollectionHandlers trap, is this why? Because of the limitations of the internal mechanism of Set, Map, WeakMap and WeakSet, the operation of modifying and deleting properties can be completed by means of Set, Add and delete, etc., which cannot be monitored by Proxy setting Set trap, similar to the implementation of variation method of Vue 2.x array. Responsivity is achieved by listening for get, HAS, add, set, delete, clear, forEach method calls in the GET trap and intercepting the method calls.
As for why Set, Map, WeakMap and WeakSet cannot be responsive, the author found the answer in why-is-set-set-with-proxy.
So we understand that the limitation of Proxy on Set, Map, WeakMap and WeakSet is similar to the variation method of Vue 2.x. Intercept method calls of GET, HAS, add, set, delete, clear and forEach to monitor the modification of set, Map, WeakMap and WeakSet data types. It is much easier to look at methods like Get, HAS, add, set, delete, clear, forEach, etc. These methods are similar to get, HAS, set and other trap handlers for object types, and I won’t go into too much detail here.
summary
In this paper, the author continues to pay attention to the dynamics of Vue 3.x. First, the author describes how to build a simple running and debugging environment for Vue 3.x code, and then analyzes the core principle of Vue 3.x responsiveness. Compared with Vue 2.x, Vue 3.x fully embraces the Proxy API for the reactive aspect and implements the reactive through the default behavior of the Proxy initial object. Reactive makes use of weak-reference properties and fast indexing of WeakMap, and uses WeakMap to preserve responsive proxy and original object. Readonly proxy and original object map each other. Finally, the author analyzes the relevant trap method of responsive Proxy, and it can be known that for object and array types, the original object responsiveness is realized through the relevant trap method of responsive Proxy, while for Set, Map, WeakMap, WeakSet types, due to the limitation of Proxy, Vue 3.x implements the reactive principle by hijacking get, HAS, add, set, Delete, clear, forEach and other method calls.