premise
There are many ways to achieve two-way binding. Vue adopts data holding combined with publish and subscribe design mode to achieve.
Use Object.defineProperty() to hijack the setter and getter of each property, publish a message to subscribers when the data changes, and trigger the corresponding listener callback.
Object.defineProperty() introduction
Function: A more popular way of saying is to manipulate object properties
Official: Define a new property directly on an object, or modify an existing property of an object, and return this object.
This method has three parameters:
Object.defineProperty(obj, prop, descriptor)
obj The object whose properties are to be defined.
prop The name or Symbol of the property to be defined or modified.
descriptor The attribute descriptor to be defined or modified.
Has the following attributes:
-
configurable: Can the descriptor be modified, that is, can the other attributes of the descriptor be modified again
-
enumerable: can the attribute be enumerated, that is, can the a attribute be for
-
writable: Can the attribute value be modified, that is, can the obj.a = 1 be modified like this
-
value: the value of the attribute
-
get: It is a function, when the attribute is accessed, the function is automatically called, and the return value of the function is the value of the attribute
-
set: is a function, when the attribute is modified, the function is automatically called, the function has one and only one parameter, the new value assigned
*The writable attribute of the value attribute in the descriptor and the set attribute of the get attribute are mutually exclusive.
Attribute holding
Vue's data data holding is achieved by using get and set attributes. The get function is triggered when the object property is read, and the set function is triggered when the value is assigned
* There cannot be a read operation in get, otherwise the loop will be endless, so when using get set, a variable is always needed
example:
let obj = {};
let name = "xx" ;
Object .defineProperty(obj, "n" , {
get () {
console .log( "Read" , name);
return name;
},
set ( newName ) {
console .log( "Settings" , newName);
value = newName;
}
});
//Trigger the get function, the return value of get is the property value
console .log(obj.n);
//Trigger the set function, the value of value becomes xxxx, the property value has not changed
obj.n = "xxxx" ;
console .log(obj.n);
Copy code
Object.defineProperty() extension
Back in Vue development, we often encounter a scenario where two-way binding cannot be performed to modify the value of an array. The reason is that the get set of defineProperty() cannot monitor the new modification operation of the object array.
Vue monitors the changes of the array by finding ways to change the original array, and then hijacking these methods.
The general steps are as follows: Array => new prototype (to perform detection behavior) => prototype of the array
There is a scenario, such as an array inside an array, then recursive thinking is used to solve this problem
This is why there is in vue delete and can only be detected by using specific methods for arrays
The overall idea of two-way binding
1. Implement a data listener, the main function is to listen to all attributes, called Observer in Vue
2. Then through the subscription-release design ideas to notify the update
(The subscription publishing model defines a one-to-many dependency relationship. One refers to the publisher, such as a topic object, and multiple refers to subscribers. Dependency is the dependence of the subscriber on the publisher; multiple subscribers monitor a topic object at the same time . When the status of the publisher, the topic object, changes, the subscribers will be notified of the change, and the subscribers will update their status accordingly.)
3. Define a subscriber Dep to collect changes in these properties to notify subscribers
4. What kind of processing needs to be done for different events, so a ComPile (instruction parser) is also needed, such as pull-down operations, input operations, etc.
5. Finally implement a Watcher (subscriber), mainly to receive different operations, for those objects, update the view
Implement Observer (listener)
function observe ( data ) {
if (data && typeof data === "object" ){
Object .keys(data).forEach( ( key )=> {
defineReactive(data, key, data[key]);
})
}
}
function defineReactive ( data,key,val ) {
//Recursively, monitor sub-objects
observe(val)
Object .defineProperty(data, key, {
enumerable : true ,
configurable : false ,
get : function () {
return val;
},
set : function ( value ) {
val = value;
},
});
}
Copy code
Implement the subscriber
//Dep
function Dep () {
this .subs = [];
}
Dep.prototype = {
addSub : function ( sub ) {
this .subs.push(sub);
},
notify : function () {
this .subs.forEach( function ( sub ) {
sub.update();
});
},
};
Copy code
Stuffed into the listener
function defineReactive ( data,key,val ) {
var dep = new Dep()
//Recursively, listen to sub-objects
observe(val)
Object .defineProperty(data, key, {
enumerable : true ,
configurable : false ,
get : function () {
return val;
},
set : function ( value ) {
dep.notify()
},
});
}
Copy code
Implement watcher (subscriber)
Ideas: 1. Insert the listener when instantiating 2. Implement the update() method 3. When the notification is reached, call the update() method to trigger the callback of the compiler (comPlie) 4. End
//Watcher
function Watcher ( vm, exp, cb ) {
this .cb = cb;
this .$vm = vm;
this .exp = exp;
//In order to trigger the getter of the property, add yourself to the dep, combined with Observer Easier to understand
this .value = this .get(); //add yourself to the subscriber operation
}
Watcher.prototype = {
update : function () {
this .run(); //Receive notification of property value changes
},
run : function () {
var value = this .get(); //
Get the latest value var oldVal = this .value; //Speaking of triggering the set function before, the attribute value has not changed
if (value !== oldVal) {
this .value = value;
this .cb.call( this .$vm, value, oldVal); //Execute the callback bound in Compile and update the view
}
},
get : function () {
Dep.target = this ; //Point the current subscriber to yourself, cache
var value = this .$vm[ this .exp]; //Force the listener to trigger and add yourself to the attribute subscriber
Dep.target = null ; //After adding, reset and release
return value;
},
};
Copy code
Then the corresponding get in the defineReactive method should also be modified
function defineReactive ( data, key, val ) {
var dep = new Dep()
observe(val); //monitor sub-property
Object .defineProperty(data, key, {
....
get : function () {
//Because you need to add a watcher in the closure, you can define a global target attribute in Dep, temporarily store the watcher, and remove it after adding
Dep.target && dep.addDep(Dep.target);
return val;
},
....
});
}
Copy code
Simple integration
<div id= "name" ></div>
< script > function Vue ( data, el, exp ) {
this .data = data;
observe(data);
el.innerHTML = this .data[exp]; //initialize the value of the template data
new Watcher( this , exp, function ( value ) {
el.innerHTML = value;
});
return this ;
}
var ele = document .querySelector( "#name" );
var vue = new Vue(
{
name : "hello world" ,
},
ele,
"name"
);
setInterval ( function () {
vue.data.name = "chuchur" + new Date () * 1 ;
}, 1000 );
</Script >
copy the code
But in the process of our use, this.xxx is used to operate, not this.data.xxx, so we also need to add a proxy, Vm proxy vm.data
function Vue ( options ) {
this .$options = options || {};
this .data = this .$options.data;
//Property proxy, implement vm.xxx -> vm.data.xxx
var self = this ;
Object .keys( this .data ).forEach( function ( key ) {
//this.data ==>this
self.proxy(key); //bind proxy properties
});
observe( this .data, this );
el.innerHTML = this .data[exp]; //Initialize the value of the template data
new Watcher( this , exp, function ( value ) {
el.innerHTML = value;
});
return this ;
}
//Also use the defineProperty() method
Vue.prototype = {
proxy : function ( key ) {
var self = this ;
Object .defineProperty( this , key, {
enumerable : false ,
configurable : true ,
get : function proxyGetter () {
return self.data[key];
},
set : function proxySetter ( newVal ) {
self.data[key] = newVal;
}
});
}
}
Copy code
Finally, the implementation of the parser Compile
The idea is as follows:
- Parse the template instructions, replace the template data, and initialize the view
- Bind the node corresponding to the template instruction to the corresponding update function, and initialize the corresponding subscriber
Compile.prototype = {
......
isDirective : function ( attr ) {
return attr.indexOf( 'v-' ) == 0 ;
},
isEventDirective : function ( dir ) {
return dir.indexOf( 'on:' ) === 0 ;
},
//Process v-instruction
compile : function ( node ) {
var nodeAttrs = node.attributes,
self = this ;
[].slice.call(nodeAttrs).forEach( function ( attr ) {
//Rule: the command is named v-xxx
//such as the
command in <span v-text="content"></span> is v-text var attrName = attr.name; //v-text
if (self.isDirective(attrName)) {
var exp = attr.value; //content
var dir = attrName.substring( 2 ); //text
if (self.isEventDirective (dir)) {
//Event instruction, such as v-on:click
self.compileEvent(node, self.$vm, exp, dir);
} else {
//Ordinary commands such as: v-model, v-html, currently only deal with v-model
self.compileModel(node, self.$vm, exp, dir);
}
//After processing, kill v-on:, v-model and other element attributes
node.removeAttribute(attrName)
}
});
},
compileEvent : function ( node, vm, exp, dir ) {
var eventType = dir.split( ':' )[ 1 ];
var cb = vm.$options.methods && vm.$options.methods[exp];
if (eventType && cb) {
node.addEventListener(eventType, cb.bind(vm), false );
}
},
compileModel : function ( node, vm, exp, dir ) {
var self = this ;
var val = this .$vm[exp];
this .updaterModel(node, val);
new Watcher( this .$vm, exp, function ( value ) {
self.updaterModel(node, value);
});
node.addEventListener( 'input' , function ( e ) {
var newValue = e.target.value;
if (val === newValue) {
return ;
}
self.$vm[exp] = newValue;
val = newValue;
});
},
updaterModel : function ( node, value, oldValue ) {
node.value = typeof value == 'undefined' ? '' : value;
},
}
Copy code
At last
If this article can give you a little help, I hope xdm can give a thumbs up