JavaScript implements simple two-way data binding

What is two-way data binding

Two-way data binding is simply that the UI view (View) and the data (Model) are bound to each other, and when the data changes, the corresponding UI view changes synchronously. Conversely, when the UI view changes, the corresponding data also changes synchronously.

The most common use case for two-way data binding is form input and submission. In general, each field in the form corresponds to the attribute of an object, so when we enter data in the form, the corresponding object attribute value is changed accordingly, and vice versa, the object attribute value is also reflected in the form after the change.

The current popular MVVM frameworks (Angular, Vue) all implement two-way data binding, which also realizes the separation of the view layer and the data layer. I believe those who have used jQuery know that we often operate the DOM directly after obtaining the data, so that the data operation and the DOM operation are highly coupled together.

Method to realize

Publisher-Subscriber Pattern

This is achieved by using a custom data attribute to indicate the binding in the HTML code. All bound JavaScript objects and DOM elements will "subscribe" to a publisher object. Anytime a JavaScript object or an HTML input field is detected to have changed, we will delegate the event to the publisher-subscriber pattern, which will in turn broadcast and propagate the change to all bound objects and elements. The specific implementation can be found in this article: http://www.html-js.com/article/Study-of-twoway-data-binding-JavaScript-talk-about-JavaScript-every-day

dirty value check

Angularjs (here refers specifically to AngularJS 1.xx version, does not represent AngularJS 2.xx version) The technical implementation of two-way data binding is dirty value checking. The principle is: Angularjs will maintain a sequence internally, and put all the properties that need to be monitored in this sequence. When some specific events occur (not timed but triggered by some special events, such as: DOM events, XHR Events, etc.), Angularjs will call the $digest method. The logic inside this method is to traverse all watchers, compare the monitored attributes, and compare whether the attribute values ​​have changed before and after the method call. If there is a change, then Call the corresponding handler.

The disadvantage of this method is obvious. It is very performance-intensive to traverse the training watcher, especially when the number of monitoring on a single page reaches an order of magnitude.

accessor listener

The principle of vue.js to implement two-way data binding is accessor monitoring. It uses the standard property Object.defineProperty method defined in ECMAScript 5.1 (ECMA-262) . Set the setter and getter of each property through Object.defineProperty, and update the UI view when the data changes.

accomplish

This article will use 访问器监听this method to implement a simple two-way data binding, mainly to achieve:

  • **_obverse**: Process the data and rewrite the corresponding set and get functions
  • **_complie**: Parse instructions (e-bind, e-model, e-click), etc., and bind view and model in the process
  • Watcher : As a bridge connecting _obverse and _complie, it is used to bind the update function to update the view

First look at our view code:

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <meta name="author" content="赖祥燃, [email protected], http://www.laixiangran.cn"/>
    <title>实现简单的双向数据绑定</title>
    <style>
        #app {
            text-align: center;
        }
    </style>
    <script src="eBind.js"></script>
    <script>
        window.onload = function () {
            new EBind({
                el: '#app',
                data: {
                    number: 0,
                    person: {
                        age: 0
                    }
                },
                methods: {
                    increment: function () {
                        this.number++;
                    },
                    addAge: function () {
                        this.person.age++;
                    }
                }
            });
        };
    </script>
</head>
<body>
<div id="app">
    <form>
        <input type="text" e-model="number">
        <button type="button" e-click="increment">增加</button>
    </form>
    <h3 e-bind="number"></h3>
    <form>
        <input type="text" e-model="person.age">
        <button type="button" e-click="addAge">增加</button>
    </form>
    <h3 e-bind="person.age"></h3>
</div>
</body>

As you can see from the view code, in <div id="app">the child element of , we apply three custom directives
e-bind, e-model, e-click, and then we new EBind({***})apply .

## analyze

### EBind

EBindThe constructor receives the application root element, data, and methods to initialize the two-way data binding:

```javascript
/**

  • EBind constructor
  • @param options
  • @constructor
    */
    function EBind(options) {
    this._init(options);
    }

/**

  • Initialize the constructor
  • @param options
  • @private
    */
    EBind.prototype._init = function (options) {

    // options is the structure passed in when used above, including el, data, methods
    this.$options = options;

    // el is #app, this.$el is the Element with id app
    this.$el = document.querySelector(options.el);

    // this.$data = {number: 0}
    this.$data = options.data;

    // this.$methods = {increment: function () { this.number++; }}
    this.$methods = options.methods;

    // _binding saves the mapping relationship between model and view, which is the instance of Watcher we defined. When the model changes, we will trigger the update of the instruction class in it to ensure that the view can also be updated in real time
    this._binding = {};

    // Override the set and get methods
    of this.$data this._obverse(this.$data);

    // Parse the directive
    this._complie(this.$el);
    };
    ```

### _obverse

