vue implements bidirectional data binding

Posted by Nomaad on Sun, 16 Jan 2022 13:41:47 +0100

Principle of realizing bidirectional data binding by vue

vue bidirectional data binding is realized by data hijacking combined with publisher subscriber mode.

vue through object Defineproperty() to implement data hijacking.
Object.defineProperty() can control some special operations of objects, such as:

let person = {}
let name = ''
Object.defineProperty(person, 'name', {
       value: 'jack',
       configurable: false,//Whether the property is configured and can be deleted
       writable: true,//Read / write
       enumerable: true//Whether enumerable,
       get: function () {
       		return temp
       }
       set: function (val) {
			name = val
		}
   })

Implementation process

We already know that to realize the two-way binding of data, we must first hijack and listen to the data, so we need to set a listener Observer to listen to all properties. If the attribute changes, you need to tell the subscriber Watcher to see if it needs to be updated. Because there are many subscribers, we need a message subscriber Dep to collect these subscribers, and then manage them uniformly between the listener Observer and the subscriber Watcher. Then, we also need an instruction parser Compile to scan and parse each node element, initialize the relevant instructions into a subscriber Watcher, replace the template data or bind the corresponding function. At this time, when the subscriber Watcher receives the corresponding attribute change, it will execute the corresponding update function to update the view. Therefore, next, we perform the following three steps to realize the two-way binding of data:

1. Implement a listener Observer, which is used to hijack and listen to all properties. If there is any change, it will notify the subscriber.

2. Implement a subscriber Watcher, which can receive the change notification of the attribute and execute the corresponding function to update the view.

3. Implement a parser Compile, which can scan and parse the relevant instructions of each node, and initialize the corresponding subscriber according to the initialization template data.

The flow chart is as follows:

Implementation process



function Compile(el, vm) {
    this.vm = vm;
    this.el = document.querySelector(el);
    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');
        }
    },
    nodeToFragment: function (el) {
        var fragment = document.createDocumentFragment();
        var child = el.firstChild;
        while (child) {
            // Move Dom element into fragment
            fragment.appendChild(child);
            child = el.firstChild
        }
        return fragment;
    },
    compileElement: function (el) {
        var childNodes = el.childNodes;
        var self = this;
        [].slice.call(childNodes).forEach(function(node) {
            var reg = /\{\{(.*)\}\}/;
            var text = node.textContent;

            if (self.isElementNode(node)) {  
                self.compile(node);
            } else if (self.isTextNode(node) && reg.test(text)) {
                self.compileText(node, reg.exec(text)[1]);
            }

            if (node.childNodes && node.childNodes.length) {
                self.compileElement(node);
            }
        });
    },
    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 instruction
                    self.compileEvent(node, self.vm, exp, dir);
                } else {  // v-model instruction
                    self.compileModel(node, self.vm, exp, dir);
                }
                node.removeAttribute(attrName);
            }
        });
    },
    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);
        });
    },
    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, dir) {
        var self = this;
        var val = this.vm[exp];
        this.modelUpdater(node, val);
        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, oldValue) {
        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;
    }
}


Learn from this: Original author article link.

Topics: Front-end Vue