Reproduced at https://segmentfault.com/a/1190000014274840
1, principle
The principle of Vue's two-way data binding is well understood, mainly through the definition property property property of the Object object, rewriting the set and get functions of data. Here, the principle is not described too much, but mainly to implement an example. In order to make the code clearer, only the most basic content can be realized here. The main three commands are v-model, v-bind and v-click. Other commands can be supplemented by themselves.
Add a picture on the Internet
2, implementation
The page structure is simple, as follows
<div id="app">
<form>
<input type="text" v-model="number">
<button type="button" v-click="increment">increase</button>
</form>
<h3 v-bind="number"></h3>
</div>
Contain:
1. An input, using the v-model instruction 2. A button, using the v-click instruction 3. An h3, using the v-bind instruction.
We'll end up using our two-way data binding in a vue-like way, adding annotations to our data structure
var app = new myVue({
el:'#app',
data: {
number: 0
},
methods: {
increment: function() {
this.number ++;
},
}
})
First we need to define a myVue constructor:
function myVue(options) {
}
To initialize the constructor, add a _init attribute to it
function myVue(options) {
this._init(options);
}
myVue.prototype._init = function (options) {
this.$options = options; // options are the structures passed in when used above, including el,data,methods
this.$el = document.querySelector(options.el); // El is # app, and this. $el is the Element element with id app.
this.$data = options.data; // this.$data = {number: 0}
this.$methods = options.methods; // this.$methods = {increment: function(){}}
}
Next, we implement the _obverse function, process the data, and rewrite the set and get functions of the data.
And transform the _init function
myVue.prototype._obverse = function (obj) { // obj = {number: 0}
var value;
for (key in obj) { //Traversing obj objects
if (obj.hasOwnProperty(key)) {
value = obj[key];
if (typeof value === 'object') { //If the value is still an object, traversal processing
this._obverse(value);
}
Object.defineProperty(this.$data, key, { //crux
enumerable: true,
configurable: true,
get: function () {
console.log(`Obtain${value}`);
return value;
},
set: function (newVal) {
console.log(`To update${newVal}`);
if (value !== newVal) {
value = newVal;
}
}
})
}
}
}
myVue.prototype._init = function (options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this._obverse(this.$data);
}
Next, we write an instruction class, Watcher, to bind update functions to update DOM elements.
function Watcher(name, el, vm, exp, attr) {
this.name = name; //Instruction names, such as text nodes, are set to "text"
this.el = el; //DOM elements corresponding to instructions
this.vm = vm; //The myVue instance to which the instruction belongs
this.exp = exp; //The corresponding value of an instruction, such as "number"
this.attr = attr; //The bound attribute value, in this case "innerHTML"
this.update();
}
Watcher.prototype.update = function () {
this.el[this.attr] = this.vm.$data[this.exp]; //For example, H3.innerHTML = this.data.number; when the number changes, the update function is triggered to ensure that the corresponding DOM content is updated.
}
Update _init function and _obverse function
myVue.prototype._init = function (options) {
//...
this._binding = {}; //_ binding preserves the mapping relationship between model and view, which is an example of Watcher we defined earlier. When the model changes, we trigger instruction class updates to ensure that the view can also be updated in real time.
//...
}
myVue.prototype._obverse = function (obj) {
//...
if (obj.hasOwnProperty(key)) {
this._binding[key] = { // According to the previous data, _binding = {number: _directives: []}
_directives: []
};
//...
var binding = this._binding[key];
Object.defineProperty(this.$data, key, {
//...
set: function (newVal) {
console.log(`To update${newVal}`);
if (value !== newVal) {
value = newVal;
binding._directives.forEach(function (item) { // When the number changes, the update of the bound Watcher class in _binding[number]._directives is triggered
item.update();
})
}
}
})
}
}
}
So how do you bind view to model? Next, we define a _compile function to parse our instructions (v-bind,v-model,v-clickde), and bind view to model in the process.
myVue.prototype._init = function (options) {
//...
this._complie(this.$el);
}
myVue.prototype._complie = function (root) { root by id by app Of Element Element, our root element
var _this = this;
var nodes = root.children;
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.children.length) { // All elements are traversed and processed
this._complie(node);
}
if (node.hasAttribute('v-click')) { // If there is a v-click attribute, we listen for its onclick event and trigger the increment event, number+.
node.onclick = (function () {
var attrVal = nodes[i].getAttribute('v-click');
return _this.$methods[attrVal].bind(_this.$data); //bind is to keep the scope of data consistent with that of method functions
})();
}
if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { // If there is a v-model attribute and the element is INPUT or TEXTAREA, we listen for its input event
node.addEventListener('input', (function(key) {
var attrVal = node.getAttribute('v-model');
//_ this._binding['number']._directives = a Watcher instance]
// Where Watcher.prototype.update = function (){
// Node ['vaule']= _this. $data ['number']; this keeps the value of node consistent with number
// }
_this._binding[attrVal]._directives.push(new Watcher(
'input',
node,
_this,
attrVal,
'value'
))
return function() {
_this.$data[attrVal] = nodes[key].value; // The value of number is consistent with the value of node, and bidirectional binding has been implemented.
}
})(i));
}
if (node.hasAttribute('v-bind')) { // If there is a v-bind attribute, we just need to update the value of node to the value of number in data in time.
var attrVal = node.getAttribute('v-bind');
_this._binding[attrVal]._directives.push(new Watcher(
'text',
node,
_this,
attrVal,
'innerHTML'
))
}
}
}
So far, we have implemented a simple two-way binding function of vue, including three instructions: v-bind, V-model and v-click. The effect is as follows
Attach all the code, less than 150 lines
<!DOCTYPE html>
<head>
<title>myVue</title>
</head>
<style>
#app {
text-align: center;
}
</style>
<body>
<div id="app">
<form>
<input type="text" v-model="number">
<button type="button" v-click="increment">increase</button>
</form>
<h3 v-bind="number"></h3>
</div>
</body>
<script>
function myVue(options) {
this._init(options);
}
myVue.prototype._init = function (options) {
this.$options = options;
this.$el = document.querySelector(options.el);
this.$data = options.data;
this.$methods = options.methods;
this._binding = {};
this._obverse(this.$data);
this._complie(this.$el);
}
myVue.prototype._obverse = function (obj) {
var value;
for (key in obj) {
if (obj.hasOwnProperty(key)) {
this._binding[key] = {
_directives: []
};
value = obj[key];
if (typeof value === 'object') {
this._obverse(value);
}
var binding = this._binding[key];
Object.defineProperty(this.$data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log(`Obtain${value}`);
return value;
},
set: function (newVal) {
console.log(`To update${newVal}`);
if (value !== newVal) {
value = newVal;
binding._directives.forEach(function (item) {
item.update();
})
}
}
})
}
}
}
myVue.prototype._complie = function (root) {
var _this = this;
var nodes = root.children;
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.children.length) {
this._complie(node);
}
if (node.hasAttribute('v-click')) {
node.onclick = (function () {
var attrVal = nodes[i].getAttribute('v-click');
return _this.$methods[attrVal].bind(_this.$data);
})();
}
if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) {
node.addEventListener('input', (function(key) {
var attrVal = node.getAttribute('v-model');
_this._binding[attrVal]._directives.push(new Watcher(
'input',
node,
_this,
attrVal,
'value'
))
return function() {
_this.$data[attrVal] = nodes[key].value;
}
})(i));
}
if (node.hasAttribute('v-bind')) {
var attrVal = node.getAttribute('v-bind');
_this._binding[attrVal]._directives.push(new Watcher(
'text',
node,
_this,
attrVal,
'innerHTML'
))
}
}
}
function Watcher(name, el, vm, exp, attr) {
this.name = name; //Instruction names, such as text nodes, are set to "text"
this.el = el; //DOM elements corresponding to instructions
this.vm = vm; //The myVue instance to which the instruction belongs
this.exp = exp; //The corresponding value of an instruction, such as "number"
this.attr = attr; //The bound attribute value, in this case "innerHTML"
this.update();
}
Watcher.prototype.update = function () {
this.el[this.attr] = this.vm.$data[this.exp];
}
window.onload = function() {
var app = new myVue({
el:'#app',
data: {
number: 0
},
methods: {
increment: function() {
this.number ++;
},
}
})
}
</script>