• JavaScript实现简单的双向数据绑定


    什么是双向数据绑定

    双向数据绑定简单来说就是UI视图(View)与数据(Model)相互绑定在一起,当数据改变之后相应的UI视图也同步改变。反之,当UI视图改变之后相应的数据也同步改变。

    双向数据绑定最常见的应用场景就是表单输入和提交。一般情况下,表单中各个字段都对应着某个对象的属性,这样当我们在表单输入数据的时候相应的就改变对应的对象属性值,反之对象属性值改变之后也反映到表单中。

    目前流行的 MVVM 框架(Angular、Vue)都实现了双向数据绑定,这样也就实现了视图层和数据层的分离。相信使用过 jQuery 的人都知道,往往我们在获取到数据之后就直接操作 DOM ,这样数据操作和 DOM 操作就高度耦合在一起了。

    实现方式

    发布者-订阅者模式

    这种实现方式就是使用自定义的 data 属性在 HTML 代码中指明绑定。所有绑定起来的 JavaScript 对象以及 DOM 元素都将 “订阅” 一个发布者对象。任何时候如果 JavaScript 对象或者一个 HTML 输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。具体实现可看这篇文章:http://www.html-js.com/article/Study-of-twoway-data-binding-JavaScript-talk-about-JavaScript-every-day

    脏值检查

    Angularjs(这里特指AngularJS 1.x.x版本,不代表AngularJS 2.x.x版本)双向数据绑定的技术实现是脏值检查。原理就是:Angularjs内部会维护一个序列,将所有需要监控的属性放在这个序列中,当发生某些特定事件时(并不是定时的而是由某些特殊事件触发的,比如:DOM事件、XHR事件等等),Angularjs会调用 $digest 方法,这个方法内部做的逻辑就是遍历所有的 watcher,对被监控的属性做对比,对比其在方法调用前后属性值有没有发生变化,如果发生变化,则调用对应的 handler。

    这种方式的缺点很明显,遍历轮训 watcher 是非常消耗性能的,特别是当单页的监控数量达到一个数量级的时候。

    访问器监听

    vue.js 实现数据双向绑定的原理就是访问器监听。它使用了 ECMAScript5.1(ECMA-262)中定义的标准属性 Object.defineProperty 方法。通过 Object.defineProperty 设置各个属性的 setter,getter,在数据变动时更新UI视图。

    实现

    本文将采用 访问器监听 这种方式来实现一个简单的双向数据绑定,主要实现:

    • obverse:对数据进行处理,重写相应的 set 和 get 函数
    • complie:解析指令(e-bind、e-model、e-click)等,并在这个过程中对 view 与 model 进行绑定
    • Watcher:作为连接 obversecomplie 的桥梁,用来绑定更新函数,实现对视图的更新

    首先看下我们的视图代码:

    <!DOCTYPE html>
    <head>
        <meta charset="UTF-8">
        <meta name="author" content="赖祥燃, laixiangran@163.com, 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>
    

    从视图代码可以看出,在 <div id="app"> 的子元素中我们应用了三个自定义指令
    e-binde-modele-click, 然后我们通过 new EBind({***}) 应用双向数据绑定。

    分析

    EBind

    EBind 构造函数接收应用根元素、数据、方法来初始化双向数据绑定:

    
    function EBind(options) {
        this._init(options);
    }
    
    EBind.prototype._init = function (options) {
    
        // options 为上面使用时传入的结构体,包括 el, data, methods
        this.$options = options;
    
        // el 是 #app, this.$el 是 id 为 app 的 Element 元素
        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 保存着 model 与 view 的映射关系,也就是我们定义的 Watcher 的实例。当 model 改变时,我们会触发其中的指令类更新,保证 view 也能实时更新
        this._binding = {};
    
        // 重写 this.$data 的 set 和 get 方法
        this._obverse(this.$data);
    
        // 解析指令
        this._complie(this.$el);
    };
    
    

    obverse

    _obverse 的关键是使用 Object.defineProperty 来定义传入数据对象的 getter 及 setter,通过 setter 来监听对象属性的变化从而触发 Watcher 中的更新方法。

    
    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

    _complie 的关键是简析自定义指令,根据不同的自定义指令实现不同的功能。如 e-click 就解析为将对应 node 绑定 onclick 事件,e-model 必须绑定在 INPUT 和 TEXTAREA 上,然后监听 input 事件,更改 model 的值,e-bind 就直接将绑定的变量值输出到DOM元素中。

    
    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

    作为连接 _obverse_complie 的桥梁,用来绑定更新函数,通过 update 实现对视图的更新。

    
    function Watcher(options) {
        // options 属性:
        // name 指令名称,例如文本节点,该值设为"text"
        // el 指令对应的DOM元素
        // eb 指令所属EBind实例
        // exp 指令对应的值,本例如"number"
        // attr 绑定的属性值,本例为"innerHTML"
        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);
    };
    
    

    总结

    这样我们就使用原生 JavaScript 实现了简单的双向数据绑定。

    源码:https://github.com/laixiangran/e-bind

  • 相关阅读:
    Codeforces Round #649 (Div. 2) A、B、C、
    poj1061 青蛙的约会(扩展欧几里得)
    Educational Codeforces Round 89 (Rated for Div. 2)A、B、C、D、E
    jxust摸底测试1
    Codeforces Round #648 (Div. 2) A、B、C、D、E、F
    大数模板
    任意进制转换(2019 ICPC Asia Yinchuan Regional Base62)
    求素数(从判断素数到筛法)
    直线 (__int128的使用)
    E. Tree Shuffling (Round #646 (Div. 2))
  • 原文地址:https://www.cnblogs.com/laixiangran/p/8922301.html
Copyright © 2020-2023  润新知