The introduction
Vue. Js 3.0 “One Piece” has been officially released for some time now.
Compared to Vue2. X, the new version of Vue3.0 offers better performance, a smaller bundle size, better TypeScript integration, and new apis for handling large-scale use cases.
Before publication, The reactive aspect was declared to use Proxy to overwrite the previous Object.defineProperty. The main purpose of object.defineProperty is to make up for some defects of object.defineProperty itself, such as the inability to detect the addition or deletion of Object attributes and the inability to listen for array changes.
Vue3 uses a new Proxy to read data and set interception, which not only makes up for the defects of Object. DefineProperty in PREVIOUS Vue2, but also improves performance.
Today, we’ll take a look at it and see how responsive is implemented in Vue3.
Proxy ?
The Proxy object enables you to create a proxy for another object, which can intercept and redefine fundamental operations for that object.
MDN
Proxy-proxy, as the name implies, is to add an intermediate layer before the object to be accessed. In this way, instead of directly accessing the object, the intermediate layer makes a transfer and modifies the target object by manipulating the Proxy object.
For more information on proxies, you can refer to my previous article, a first look at one of the highlights of Vue3.0 – Proxy! I will not repeat it here.
Reactive and effect methods
Reactive core methods in Vue3 are Reactive and Effect. Reactive is responsible for changing data to reactive, and effect is responsible for updating views or calling functions based on data changes, similar to useEffect in React
Its general usage is as follows:
let { reactive, effect } = Vue;
let data = reactive({ name: 'Hello' });
effect(() => {
console.log(data.name)
})
data.name = 'World';
Copy the code
By default, it prints Hello once, and then prints World once after changing the value of data.name.
Let’s look at the implementation of reactive
reactive.js
First, it should be clear that we should export a reactive method that takes a parameter, target, to make target a reactive object, so that the return value is a reactive object.
import {isObject} from ".. /shared/utils"; // Vue3 responsive principle // responsive method, Export function Reactive (target) {return createReactiveObject(target); Function createReactiveObject (target) {// If (! isObject(target) ) return target; Const observed = new Proxy(target,{}) return observed; // Create Proxy const observed = new Proxy(target,{}) return observed; }Copy the code
Reactive is the basic structure of a method that, given an object, returns a reactive object.
The isObject method is used to determine whether it is an object. If it is not an object, it does not need a proxy and can be returned directly.
The reactive method focuses on the second parameter handler of Proxy, which is responsible for monitoring object changes, dependency collection, view updates and other important responsibilities. We will focus on this object.
handler.js
Vue3 Proxy handler sets the properties of get, set, deleteProperty, has, ownKeys, which intercepts the reading, setting, and deleting of objects. And in the Object. GetOwnPropertyNames method and Object. GetOwnPropertySymbols method.
Let’s be lazy here and consider set and get for the moment.
handler.get()
Get is a little bit easier to get, so let’s look at this one first, where we create getHanlder with a method.
Get function createGetter () {return function get (target, key, receiver) { // proxy + reflect const res = Reflect.get(target, key, receiver); // target[key]; If (isObject(res)) return reactive(res); Console. log(' get attribute value: ', 'target: ', target, 'key: ', key) return res; }}Copy the code
Get is recommended instead of target[key].
It can be found that Vue3 recursively traverses attributes during the value, instead of adding Watcher to each attribute with recursive data at the beginning of Vue2, which is also one of the performance improvements of Vue3.
handler.set()
For the set operation, we create the setHandler in the same way.
Set function createSetter () {return function set (target, key, value, Const res = reflect. set(target, key, value, receiver); return res; }}Copy the code
Reflect.set returns a Boolean value that determines whether the property was set successfully.
Export the handler and then introduce it to Reactive.
const get = createGetter(); const set = createSetter(); Export const mutableHandler = {get, set}Copy the code
There seems to be no problem with testing several groups of objects. In fact, there is a pit, which is also related to the array.
let { reactive } = Vue; Let arr = [1,2,3] let proxy = reactive(arr)Copy the code
In the example above, if we select a proxy array and print its key and value in setHandler we get 3, 4, and Length 4:
- The first group is represented by an array index of
3
Add a new one to the position of4
The value of the - The second group represents the array
length
Instead of4
If left untreated, it will trigger twice if the view is updated, which is definitely not allowed, so we need to distinguish between new and modify operations.
In Vue3, a tool method — hasOwnProperty is used to distinguish new or modified operations by determining whether the target has this property.
/ / determine whether itself contains a property function hasOwnProperty (target, key) {return Object. The prototype. The hasOwnProperty. Call (target, key); }Copy the code
Here we modify the createSetter method as follows:
Function createSetter () {return function set (target, key, value, receiver) {return function set (target, key, value, receiver) { Const hasKey = hasOwnProperty(target, key); // Get the original value const oldVal = target[key]; const res = Reflect.set(target, key, value, receiver); // target[key]=value; if ( ! HasKey) {// Add attributes console.log(' add attributes: ', 'key: ', key, 'value: ', value); } else if (hasChanged(value, oldVal)) {console.log(' changed property: ', 'key: ', key, 'value: '); ', value)} return res; }}Copy the code
This way, when we call the push method, only one update will be triggered, very cleverly avoiding meaningless update operations.
effect.js
We need an effect method, which stores the data dependencies associated with it during initialization. When the dependencies change, the function passed by effect is triggered again.
The basic prototype is as follows, the input parameter is a function, and there is an optional parameter options to facilitate the use of later calculation of attributes, etc., temporarily not considered:
// export function effect (fn, Const reactiveEffect = createReactiveEffect(fn, options); const reactiveEffect = createReactiveEffect(fn, options); ReactiveEffect ()}Copy the code
CreateReactiveEffect is used to change fn into a reactive function, monitor data changes, and execute fn, so it is a higher-order function.
let activeEffect; // Current effect const effectStack = []; // create effect function createReactiveEffect (fn, Options) {const reactiveEffect = function () {// Prevent an endless loop if (! effectStack.includes(reactiveEffect) ) { try { effectStack.push(reactiveEffect); ActiveEffect = reactiveEffect; // Run the fn function return fn(); } finally {// Empty effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; } } } return reactiveEffect; }Copy the code
CreateReactiveEffect transforms the original FN into a reactvieEffect and attaches the current effect to the global activeEffect, in order to later correspond to the currently dependent property.
We must construct the dependency property as {prop: [effect,effect]} this structure can ensure that when the dependent attribute changes, the related effect will be triggered successively. Therefore, it is necessary to do the dependency collection of the attribute when the get attribute, and associate the attribute with effect.
Dependent collection —track
When a property of an object is acquired, getHandler is triggered to do the dependency collection of the property again, namely publish and subscribe in Vue2.
In setHandler, do a track(target, key) operation when getting a property.
The data structure of the whole track is roughly like this
/** * The outermost layer is WeakMap, where the key is the target object and the value is a map. * The map contains the target attribute, and the key is each attribute. Val (map) {name: 'Chris} {name: Set(effect,effect), age: Set()}Copy the code
The purpose is to do the corresponding relationship mapping among target, key and effect.
const targetMap = new WeakMap(); Export function tract(target,key){if (activeEffect === undefined){return; Let depsMap = targetmap.get (target); // There is no build if (! depsMap ) { targetMap.set(target, (depsMap = new Map())); } dep = depmap.get (key); // Create if (! dep ) { depsMap.set(key, (dep = new Set())); } // Add if (! dep.has(activeEffect) ) { dep.add(activeEffect); }}Copy the code
The structure of the printed targetMap is as follows:
** Triggers updates — trigger **
Now that dependency collection is complete, all that remains is to monitor data changes and trigger the update operation, which is to add trigger to the setHandler.
Export function trigger (target, type, key) {const depsMap = targetmap.get (target); // Return if (! depsMap ) return; Const effects = new Set(); Const add = (effectsToAdd) => {if (effectsToAdd) {effectStoadd.foreach (effect => { Effects.add (effect)})}} const run = (effect) => {effect && effect()} == null ) { add(depsMap.get(key)); } if (type === 'add') {let effects = depmap.get (array.isarray (target)? 'length' : ''); add(effects); } // Trigger update effects.foreach (run); }Copy the code
In this way, when acquiring data, track is used for dependent collection, and when updating data, trigger is used for updating, thus completing the responsive operation of the whole data.
Let’s go back to our earlier example:
let { effect, reactive } = Vue;
let data = reactive({ name: 'Hello' })
effect(() => {
console.log(data.name, ' ***** effect ***** ');
})
data.name = 'World'
Copy the code
The console will print Hello ***** effect ***** and World ***** effect ***** in sequence, which are triggered by the first rendering and triggered by the update data rerendering respectively. So far, the function is realized!
conclusion
Overall, COMPARED with Vue2, Vue3 has been adjusted in many aspects, and the responsiveness of data is only the tip of the iceberg. However, it can be seen that the Utah team has made very clever use of the characteristics of Proxy and the data structure and method of ES6. In addition, the model of Composition API is similar to React to some extent. This design mode makes it faster in actual development and use, which is worth learning. Come on!
Finally, attached is the address of the warehouse github, welcome your criticism and criticism