Because Vue 3.0 was just released, I was going to have a brief understanding of it, but after learning about the new features of Vue 3.0, such as Monorepo code management, source code biased toward functional programming, and Composition Api design, I applauded and decided to study it seriously. I bought Vue 3.0 source code analysis at a cheap price, and compiled a note on Vue 3.0 responsive principle based on the recent hands-on Api learning in the pull pull front-end training camp.
The agent
Before you dive into Vue 3.0, you must first understand Javascript proxies, because Vue 3.0’s reactive data is proxy-based. In the Vue 2.0 era, responsive data was implemented based on Object.defineProperty, which can be summarized as follows:
Object. DefineProperty has the advantage of good compatibility and can control the details of Object attributes. However, there are also corresponding problems. Fourth, you can’t handle arrays.
The above problems are solved by Proxy in Vue 3.0.
The proxy pattern
The Proxy is not a unique object of JS. JS Proxy is designed based on the Proxy mode, so understanding the Proxy mode helps us better understand the Proxy. The proxy pattern refers to the design of indirectly manipulated objects, as summarized in a diagram:
The shortcut we usually use on the desktop is actually the implementation of proxy mode, the user does not open the application directly, but through the shortcut of the desktop.
Proxy
Javascript proxies are designed based on the above Proxy pattern and can be used to manipulate objects or arrays indirectly.
Here’s a quick code example to illustrate its use:
const target = { foo: 'bar' }; const handler = { get() { return 'handler override'; }}; const proxy = new Proxy(target, handler); console.log (proxy.foo) // handler override console.log (target.foo) // barCopy the code
Proxy receives two parameters, the first is the object to be Proxy, the second is the handler, it is an object, there are Proxy specified capture methods, such as GET, set, delete, used to operate the Proxy object, trigger different capture. Note that unlike Object.defineProperty, it is based on the entire Object rather than attributes. Users can operate the instance created by Proxy to indirectly operate the object itself. In the example above, add a GET handler that overloads fetching objects.
Get Receives three parameters: trapTarget, Property, and receiver. TrapTarget is the capture object, Property is the property, and Receiver is the proxy object itself. With these parameters, you can reconstruct the original behavior of the captured method:
const target = {
foo: 'bar'
};
const handler = {
get(trapTarget, property, receiver) {
console.log (receiver === proxy) // true
returntrapTarget[property]; }};const proxy = new Proxy(target, handler);
console.log(proxy.foo); // true bar
console.log(target.foo); // bar
Copy the code
All methods that can be captured in a handler object have corresponding Reflection API methods. These methods have the same name and function signature as the methods intercepted by the catcher, and they also have the same behavior as the intercepted methods. Therefore, it is also possible to define empty proxy objects using the reflection API as follows:
const target = {
foo: 'bar'
};
const handler = {
get() {
return Reflect.get(...arguments);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
Copy the code
So much for a brief introduction to Proxy, see MSDN or Javascript Advanced Programming (4th edition) for details.
Reactive simple implementation
The creation of reactive data in Vue 2.0 takes place in a “black box”, where reactive data is created based on the parameters passed in when a Vue instance is created. In Vue 3.0, you can explicitly create responsive data:
<template>
<div>
<p>{{ state.msg }}</p>
<button @click="random">Random msg</button>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({
msg: 'msg reactive'
})
const random = function() {
state.msg = Math.random()
}
return {
random,
state
}
}
}
</script>
Copy the code
The above example imports the reactive function to explicitly create reactive data.
Before reading the reactive source code, implement a simple version of Reactive to understand it.
Check out the official documentation about what Reactive does:
Returns a reactive copy of the object.
The Reactive conversion is “deep” — it affects all properties. In The ES2015 Proxy Based implementation, the returned proxy is not equal to the original object. It is recommended to work exclusively with the reactive proxy and avoid relying on the original object.
In simple terms, Reactive receives an object and returns a reactive copy, which is a Proxy instance in which properties, including nested objects, are reactive.
The function first checks whether the parameter is an object and returns a Proxy instance:
// Because the null typeof is also an object, we need to add additional criteria for it
const isObject = val= >val ! = =null && typeof val === 'object'
function reactive (target) {
if(! isObject (target)) {return target
}
...
return new Proxy(target, handler)
}
Copy the code
Now that you can implement the trap, you need to be careful how you handle nested cases:
const isObject = val => val ! == null && typeof val === 'object' const convert = target => isObject(target) ? reactive(target) : target function reactive (target) { if (! isObject(target)) return target const handler = { get (target, key, receiver) { const result = Reflect.get(target, key, receiver) return convert(result) }, set (target, key, value, receiver) { const oldValue = Reflect.get(target, key, receiver) let result = true if (oldValue ! == value) { result = Reflect.set(target, key, value, receiver) } return result }, deleteProperty (target, key) { const result = Reflect.deleteProperty(target, key) return result } return new Proxy(target, handler) }Copy the code
In the case of nested objects, Vue 2.0 directly recursively transforms them into responsive data when creating instances, while Vue 3.0 deals with them when obtaining corresponding attributes to determine whether they are nested objects, and recursively creates responsive data to optimize performance.
There is a rough implementation, but the most critical responsive part is not yet implemented. The responsive design of Vue 3.0 is similar to Vue 2.0 and also uses the observer pattern, so a simple implementation of Vue 2.0 can be referred to to help you understand the implementation of Vue 3.0.
Vue 3.0 will have a global TargetMap for collecting dependencies with keys for the dependent object and values for the Map, keys for the dependent attribute, and values for the function that needs to be called when the attribute changes. So we need track and trigger functions, the former to collect dependencies, and the latter to call functions when properties change.
let targetMap = new WeakMap(a)// global variables to store dependencies
function track (target, key) {
if(! activeEffect)return
let depsMap = targetMap.get(target) // Get the Map of the dependent object
if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key) // Get the function to be called according to the object properties, create if there is no
if(! dep) { depsMap.set(key, (dep =new Set()))
}
dep.add(activeEffect)
}
function trigger (target, key) {
const depsMap = targetMap.get(target)
if(! depsMap)return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect= > {
effect()
})
}
}
Copy the code
The most critical effects remain to be implemented. Before implementing it, let’s look at a usage example:
<! DOCTYPEhtml>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, initial - scale = 1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import { reactive, effect } from './reactivity/index.js'
const product = reactive({
name: 'iPhone'.price: 5000.count: 3
})
let total = 0
effect(() = > {
total = product.price * product.count
})
console.log(total) / / 15000
product.price = 4000
console.log(total) / / 12000
product.count = 1
console.log(total) / / 4000
</script>
</body>
</html>
Copy the code
As you can see from the above example, when the effect function is called, it executes the function passed in once, and if the value in the function passed in later changes, it calls the function passed in earlier. The question is how does Vue know which function to call? The answer is that when the effect function is called, Vue already collects the dependency when it gets the corresponding value. Let’s look at the implementation of effect:
let activeEffect = null // The global pointer points to the function that was recently passed effect
function effect (callback) {
activeEffect = callback
callback() // Access responsive object properties to collect dependencies
activeEffect = null
}
Copy the code
Use the above example to explain the effect execution process. If we call effect, activeEffect refers to totalSum. If we call totalSum, it will get product.price and product.count, respectively. The get trap for the proxy object is fired, and therefore the track function is fired to collect dependencies. Watch Track again:
let targetMap = new WeakMap(a)// global variables to store dependencies
function track (target, key) {
if(! activeEffect)return
let depsMap = targetMap.get(target) // Get the Map of the dependent object
if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key) // Get the function to be called according to the object properties, create if there is no
if(! dep) { depsMap.set(key, (dep =new Set()))
}
dep.add(activeEffect) // Collect dependencies
}
Copy the code
Add track and trigger to the proxy object to reactive:
const isObject = val= >val ! = =null && typeof val === 'object'
const convert = target= > isObject(target) ? reactive(target) : target
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) = > hasOwnProperty.call(target, key)
function reactive (target) {
if(! isObject(target))return target
const handler = {
get (target, key, receiver) {
// Collect dependencies
track(target, key)
const result = Reflect.get(target, key, receiver)
return convert(result)
},
set (target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let result = true
if(oldValue ! == value) { result =Reflect.set(target, key, value, receiver)
// Trigger the update
trigger(target, key)
}
return result
},
deleteProperty (target, key) {
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
// Trigger the update
trigger(target, key)
}
return result
}
}
return new Proxy(target, handler)
}
Copy the code
Vue 3.0’s responsivity principle is summarized using a diagram found on the web:
The source code to read
Once you have the basics of a simple implementation, you can read the source code.
Reactive function source code in the source path packages/reactivity/SRC/Reactive. Ts.
function reactive (target) {
// If you try to make a readonly proxy responsive, return the readonly proxy directly
if (target && target.__v_isReadonly) {
return target
}
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
}
// isReadonly specifies whether the target is read-only, baseHandlers are the proxy catcher for the base data type, and collectionHandlers are the collection
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
if(! isObject(target)) {// The target must be an object or array type
if((process.env.NODE_ENV ! = ='production')) {
console.warn(`value cannot be made reactive: The ${String(target)}`)}return target
}
if(target.__v_raw && ! (isReadonly && target.__v_isReactive)) {// Target is already a Proxy object
// With one exception, continue if readOnly is applied to a responsive object
return target
}
if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {
// Target already has a Proxy
return isReadonly ? target.__v_readonly : target.__v_reactive
}
// Only whitelisted data types can become responsive
if(! canObserve(target)) {return target
}
// Use Proxy to create reactive style
const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers)
// Mark the raw data to indicate that it has become responsive and has a corresponding Proxy
def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed)
return observed
}
Copy the code
This is the basic build process of Reactive, which is similar to the previous implementation, but with more source code consideration. IsReadonly is a Boolean value that indicates whether the proxy object is read-only. One advantage of Proxy over Object.defineProperty is that it can handle arrays.
The canObserve function further restricts target:
const canObserve = (value) = > {
return(! value.__v_skip && isObservableType(toRawType(value)) && !Object.isFrozen(value))
}
const isObservableType = /*#__PURE__*/ makeMap('Object,Array,Map,Set,WeakMap,WeakSet')
Copy the code
Objects with a __v_SKIP attribute, frozen objects, and objects not in the whitelist cannot become responsive.
const observed = new Proxy(target, collectionTypes.has(target.constructor) ? Creates the proxy object and, according to target’s constructor, returns baseHandlers, whose value is mutableHandlers, if it is a basic data type.
const mutableHandlers = {
get,
set,
deleteProperty,
has,
ownKeys
}
Copy the code
Whichever processor function is hit, it does one of two things: collecting and distributing notifications.
Dependency collection: the GET function
Take a look at the source code for creating the GET trap:
function createGetter(isReadonly = false) {
return function get(target, key, receiver) {
// Returns different results according to different attributes
if (key === "__v_isReactive" /* isReactive */) {
/ / agent observed. __v_isReactive
return! isReadonly }else if (key === "__v_isReadonly" /* isReadonly */) {
/ / agent observed. __v_isReadonly
return isReadonly;
}
else if (key === "__v_raw" /* raw */) {
/ / agent observed. __v_raw
return target
}
const targetIsArray = isArray(target)
// If target is an array and the attribute is contained in arrayInstrumentations
ArrayInstrumentations contains functions that modify some methods of the array
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
/ / evaluated
const res = Reflect.get(target, key, receiver)
// The built-in Symbol key does not rely on collection
if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
return res
}
// Rely on collection! isReadonly && track(target,"get" /* GET */, key)
return isObject(res)
? isReadonly
?
readonly(res)
// If res is an object or array type, execute reactive recursively to make res reactive
: reactive(res)
: res
}
}
Copy the code
Look at arrayInstrumentations, which is a collection of dependencies on an array of proxy methods that call the included methods:
const arrayInstrumentations = {}
['includes'.'indexOf'.'lastIndexOf'].forEach(key= > {
arrayInstrumentations[key] = function (. args) {
// toRaw can convert reactive objects into raw data
const arr = toRaw(this) // This is the array itself
for (let i = 0, l = this.length; i < l; i++) {
// Rely on collection
track(arr, "get" /* GET */, i + ' ')}// Try using the parameters themselves, possibly reactive data
constres = arr[key](... args)if (res === -1 || res === false) {
// If this fails, try again to convert the parameter to the original data
returnarr[key](... args.map(toRaw)) }else {
return res
}
}
})
Copy the code
Why change these methods? Because these methods may get different values when modifying array data, they need to be recollected each time they are called.
Before looking at the track function, take a look at the end of the get function, and finally take different actions based on the result. If it is a basic data type, return the value directly, otherwise the recursion becomes reactive data, which is different from Vue 2.0. Vue 2.0 is the direct recursive processing when creating, while vUE 3 is the judgment whether to process or not when obtaining the attribute, and the delayed definition of the responsive implementation of sub-objects will have a great improvement in performance.
Finally, let’s look at the core function track of GET:
// Whether dependencies should be collected
let shouldTrack = true
// Effect currently active
let activeEffect
// Raw data object map
const targetMap = new WeakMap(a)function track(target, type, key) {
if(! shouldTrack || activeEffect ===undefined) {
return
}
let depsMap = targetMap.get(target)
if(! depsMap) {// Each target corresponds to a depsMap
targetMap.set(target, (depsMap = new Map()))}let dep = depsMap.get(key)
if(! dep) {// Each key corresponds to a deP set
depsMap.set(key, (dep = new Set()))}if(! dep.has(activeEffect)) {// Collect the currently active effects as dependencies
dep.add(activeEffect)
// The currently active effect collects the DEP collection as a dependency
activeEffect.deps.push(dep)
}
}
Copy the code
The basic implementation is the same as the previous simple version, except that effects that are now activated also collect THE DEP as a dependency.
Sending notification: set function
Sending notifications occurs during the data update phase, and since we hijack the data object using the Proxy API, the set function is executed when the reactive object properties are updated. Let’s look at the implementation of the set function, which returns the createSetter function:
function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key]
value = toRaw(value)
const hadKey = hasOwn(target, key)
// Use Reflect to modify
const result = Reflect.set(target, key, value, receiver)
// If the target's prototype chain is also a proxy, modifying the attributes on the prototype chain via reflect.set will trigger the setter again, in which case there is no need to trigger twice
if (target === toRaw(receiver)) {
if(! HadKey) {increment without attribute triggers increment type trigger(target,"add" /* ADD */, key, value)
}
else if(hasChanged(value, oldValue)) {trigger(target,"set" /* SET */, key, value, oldValue)
}
}
return result
}
}
Copy the code
The logic of the set is simple, with the emphasis on the trigger function, which sends out notifications.
WeakMap is characterized by a key that is a reference
const targetMap = new WeakMap(a)function trigger(target, type, key, newValue) {
// Get the target dependency set from targetMap
const depsMap = targetMap.get(target)
if(! depsMap) {// No dependencies, return directly
return
}
// Create a collection of effects to run
const effects = new Set(a)// Add the effects function
const add = (effectsToAdd) = > {
if (effectsToAdd) {
effectsToAdd.forEach(effect= > {
effects.add(effect)
})
}
}
/ / SET | ADD | DELETE operation, one of the corresponding effects
if(key ! = =void 0) {
add(depsMap.get(key))
}
const run = (effect) = > {
// Schedule execution
if (effect.options.scheduler) {
effect.options.scheduler(effect)
}
else {
// Run directly
effect()
}
}
// Iterate over effects
effects.forEach(run)
}
Copy the code
Dispatching notifications is similar to the previous implementation, which takes the corresponding proxy object property collection dependency and then dispatches notifications.
Side effect function: effect
The main focus is on ActiveEffects (the current activation side effect function), which is much more complex to implement than the previous lite version and is the focus of the overall Vue 3.0 responsiveness.
// Global effect stack
const effectStack = []
// Effect currently active
let activeEffect
function effect(fn, options = EMPTY_OBJ) {
if (isEffect(fn)) {
// If fn is already an effect function, point to the original function
fn = fn.raw
}
// Create a wrapper to wrap the incoming function, which is a reactive side effect function
const effect = createReactiveEffect(fn, options)
if(! options.lazy) {// The lazy configuration is used to calculate attributes, and the non-lazy configuration is executed directly once
effect()
}
return effect
}
Copy the code
Instead of simply pointing to the most recently used effect function, the source code also wraps the effect function. See how it wraps:
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect(. args) {
if(! effect.active) {// The original function is executed directly if the execution is not scheduled.
return options.scheduler ? undefined: fn(... args) }if(! effectStack.includes(effect)) {// Clear the dependencies referenced by effect
cleanup(effect)
try {
// Enable global shouldTrack to allow dependency collection
enableTracking()
/ / pressure stack
effectStack.push(effect)
activeEffect = effect
// Execute the original function
returnfn(... args) }finally {
/ / out of the stack
effectStack.pop()
// Restore the state before shouldTrack was started
resetTracking()
// point to the last effect on the stack
activeEffect = effectStack[effectStack.length - 1]}}}// Give effect an ID
effect.id = uid++
// The identifier is an effect function
effect._isEffect = true
// effect state of itself
effect.active = true
// Wrap the original function
effect.raw = fn
// Effect dependencies, bidirectional Pointers, dependencies contain references to effect, and effect also contains references to dependencies
effect.deps = []
// effect configuration
effect.options = options
return effect
}
Copy the code
CreateReactiveEffect returns an effect function with attributes. Wrapping the effect function does two things:
- Set the direction of activeEffect
- In and out of the effectStack
Point 1 was in the easy version before, why have an effectStack? Because you’re dealing with nested scenarios, consider the following scenarios:
import { reactive} from 'vue'
import { effect } from '@vue/reactivity'
const counter = reactive({
num: 0.num2: 0
})
function logCount() {
effect(logCount2)
console.log('num:', counter.num)
}
function count() {
counter.num++
}
function logCount2() {
console.log('num2:', counter.num2)
}
effect(logCount)
count()
Copy the code
We want logCount to be triggered when counter. Num changes, but if there is no stack, there is only an activeEffect pointer. When effect(logCount) is called, the activeEffect pointer points to logCount2, Instead of logCount, so the final result is:
num2: 0
num: 0
num2: 0
Copy the code
Instead of:
num2: 0
num: 0
num2: 0
num: 1
Copy the code
So we need a stack to hold the outer effect function so that the activeEffect pointer points to the outer effect. Revisit the source code:
function createReactiveEffect(fn, options) {...try{.../ / pressure stack
effectStack.push(effect)
activeEffect = effect
// Execute the original function
returnfn(... args) }finally {
// Exit the stack
effectStack.pop()
// Restore the state before shouldTrack was started
resetTracking()
// point to the last effect on the stack
activeEffect = effectStack[effectStack.length - 1]}}}...return effect
}
Copy the code
When effect calls logCount, it pushes logCount into the effectStack, and then inside of logCount, there’s another effect call logCount2 that pushes logCount2 into the effectStack. Num2 collects logCount2 (activeEffect) as a dependency, and effect executes the code in the finally area. Num = counter. Num = counter. Num = logCount; Because activeEffect points to logCount.
If counter. Num changes, logCount is executed.
Finally, there is an unexplained cleanUp function that removes the effect dependency:
function cleanup(effect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0}}Copy the code
In addition to collecting the currently activeEffect as a dependency, activeeffect.deps.push (dep) takes dep as an activeEffect dependency when the track function is executed. This way we can find the corresponding DEPs for effect at the cleanup and remove effect from those DEPs.
Why do you need cleanup? Consider the component’s rendering function as a side effect function, as in the following scenario:
<template>
<div v-if="state.showMsg">
{{ state.msg }}
</div>
<div v-else>
{{ Math.random()}}
</div>
<button @click="toggle">Toggle Msg</button>
<button @click="switchView">Switch View</button>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({
msg: 'Hello World'.showMsg: true
})
function toggle() {
state.msg = state.msg === 'Hello World' ? 'Hello Vue' : 'Hello World'
}
function switchView() { state.showMsg = ! state.showMsg }return {
toggle,
switchView,
state
}
}
}
</script>
Copy the code
This is an example given by Huang Yi in Vue 3.0. His explanation is as follows:
The component’s View will display MSG or a random number based on the control of the showMsg variable, which will be changed when we click the Switch View button.
ActiveEffect is a dependency on state. MSG when the template is rendered for the first time. We call it the render effect. Then we click the Switch View button, and the View switches to display random number. At this time, we click the Toggle Msg button, and since the state. Msg will send a notification, find the Render effect and execute it, and then trigger the re-rendering of the component.
However, this behavior is actually not expected, because when we click the Switch View button and the View switches to display random numbers, the component will also be re-rendered, but the View is not rendering state.msg, so the change to it should not affect the re-rendering of the component.
So before the component’s render effect is executed, we can remove the previously collected render effect dependencies from state.msg if we cleanup the dependencies by cleanup. This way, when we modify state.msg, the component will not be rerendered because there are no more dependencies, as expected.
It’s a bit complicated, but read it a few times to see what cleanUp does.
So that’s what Reactive is all about.