_obverseThe key is to use Object.defineProperty to define the getter and setter of the incoming data object, and use the setter to monitor changes in object properties to trigger the update method in Watcher.

```javascript
/**

  • Process the data and rewrite the corresponding set and get functions
  • @param currentObj current object
  • @param completeKey
  • @private
    */
    EBind.prototype._obverse = function (currentObj, completeKey) {
    var _this = this;
    Object.keys(currentObj).forEach(function (key) {
    if (currentObj.hasOwnProperty(key)) {

        // 按照前面的数据,_binding = {number: _directives: [], preson: _directives: [], preson.age: _directives: []}
        var completeTempKey = completeKey ? completeKey + '.' + key : key;
        _this._binding[completeTempKey] = {
            _directives: []
        };
        var value = currentObj[key];
    
        // 如果值还是对象,则遍历处理
        if (typeof value === 'object') {
            _this._obverse(value, completeTempKey);
        }
        var binding = _this._binding[completeTempKey];
    
        // 双向数据绑定的关键
        Object.defineProperty(currentObj, key, {
            enumerable: true,
            configurable: true,
            get: function () {
                console.log(key + '获取' + JSON.stringify(value));
                return value;
            },
            set: function (newVal) {
                if (value !== newVal) {
                    console.log(key + '更新' + JSON.stringify(newVal));
                    value = newVal;
    
                    // 当 number 改变时,触发 _binding[number]._directives 中的绑定的 Watcher 类的更新
                    binding._directives.forEach(function (item) {
                        item.update();
                    });
                }
            }
        });
    }

    })
    };
    ```

### _complie

_complieThe key is to analyze the custom instructions briefly, and implement different functions according to different custom instructions. For example e-click, it is resolved to bind the corresponding node to the onclick event, which e-modelmust be bound to INPUT and TEXTAREA, and then listen to the input event, change the value of the model, e-bindand directly output the bound variable value to the DOM element.

```javascript
/**

  • Parse instructions (e-bind, e-model, e-click), etc., and bind view and model in the process
  • @param root root is the Element element whose id is app, which is our root element
  • @private
    */
    EBind.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);
    }
    
    // 如果有 e-click 属性,我们监听它的 onclick 事件,触发 increment 事件,即 number++
    if (node.hasAttribute('e-click')) {
        node.onclick = (function () {
            var attrVal = node.getAttribute('e-click');
    
            // bind 是使 data 的作用域与 method 函数的作用域保持一致
            return _this.$methods[attrVal].bind(_this.$data);
        })();
    }
    
    // 如果有 e-model 属性且元素是 INPUT 和 TEXTAREA,我们监听它的 input 事件,更改 model 的值
    if (node.hasAttribute('e-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
        node.addEventListener('input', (function (index) {
            var attrVal = node.getAttribute('e-model');
    
            // 添加指令类 Watcher
            _this._binding[attrVal]._directives.push(new Watcher({
                name: 'input',
                el: node,
                eb: _this,
                exp: attrVal,
                attr: 'value'
            }));
    
            return function () {
                var keys = attrVal.split('.');
                var lastKey = keys[keys.length - 1];
                var model = keys.reduce(function (value, key) {
                    if (typeof value[key] !== 'object') {
                        return value;
                    }
                    return value[key];
                }, _this.$data);
                model[lastKey] = nodes[index].value;
            }
        })(i));
    }
    
    // 如果有 e-bind 属性
    if (node.hasAttribute('e-bind')) {
        var attrVal = node.getAttribute('e-bind');
    
        // 添加指令类 Watcher
        _this._binding[attrVal]._directives.push(new Watcher({
            name: 'text',
            el: node,
            eb: _this,
            exp: attrVal,
            attr: 'innerHTML'
        }));
    }

    }
    };
    ```

Watcher

As a bridge connecting _obverse and _complie, it is used to bind the update function and update the view through update.

/**
 * 指令类Watcher,用来绑定更新函数,实现对DOM元素的更新
 * @param options Watcher 类属性:
 * name 指令名称,例如文本节点,该值设为"text"
 * el 指令对应的DOM元素
 * eb 指令所属EBind实例
 * exp 指令对应的值,本例如"number"
 * attr 绑定的属性值,本例为"innerHTML"
 * @constructor
 */
function Watcher(options) {
    this.$options = options;
    this.update();
}

/**
 * 根据 model 更新 view
 */
Watcher.prototype.update = function () {
    var _this = this;
    var keys = this.$options.exp.split('.');

    // 比如 H3.innerHTML = this.data.number; 当 number 改变时,会触发这个 update 函数,保证对应的 DOM 内容进行了更新。
    this.$options.el[this.$options.attr] = keys.reduce(function (value, key) {
        return value[key];
    }, _this.$options.eb.$data);
};

Summarize

This allows us to implement simple two-way data binding using native JavaScript.

Source code: https://github.com/laixiangran/e-bind

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324781453&siteId=291194637