preface
In the last article, we talked about Proxy in ES6. Now let’s use Proxy to implement a two-way binding that simulates VUE step by step.
directory
- 1. How
- 2. Implement the observer
- 3. Dynamically update data to the front end
- 4. Implement bidirectional data binding
How to implement
- When learning vUE, VUE hijacks data changes and changes the front end view when listening to data changes.
- To implement bidirectional binding, you must have a way to listen for data. As the title of the article shows, used here
proxy
Implement data listening. - When data changes are heard, a watcher needs to respond and call the compile method that updates the data to update the front view.
- In the vue
v-model
As the entry to the binding. When we listen to the front-end input and bind the data item, we need to tell the watcher first, and the watcher will change the listener’s data. - The general principle of bidirectional binding is:
1. Implement an observer
It’s easy to implement a data listener using a proxy, because the proxy is listening for changes to the entire object, so we can write:
class VM {
constructor(options, elementId) {
this.data = options.data || {}; // The data object to listen to
this.el = document.querySelector(elementId);
this.init(); / / initialization
}
/ / initialization
init() {
this.observer();
}
// Listen for data changes
observer() {
const handler = {
get: (target, propkey) = > {
console.log(` listening to${propkey}Is taken, the value is:${target[propkey]}`);
return target[propkey];
},
set: (target, propkey, value) = > {
if(target[propkey] ! == value){console.log(` listening to${propkey}Changed, the value becomes:${value}`);
}
return true; }};this.data = new Proxy(this.data, handler); }}// Test
const vm = new VM({
data: {
name: 'defaultName'.test: 'defaultTest',}},'#app');
vm.data.name = 'changeName'; // Listen to name change, value changed to :changeName
vm.data.test = 'changeTest'; // Listen to test change, value changed to :changeTest
vm.data.name; // Listen to name is taken, the value is changeName
vm.data.test; // Listen to test taken, value :changeTest
Copy the code
In this way, the data listener is basically implemented, but now it can only listen to changes in the data, not change the front view information. Now you need to implement a way to change the front-end information by adding the method changeElementData to the VM class
// Change the front-end data
changeElementData(value) {
this.el.innerHTML = value;
}
Copy the code
ChangeElementData is called to change the front end data when data changes are heard, and methods are called in the handler’s set method
set(target, propkey, value) {
this.changeElementData(value);
return true;
}
Copy the code
Set a timer in init to change the data
init() {
this.observer();
setTimeout(() = > {
console.log('change data !! ');
this.data.name = 'hello world';
}, 1000)}Copy the code
You can already see the monitored information changing to the front end, but!
Writing dead binding data in this way obviously makes no sense, and the logic now implemented is roughly like the figure below
2. Dynamically update data to the front end
A simple data binding demonstration is implemented above, but only a specified node can be bound to change the data binding of that node. This is obviously not enough. We know that vue is{{key}}
This form binds the displayed data, and the vUE listens on all children of the specified node. So the object needs to be inVIEWandOBSERVERAdd a listening layer betweenWATCHER. When data changes are detected, passWATCHERTo changeVIEWAs shown in figure:According to this process, the next steps we need to do are:
- Listen for all nodes of the entire bound Element and match all of the nodes
{{text}}
The template - When listening for changes, tell Watcher that the data has changed and replace the front end template
Add three parameters to the CONSTRUCTOR of the VM class
constructor() {
this.fragment = null; // Document fragment
this.matchModuleReg = new RegExp('\{\{\s*.*? \s*\}\}'.'gi'); // Match all {{}} templates
this.nodeArr = []; // All front-end nodes with templates
}
Copy the code
Create a method that traverses all the nodes in the EL and places it in the fragment
/** create a document fragment */
createDocumentFragment() {
let documentFragment = document.createDocumentFragment();
let child = this.el.firstChild;
// Loop to add nodes to the document fragment
while (child) {
documentFragment.appendChild(child);
child = this.el.firstChild;
}
this.fragment = documentFragment;
}
Copy the code
Match the data for {{}} and replace the template
/** * Match template * @param {string} key trigger key * @param {documentElement} fragment node */ matchElementModule(key, fragment) { const childNodes = fragment || this.fragment.childNodes; [].slice.call(childNodes).forEach((node) => { if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) { node.defaultContent = node.textContent; // Save the initialized front-end content to the defaultContent of the node this.changeData(node); this.nodeArr.push(node); If (node.childnodes && Node.childnodes. Length) {this.matchelementModule (key, node.childNodes); } /** * Change view data * @param {documentElement} node */ changeData(node) {const matchArr = node.defaultContent.match(this.matchModuleReg); // Get all templates to match let tmpStr = node.defaultContent; for(const key of matchArr) { tmpStr = tmpStr.replace(key, this.data[key.replace(/\{\{|\}\}|\s*/g, '')] || ''); } node.textContent = tmpStr; }Copy the code
To implement watcher, data changes are triggered by this Watcher update front end
watcher(key) {
for(const node of this.nodeArr) {
this.changeData(node); }}Copy the code
Execute the new method in the init and proxy’s set methods
init() {
this.observer();
this.createDocumentFragment(); // Put the bound nodes into the document fragment
for (const key of Object.keys(this.data)) {
this.matchElementModule(key);
}
this.el.appendChild(this.fragment); // Output the initialized data to the front end
}
set: () = > {
if(target[propkey] ! == value) { target[propkey] = value;this.watcher(propkey);
}
return true;
}
Copy the code
Test it out:
3. Implement bidirectional data binding
Now that our program can dynamically change the presentation of the front end by changing the data, we need to implement a method similar to the vuev-Model binding input, which dynamically outputs the input information to the corresponding front end template. The general flow chart is as follows:
A simple implementation process is as follows:
- Gets all input nodes with v-Model
- Listen for input information and set to the corresponding data
Add in Constructor
constructor() {
this.modelObj = {};
}
Copy the code
New method in VM class
/ / bind y - model
bindModelData(key, node) {
if (this.data[key]) {
node.addEventListener('input'.(e) = > {
this.data[key] = e.target.value;
}, false); }}// Set the y-model value
setModelData(key, node) {
node.value = this.data[key];
}
// Check the y-model properties
checkAttribute(node) {
return node.getAttribute('y-model');
}
Copy the code
The setModelData method is executed in watcher, and the bindModelData method is executed in matchElementModule. The modified matchElementModule and watcher methods are as follows
matchElementModule(key, fragment) {
const childNodes = fragment || this.fragment.childNodes;
[].slice.call(childNodes).forEach((node) = > {
// Listen for all nodes with y-model
if (node.getAttribute && this.checkAttribute(node)) {
const tmpAttribute = this.checkAttribute(node);
if(!this.modelObj[tmpAttribute]) {
this.modelObj[tmpAttribute] = [];
};
this.modelObj[tmpAttribute].push(node);
this.setModelData(tmpAttribute, node);
this.bindModelData(tmpAttribute, node);
}
// Save all nodes with {{}} template
if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
node.defaultContent = node.textContent; // Save the initialized front-end content to the defaultContent of the node
this.changeData(node);
this.nodeArr.push(node); // Save the node with the template
}
// Iterate through the child nodes recursively
if(node.childNodes && node.childNodes.length) {
this.matchElementModule(key, node.childNodes); }})}watcher(key) {
if (this.modelObj[key]) {
this.modelObj[key].forEach(node= > {
this.setModelData(key, node); })}for(const node of this.nodeArr) {
this.changeData(node); }}Copy the code
To see if the binding is successful, write the test code:
Success!!!!!
The final code looks like this:
class VM {
constructor(options, elementId) {
this.data = options.data || {}; // The data object to listen to
this.el = document.querySelector(elementId);
this.fragment = null; // Document fragment
this.matchModuleReg = new RegExp('\{\{\s*.*? \s*\}\}'.'gi'); // Match all {{}} templates
this.nodeArr = []; // All front-end nodes with templates
this.modelObj = {}; // The object bound to the Y-Model
this.init(); / / initialization
}
/ / initialization
init() {
this.observer();
this.createDocumentFragment();
for (const key of Object.keys(this.data)) {
this.matchElementModule(key);
}
this.el.appendChild(this.fragment);
}
// Listen for data changes
observer() {
const handler = {
get: (target, propkey) = > {
return target[propkey];
},
set: (target, propkey, value) = > {
if(target[propkey] ! == value) { target[propkey] = value;this.watcher(propkey);
}
return true; }};this.data = new Proxy(this.data, handler);
}
/** create a document fragment */
createDocumentFragment() {
let documentFragment = document.createDocumentFragment();
let child = this.el.firstChild;
// Loop to add nodes to the document fragment
while (child) {
documentFragment.appendChild(child);
child = this.el.firstChild;
}
this.fragment = documentFragment;
}
/** * Match template *@param { string } Key Key * that triggers the update@param { documentElement } Fragments node * /
matchElementModule(key, fragment) {
const childNodes = fragment || this.fragment.childNodes;
[].slice.call(childNodes).forEach((node) = > {
// Listen for all nodes with y-model
if (node.getAttribute && this.checkAttribute(node)) {
const tmpAttribute = this.checkAttribute(node);
if(!this.modelObj[tmpAttribute]) {
this.modelObj[tmpAttribute] = [];
};
this.modelObj[tmpAttribute].push(node);
this.setModelData(tmpAttribute, node);
this.bindModelData(tmpAttribute, node);
}
// Save all nodes with {{}} template
if (node.nodeType === 3 && this.matchModuleReg.test(node.textContent)) {
node.defaultContent = node.textContent; // Save the initialized front-end content to the defaultContent of the node
this.changeData(node);
this.nodeArr.push(node); // Save the node with the template
}
// Iterate through the child nodes recursively
if(node.childNodes && node.childNodes.length) {
this.matchElementModule(key, node.childNodes); }})}/** * Change view data *@param { documentElement } node* /
changeData(node) {
const matchArr = node.defaultContent.match(this.matchModuleReg); // Get all templates that need to be matched
let tmpStr = node.defaultContent;
for(const key of matchArr) {
tmpStr = tmpStr.replace(key, this.data[key.replace(/\{\{|\}\}|\s*/g.' ')] || ' ');
}
node.textContent = tmpStr;
}
watcher(key) {
if (this.modelObj[key]) {
this.modelObj[key].forEach(node= > {
this.setModelData(key, node); })}for(const node of this.nodeArr) {
this.changeData(node); }}/ / bind y - model
bindModelData(key, node) {
if (this.data[key]) {
node.addEventListener('input'.(e) = > {
this.data[key] = e.target.value;
}, false); }}// Set the y-model value
setModelData(key, node) {
node.value = this.data[key];
}
// Check the y-model properties
checkAttribute(node) {
return node.getAttribute('y-model'); }}Copy the code
The last
In this section, we use Proxy to implement a two-way binding that mimics VUE, starting from the listener to the observer step by step. There may be a lot of irregularities in the code. If you find errors, you can point out ~ ~