1. Introduction
I have always considered vUE’s data-responsive mechanism to be its soul, which is one of the reasons I prefer VUE. On October 5, 2019, UVU released the preview version of Vue3.0 source code, in which the responsive mechanism was also rewritten by the new syntax in ES6, I sort out the implementation principle of Vue3.0 data binding for your reference.
2.Vue2. X data binding mechanism
DefineProperty is used to intercept objects, add set and GET methods to the attributes of objects, collect dependencies in get method, and notify dependency update view in set method. However, this mechanism has certain drawbacks:
- Deep recursive traversal of objects is a waste of memory
Object.defineProperty
You cannot listen for array changes, so you need to manually wrap array method hijacking- Object over
get/set
Method to add a key-value pair directly cannot bind the newly added key-value pair
Here is a brief description of the data binding mechanism for VUe2 2.x:
Object to intercept
function observer(target){
// If it is not an object data type, return it directly
if(! isObject(target)){return target
}
// Redefine key
for(let key in target){
defineReactive(target,key,target[key])
}
}
function isObject(target){
return typeof target === "object"&& target ! = =null;
}
function defineReactive(obj,key,value){
if(isObject(value)){
observer(value); // Values for object types require deep recursive hijacking
}
Object.defineProperty(obj,key,{
get(){
Collect dependencies in the get method
return value
},
set(newVal){
if(newVal ! == value){// Need to continue hijacking for object type
if(isObject(value)){
observer(value);
}
update(); // Trigger an update in the set method}}})}function update(){
console.log('update view')}let obj = {name:'youxuan'}
observer(obj);
obj.name = 'webyouxuan';
Copy the code
Array method hijacking
const oldProtoMehtods = Array.prototype;
const proto = Object.create(oldProtoMehtods);
['push'.'pop'.'shift'.'unshift'. ] .forEach(method= >{
Object.defineProperty(proto,method,{
get(){
update();
oldProtoMehtods[method].call(this. arguments) } }) })function observer(target){
// If it is not an object data type, return it directly
if(typeoftarget ! = ='object') {return target
}
// Add a custom data hijacking method for an array
if(Array.isArray(target)){
Object.setPrototypeOf(target,proto);
// Observr is applied to each item in the array
for(let i = 0; i < target.length; i++){ observer(target[i]) }return
};
// Redefine key
for(let key in target){
defineReactive(target,key,target[key])
}
}
Copy the code
The data binding principle of VUe2.x is not explained here, but we will focus on Vue3.0.
3.Vue3.0 source directory analysis
├── Packages │ ├── Compiler-Core # all Platforms │ ├── Compiler-DOM # For The Browser compiler │ ├─ ReActivity # Data Response System │ ├─ │ ├─ Running-core # Running-Core # Running-Core # ├─ Running-dom # Its capabilities include handling native DOM apis, DOM events, and DOM attributes. │ ├ ─ ─ the runtime - test # written specifically for testing the runtime │ ├ ─ ─ server - the renderer # for SSR │ ├ ─ ─ Shared # help method │ ├ ─ ─ the template - explorer │ └ ─ ─ vue Build vue Runtime + CompilerCopy the code
Compiler compiler-core exposes the compiler API and baseCompile method. Compiler – DOM is based on compiler-core and wraps the compiler for the browser.
Runtime Runtime – Core The virtual DOM renderer, Vue components, and various Vue apis runtime-test format DOM structures into objects, Runtime: createApp; render; createApp
Reactivity is a separate data responsive system with core methods Reactive, effect, REF, computed
Vue integrated Compiler + Runtime
4. Vue3.0 early experience
Vue3.0 directory structure is very clear. For students who want to experience Vue3.0, the official scaffolding has also been released to support vuE-next version vuE-cli-plugin-vue-next. The specific experience method is as follows:
# in an existing Vue CLI project
vue add vue-next
Copy the code
Initialize a project using vue-CLI, then type vue add vue-next on the command line in the project root directory.
Please note: vuE-CLI version must be updated to V4.3.1, and vue-Router and Vuex are not currently supported by VUe3.0. – 20200511.
Here is a simple demo of vue3.0 code:
< the template > < div id = "app" > < div > the mouse coordinates X - {{X}} < / div > < div > mouse Y - {{Y}} < / div > < / div > < / template > < script > / / similar to react Import {ref, onMounted, onUnmounted} from "vue"; Function usePosition() {const x = ref(0); const y = ref(0); function update(e) { x.value = e.pageX; y.value = e.pageY; } onMounted(() => { window.addEventListener("mousemove", update); }); onUnmounted(() => { window.removeEventListener("mousemove", update); }); return { x, y }; } export default { setup() { const { x, y } = usePosition(); Return {x, y}; }}; </script>Copy the code
5. Parse Vue3.0 data binding
Before learning Vue3.0, it is necessary to master Proxy, Reflect and Map and Set data structures in ES6. If you are not familiar with them, it is recommended to master these knowledge first.
Let’s start by looking at how data binding is implemented in Vue3.0
const person = Vue.reactive({name:'cangshudada'}); // The person object has become responsive data
Vue.effect(() = >{ The effect method fires once immediately
console.log(person.name);
})
person.name = Hamster Big;; // The effect method is fired again when the property is modified
Copy the code
Source code is prepared by TS, because there may be unfamiliar with TS students, here we use JS to write from 0 to achieve the principle, then look at the source code will be more relaxed!
5.1 reactive implementation
/ * * * *@description Generate a responsive object *@param {any} target
* @returns* /
function reactive(target) {
// Create a responsive object
return createReactiveObject(target);
}
/ * * * *@description Check whether it is object *@param {any} target
* @returns {boolean}* /
function isObject(target) {
return typeof target === "object"&& target ! = =null;
}
/ * * * *@description Create responsive objects *@param {any} target
* @returns* /
function createReactiveObject(target){
// Check whether target is an object
if(! isObject(target)){return target;
}
// get set delete ... Object methods
const handlers = {
get(target,key,receiver){ / / value
let res = Reflect.get(target,key,receiver);
return res;
},
set(target,key,value,receiver){ // Change/add attributes
let result = Reflect.set(target,key,value,receiver);
return result;
},
deleteProperty(target,key){ // Delete attributes
const result = Reflect.deleteProperty(target,key);
returnresult; }}// Start the proxy
observed = new Proxy(target,handlers);
return observed;
}
let p = reactive({name:'cangshudada'});
console.log(p.name); / / value
p.name = Hamster Big; / / set
delete p.name; / / delete
Copy the code
But such objects may exist
const person ={
name: 'cangshudada'.age: 24.pets: {
dog: {
name: 'guagua'.age: 1
},
cat: {
name: 'gugu'.age: 2}}}Copy the code
So we need to continue to implement the proxy in the case of multi-layer object nesting:
get(target, key, receiver) {
/ / value
const res = Reflect.get(target, key, receiver);
return isObject(res) ? reactive(res) : res; Vue2.0 will recursively add getters and setters all the time
}
Copy the code
Let’s move on to the array case
Proxy can support array by default, so we don’t need like Vue2. X as an array encapsulation and in which to hijack my way to monitor data changes, but when we change the array will still be able to find problem, that is the change of the array will trigger two set, respectively is the length of the array change and the change of the index value, Next we need to mask the problem of multiple triggers.
set(target, key, value, receiver) {
const oldValue = target[key];
const hadKey = target.hasOwnProperty(key);
const result = Reflect.set(target, key, value, receiver);
// Check whether it is new or modified
if(! hadKey) {// If there is no key, it is added
trigger(target, 'add', key)
} else if(oldValue ! == value) {// Prevent set from being triggered more than once when the array is repeatedly manipulating the index or length
trigger(target, 'set', key)
}
return result;
}
Copy the code
At this point, the problem of array is also solved. Finally, the compatibility of repeated proxy for the same object is solved by using WeakMap. In this case, the complete reactive implementation is as follows:
const toProxy = new WeakMap(a);// Store the proxied object
const toRaw = new WeakMap(a);// Store the proxied object
/ * * * *@description Generate a responsive object *@param {any} target
* @returns* /
function reactive(target) {
// Create a responsive object
return createReactiveObject(target);
}
/ * * * *@description Check whether it is object *@param {any} target
* @returns {boolean}* /
function isObject(target) {
return typeof target === "object"&& target ! = =null;
}
/ * * * *@description Determines whether the key * exists in the object@param {object} target
* @param {object} key
* @returns {boolean}* /
function hasOwn(target, key) {
return target.hasOwnProperty(key);
}
/ * * * *@description Create responsive objects *@param {any} target
* @returns* /
function createReactiveObject(target) {
// Whether it is an object
if(! isObject(target)) {return target;
}
// Determine the proxied object
let observed = toProxy.get(target);
if (observed) { // Determine whether the proxy is used
return observed;
}
if (toRaw.has(target)) { // Determine the condition of duplicate proxy, if duplicate proxy
return target;
}
const handlers = {
get(target, key, receiver) {
/ / value
const res = Reflect.get(target, key, receiver);
track(target, 'get', key);// Collect dependencies
return isObject(res) ? reactive(res) : res; Vue2.0 will recursively add getters and setters all the time
},
set(target, key, value, receiver) {
const oldValue = target[key];
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
// Check whether it is new or modified
if(! hadKey) {// If there is no key, it is added
trigger(target, 'add', key) // Trigger dependency update - increment
} else if(oldValue ! == value) {// Prevent set from being triggered more than once when the array is repeatedly manipulating the index or length
trigger(target, 'set', key) // Trigger dependency update - modify
}
return result;
},
deleteProperty(target, key) {
trigger(target, 'delete', key);// Trigger dependency update - delete
const result = Reflect.deleteProperty(target, key);
returnresult; }};// Start the proxy
observed = new Proxy(target, handlers);
toProxy.set(target, observed);
toRaw.set(observed, target); // create a mapping table
return observed;
}
// Object status
const person = reactive({ name: 'cangshudada' });
console.log('person.name >>', person.name); / / to get
person.name = Hamster Big; / / set
delete person.name; / / delete
person.age = 12;// Can delegate to keys added directly to the object
person.age = 24
// Can directly proxy arrays and duplicate proxies
const ary = reactive([1.2.3.4]);
ary.push(5)
const ary1 = reactive(ary); // The repeated proxy returns the previously propped object
Copy the code
By now, the reactive method has been basically implemented, and the next step is to collect dependencies and trigger dependency updates like the logic in Vue2. X, where track is used to collect dependencies and mainly collects effects, and trigger is used to notify effect updates
5.2 effect to realize
Effect, which stands for side effect, is invoked first by default, and then triggered again if the data changes.
const person = Vue.reactive({name:'cangshudada'}); // The person object has become responsive data
Vue.effect(() = >{ The effect method fires once immediately
console.log(person.name);
})
person.name = Hamster Big;; // The effect method is fired again when the property is modified
Copy the code
Let’s implement the effect function first
/ * * * *@description Effect function *@param {function} Fn callback function *@returns* /
function effect(fn) {
const effect = createReactiveEffect(fn); // Create a reactive effect
effect(); // Execute first
return effect;
}
// place the response effect
const activeReactiveEffectStack = [];
/ * * * * *@param {function} Fn callback function *@returns* /
function createReactiveEffect(fn) {
const effect = function () {
// effect
return run(effect, fn);
};
return effect;
}
/ * * * *@param {function} Effect Responsive effect *@param {function} Fn callback function *@returns* /
function run(effect, fn) {
try {
activeReactiveEffectStack.push(effect);
return fn(); // Effect can be stored to the corresponding key property when fn executes
} finally{ activeReactiveEffectStack.pop(effect); }}Copy the code
The get method may fire when fn() is called, which fires the track function called in get above
const targetMap = new WeakMap(a);function track(target,type,key){
// Check for effect
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length-1];
if(effect){
const depsMap = targetMap.get(target);
if(! depsMap){// Add a Map object if there are no dependent array objects
targetMap.set(target,depsMap = new Map());
}
const deps = depsMap.get(target);
if(! deps){// Add Set array if deps does not exist
depsMap.set(key,(deps = new Set()));
}
if(! deps.has(effect)){// Add effect to the dependent array if it is not present in depsdeps.add(effect); }}}Copy the code
Trigger execution is triggered when the property is updated, and effects are found in the corresponding storage set according to the key value
function trigger(target,type,key){
const depsMap = targetMap.get(target);
if(! depsMap){return
}
const deps = depsMap.get(key);
if(deps){
deps.forEach(effect= >{ effect(); }}})Copy the code
At this time, there is still the problem of length. For example, we listen the length of array in effect. At this time, because we set the mechanism that length change does not trigger trigger function in the set function above, we need to add judgment in trigger to accommodate this situation
function trigger(target, type, key) {
const depsMap = targetMap.get(target);
if(! depsMap) {return;
}
const deps = depsMap.get(key);
if (deps) {
deps.forEach(effect= > {
deps();
});
}
// If the current update type is increment, effect using array length should also be executed
if (type === "add") {
const lengthDeps = depsMap.get("length");
if (lengthDeps) {
lengthDeps.forEach(effect= >{ effect(); }); }}}Copy the code
5.3 ref implementation
Ref can also convert the raw data type into responsive data, which requires the.value attribute to get the value
/* * * @description Specifies the different types of data to be processed by reactive
function convert(target) {
return isObject(target) ? reactive(target) : target;
}
function ref(raw) {
raw = convert(raw);
const v = {
_isRef:true.// Identifies the ref type
get value() {
track(v, "get"."");
return raw;
},
set value(newVal) {
raw = newVal;
trigger(v,'set'.' '); }};return v;
}
Copy the code
In this case, if the following situation occurs, each call will have an extra.value, which is very troublesome, so we have to make compatibility for this situation
const name = ref('cangshudada');
const person = reactive({
c_Name: name
});
console.log(person.c_Name.value); // every time you call c.a, you have to add.value
Copy the code
This requires compatibility in the GET function
get(target, key, receiver) {
/ / value
const res = Reflect.get(target, key, receiver);
// the value of a ref cannot be returned directly
if(res._isRef){
return res.value
}
track(target, 'get', key);// Collect dependencies
return isObject(res) ? reactive(res) : res; / / lazy agent
}
Copy the code
5.4 the computed to realize
In previous versions of computed function, the function was triggered only when the value of the monitored variable changed, which is very useful in real projects. Now vue3.0 has rewritten the responsive data mechanism, which also leads to the rewriting of computed. Let’s see how computed is implemented in Vue3.0. First let’s look at usage
const person = reactive({name:'cangshudada'});
const _computed = computed(() = >{
console.log('Computed executes')
return `${person.name} --- xixi`;
})
// If _computed. Value is not computed, the callback function is not executed, unless the listener changes n times and only once
console.log(_computed.value);// Computed implements cangshudada -- xixi
console.log(_computed.value);// cangshudada --- xixi
person.name = Hamster Big;
console.log(_computed.value);// Computed performs hamster greatly - xixi
Copy the code
The computed to realize
function computed(fn){
let dirty = true; // The value is triggered for the first time
const runner = effect(fn,{ // Indicates that this effect is lazy
lazy:true./ / lazy to perform
scheduler:() = >{ // This method is called when the attribute of the dependency changes, instead of re-executing effect, the dirty is not updated if the dependency is not updated, thus not triggering runner(), the caching mechanism
dirty = true; }});let value;
return {
_isRef:true.get value() {if(dirty){
value = runner(); // The runner continues to collect dependencies
dirty = false;
}
return value; // No computed callback is performed if value changes}}}Copy the code
Modifying the effect function You are advised to view it in 5.2 Effect
function effect(fn,options) {
let effect = createReactiveEffect(fn,options);
if(! options.lazy){// If it is lazy, it is not executed immediately
effect();
}
return effect;
}
function createReactiveEffect(fn,options) {
const effect = function() {
return run(effect, fn);
};
effect.scheduler = options.scheduler;
return effect;
}
Copy the code
On the trigger
deps.forEach(effect= > {
if(effect.scheduler){ // Effect does not need to be executed if there is a scheduler
effect.scheduler(); // Set dirty to true so that the runner method can be re-executed the next time the value changes
}else{
effect(); // Otherwise, execute effect normally}});Copy the code
const person = reactive({name:'cangshudada'});
const _computed = computed(() = >{
console.log('Computed executes')
return `${person.name} --- xixi`;
})
// If _computed. Value is not computed, the callback function is not executed, unless the listener changes n times and only once
console.log(_computed.value);
person.name = Hamster Big; // Changing the value does not trigger recalculation, but does change dirty to true
console.log(_computed.value); // The get function is triggered and runner() is called to re-call the calculation method
Copy the code
6. Summary
At this point we will Vue3.0 source code reactivity part of the parsing is complete! Understand the vUE data binding mechanism for after the interview or later application has a great help, of course, this article is just a brief analysis of this part, clear data binding this part of the logic and thought to read the source code of this part I believe you will have more harvest. This article was actually published earlier and was not released to the community at that time. Today, I sorted it out and re-published it. If you are interested, I will also analyze and interpret other modules. Of course, if you have different understanding or opinion of this article, welcome to comment on the exchange, learning and progress together ~