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 hereproxyImplement 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 vuev-modelAs 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:

  1. Listen for all nodes of the entire bound Element and match all of the nodes{{text}}The template
  2. 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:

  1. Gets all input nodes with v-Model
  2. 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 ~ ~