Application of Publish-Subscribe Mode to the Principle of Bidirectional Binding of Vue Data

Application of Publish-Subscribe Mode to the Principle of Bidirectional Binding of Vue Data

Preface

This chapter mainly talks about the application of Publish-Subscribe mode on the principle of two-way binding of Vue data, which takes about 10 minutes.

I. Vue and MVVM

Vue is a framework of MVVM patterns, namely Model-View-ViewModel

  1. Model is data
  2. View is a view
  3. ViewModel is a bridge between View and Model. It monitors changes in View (Model) and notifies Model updates, as shown in the following figure:


Binding data in two directions means that the enemy will not move and I will follow when the enemy moves. That is to say, when the user updates the View view, the Model data will be modified, and when we modify the Model data, the View view will be modified as well. Its principle is to use publish-subscribe mode + data hijacking. Let's first talk about what data hijacking is.

Data hijacking

Data hijacking, as its name implies, is the operation of data, hijacking this operation incidentally do what we want to do.
Prior to Vue3.0, data hijacking was achieved by using Object.defineProperty to hijack setter and getter operations of object attributes. Getter is to get an attribute, and setter is to set an attribute. Look at the following code example:

var people = {
   name:'pjj'
 }
 Object.keys(people).forEach(function(key) {
   Object.defineProperty(people, key, {
     get:function(){
         console.log('Hijacking triggers this when acquiring attributes console.log');
     },
     set:function(){
         console.log('Hijacking triggers this when setting properties console.log');
     }
   })
 })
 people.name; //The console prints out "Hijack triggers this console.log when attributes are acquired"
 people.name = 'panjj'; //The console prints "Hijack triggers this console.log when setting properties"

In Vue 3.0, data hijacking was implemented by Proxy instead.
A big reason for using Proxy instead of Object.defindProprety is that the original data hijacking, if the attribute value is complex type of data, requires deep traversal, can not be directly monitored. Proxy listens directly to the whole object. It's much simpler, but it's ES6 grammar, so compatibility is not very good.

let people = {
	name: 'pjj'
}
let handler = {
	get: function(target, key) {
		console.log('Hijacking triggers this when acquiring attributes console.log');
        return key in target ? target[key] : 'newKey';
    },
	set: function(target, key, newVal) {
		let res = Reflect.set(target, key, newVal);
		console.log('Hijacking triggers this when setting properties console.log');
		return target[key] = newVal;
	}
}
let p = new Proxy(obj, handler);
//Target refers to any target object to be wrapped by Proxy
//handler is an object whose properties are functions of execution behavior.
p.name = 'panjj';	//The console prints out "Hijack triggers this console.log when attributes are acquired"
console.log(p.age); //The console prints "Hijack triggers this console.log when setting properties"

From the above two pieces of code, we can see that data hijacking is actually hijacking the get and set of data attributes, so that the attributes of data make additional operations when they are acquired or set.

Based on this, as long as we use this extra operation to monitor data and update views, we can actually update data - > views.
In fact, updating the view - > data can be achieved by listening events, such as input events.

3. Implementation of Bidirectional Data Binding


Let's sort it out first.

  1. Implement an Observer for hijacking, converting attributes into accessor attributes getter and setter for data listening.
  2. To implement a Watcher, Watcher is the observer of the data, observes the update of the data, and executes the corresponding update function to update the view.
  3. Implement a Dep to connect Observer and Watcher. Each observer creates a Dep instance that allows each watcher to listen for data at get and executes the update callback function in the watcher at set.
  4. Implementing a parser Compile can scan and parse the relevant instructions of each node (v-model, v-on and so on). If the node has v-model, v-on and other instructions, the parser Compile initializes the template data of such nodes so that it can be displayed on the view, and then initializes the corresponding subscriber (Watcher).

Observer and Dep

//Used to hijack and monitor all attributes and notify subscribers of any changes.

function Observer(data) {
	this.data = data;
	this.walk(data); // Traversing through each attribute of data, data hijacking
}

Observer.prototype = {
	walk: function(data) {
		var self = this;
		//Here, by traversing an object, all attributes of the object are monitored.
		Object.keys(data).forEach(function(key) {
			self.defineReactive(data, key, data[key]);
		});
	},
	defineReactive: function(data, key, val) {
		//Data hijacking
		var dep = new Dep();
		// Recursive traversal of all subattributes
		Object.defineProperty(data, key, {
			get: function getter() {
				if (Dep.target) {
					// Add a subscriber here, Dep.target????
					dep.addSub(Dep.target);
				}
				return val;
			},
			// Setter, if the value of an object attribute changes, it triggers dep.notify() in setter, notifies watcher (subscriber) of data changes, and executes an update function for the corresponding subscriber to update the view.
			set: function setter(newVal) {
				if (newVal === val) {
					return;
				}
				val = newVal;
				// If the new value is object, listen
				childObj = observe(newVal);
				dep.notify();
			}
		});
	}
};

// Returns the instantiated object
function observe(value, vm) {
	if (!value || typeof value !== 'object') {
		return;
	}
	return new Observer(value);
}

// Message Subscriber Dep, which collects subscribers and then performs update functions for the corresponding subscribers when the attributes change
function Dep() {
	this.subs = []; //Store Subscriber's subs
}
Dep.prototype = {
	// Add a subscription
	addSub: function(sub) {
		this.subs.push(sub);
	},
	// Notify Subscribers of Data Changes
	notify: function() {
		this.subs.forEach(function(sub) {
			sub.update();
		});
	}
};
Dep.target = null;

Watcher

//You can update the view by receiving notifications of changes in attributes and performing corresponding functions.

function Watcher(vm, exp, cb) {
	this.vm = vm; // vm is a new Vue object
	this.exp = exp; // Exp is a binding property such as v-model or v-on, for example v-modle='name', exp is name.
	this.cb = cb; // cb is the update function of Watcher binding
	this.value = this.get(); // Value here is the value of the following 22 lines, so that it's easy to compare the old and new values when updating.
}

Watcher.prototype = {
	update: function() {
		var value = this.vm.data[this.exp]; // New value
		var oldVal = this.value; // Original value
		if (value !== oldVal) {
			// Unequal updates
			this.value = value;
			this.cb.call(this.vm, value, oldVal); // call calls the update function cb for updating
		}
	},
	run: function() {
		var value = this.vm.data[this.exp];
		var oldVal = this.value;
		if (value !== oldVal) {
			this.value = value;
			this.cb.call(this.vm, value);
		}
	},
	get: function() {
		Dep.target = this; // Here let Dep.terget point to himself (a watcher)
		var value = this.vm.data[this.exp]; // Here, this.vm.data[this.exp] calls the name of the data in the example above, triggering the get function in object.dedefineProperty to add the watcher to the subscriber
		Dep.target = null; // Release oneself
		return value;
	}
};

Compile

function Compile(el, vm) {
	this.vm = vm;
	this.el = document.querySelector(el); // The root element of the binding
	this.fragment = null;
	this.init();
}

Compile.prototype = {
	init: function() {
		if (this.el) {
			this.fragment = this.nodeToFragment(this.el);
			this.compileElement(this.fragment);
			this.el.appendChild(this.fragment);
		} else {
			console.log('Dom Element does not exist');
		}
	},
	// Add the bound dom element as a whole to the fragment element
	nodeToFragment: function(el) {
		var fragment = document.createDocumentFragment();
		var child = el.firstChild;
		while (child) {
			// Move Dom elements into fragment s
			fragment.appendChild(child);
			child = el.firstChild;
		}
		return fragment;
	},
	// Analytical element
	compileElement: function(el) {
		var childNodes = el.childNodes;
		var self = this;
		[].slice.call(childNodes).forEach(function(node) {
			var reg = /\{\{(.*)\}\}/; // Get data in beard grammar {} by regularization
			var text = node.textContent;
			// If it is an element node
			if (self.isElementNode(node)) {
				self.compile(node);
				// If it is a text node
			} else if (self.isTextNode(node) && reg.test(text)) {
				//The zero element is the text reg.exec(text)[0] that matches the regular expression, which is'{data}'.
				//The first element is the text reg.exec(text)[1] that matches the first subexpression of RegExpObject as'data'
				self.compileText(node, reg.exec(text)[1]);
			}
			if (node.childNodes && node.childNodes.length) {
				self.compileElement(node);
			}
		});
	},
	// If it is an instruction
	compile: function(node) {
		var nodeAttrs = node.attributes;
		var self = this;
		Array.prototype.forEach.call(nodeAttrs, function(attr) {
			var attrName = attr.name;
			if (self.isDirective(attrName)) {
				var exp = attr.value;
				var dir = attrName.substring(2);
				if (self.isEventDirective(dir)) {
					// Event directives
					self.compileEvent(node, self.vm, exp, dir); //Binding listening events
				} else {
					// v-model instruction
					self.compileModel(node, self.vm, exp, dir);
				}
				node.removeAttribute(attrName);
			}
		});
	},
	// If {}
	compileText: function(node, exp) {
		var self = this;
		var initText = this.vm[exp];
		this.updateText(node, initText);
		new Watcher(this.vm, exp, function(value) {
			self.updateText(node, value);
		});
	},
	//Binding listening events
	compileEvent: function(node, vm, exp, dir) {
		var eventType = dir.split(':')[1];
		var cb = vm.methods && vm.methods[exp];

		if (eventType && cb) {
			node.addEventListener(eventType, cb.bind(vm), false);
		}
	},
	compileModel: function(node, vm, exp) {
		var self = this;
		var val = this.vm[exp];
		this.modelUpdater(node, val); // After mounting, the value in {{}} is rendered to the value in data
		new Watcher(this.vm, exp, function(value) {
			self.modelUpdater(node, value);
		});

		node.addEventListener('input', function(e) {
			var newValue = e.target.value;
			if (val === newValue) {
				return;
			}
			self.vm[exp] = newValue;
			val = newValue;
		});
	},
	updateText: function(node, value) {
		node.textContent = typeof value == 'undefined' ? '' : value;
	},
	modelUpdater: function(node, value) {
		node.value = typeof value == 'undefined' ? '' : value;
	},
	isDirective: function(attr) {
		return attr.indexOf('v-') == 0;
	},
	isEventDirective: function(dir) {
		return dir.indexOf('on:') === 0;
	},
	isElementNode: function(node) {
		return node.nodeType == 1;
	},
	isTextNode: function(node) {
		return node.nodeType == 3;
	}
};

Tags: Fragment Vue Attribute

Posted on Thu, 29 Aug 2019 02:54:40 -0700 by Sobbs