文档章节

前端数据的双向绑定方法

ouven
 ouven
发布于 2015/12/09 11:20
字数 1544
阅读 489
收藏 9

前端数据的双向绑定方法

  前端的视图层和数据层有时需要实现双向绑定(two-way-binding),例如mvvm框架,数据驱动视图,视图状态机等,研究了几个目前主流的数据双向绑定框架,总结了下。目前实现数据双向绑定主要有以下三种。

github演示例子

1、手动绑定

比较老的实现方式,有点像观察者编程模式,主要思路是通过在数据对象上定义get和set方法(当然还有其它方法),调用时手动调用get或set数据,改变数据后出发UI层的渲染操作;以视图驱动数据变化的场景主要应用与input、select、textarea等元素,当UI层变化时,通过监听dom的change,keypress,keyup等事件来出发事件改变数据层的数据。整个过程均通过函数调用完成。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>data-binding-method-set</title>
</head>
<body>
    <input q-value="value" type="text" id="input">
    <div q-text="value" id="el"></div>
    <script>
        var elems = [document.getElementById('el'), document.getElementById('input')];

        var data = {
            value: 'hello!'
        };

        var command = {
            text: function(str){
                this.innerHTML = str;
            },
            value: function(str){
                this.setAttribute('value', str);
            }
        };

        var scan = function(){        
            /**
             * 扫描带指令的节点属性
             */
            for(var i = 0, len = elems.length; i < len; i++){
                var elem = elems[i];
                elem.command = [];
                for(var j = 0, len1 = elem.attributes.length; j < len1; j++){
                    var attr = elem.attributes[j];
                    if(attr.nodeName.indexOf('q-') >= 0){
                        /**
                         * 调用属性指令,这里可以使用数据改变检测
                         */
                        command[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
                        elem.command.push(attr.nodeName.slice(2));
                    }
                }
            }
        }

        /**
         * 设置数据后扫描
         */
        function mvSet(key, value){
            data[key] = value;
            scan();
        }
        /**
         * 数据绑定监听
         */
        elems[1].addEventListener('keyup', function(e){
            mvSet('value', e.target.value);
        }, false);

        scan();

        /**
         * 改变数据更新视图
         */
        setTimeout(function(){
            mvSet('value', 'fuck');
        },1000)

    </script>
</body>
</html>

2、脏检查机制

  以典型的mvvm框架angularjs为代表,angular通过检查脏数据来进行UI层的操作更新。关于angular的脏检测,有几点需要了解些:

  • 脏检测机制并不是使用定时检测。
  • 脏检测的时机是在数据发生变化时进行。
  • angular对常用的dom事件,xhr事件等做了封装, 在里面触发进入angular的digest流程。
  • 在digest流程里面, 会从rootscope开始遍历, 检查所有的watcher。 (关于angular的具体设计可以看其他文档,这里只讨论数据绑定),那我们看下脏检测该如何去做:主要是通过设置的数据来需找与该数据相关的所有元素,然后再比较数据变化,如果变化则进行指令操作
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>data-binding-drity-check</title>
</head>

<body>
    <input q-event="value" ng-bind="value" type="text" id="input">
    <div q-event="text" ng-bind="value" id="el"></div>
    <script>

    var elems = [document.getElementById('el'), document.getElementById('input')];
    
    var data = {
        value: 'hello!'
    };

    var command = {
        text: function(str) {
            this.innerHTML = str;
        },
        value: function(str) {
            this.setAttribute('value', str);
        }
    };

    var scan = function(elems) {
        /**
         * 扫描带指令的节点属性
         */
        for (var i = 0, len = elems.length; i < len; i++) {
            var elem = elems[i];
            elem.command = {};
            for (var j = 0, len1 = elem.attributes.length; j < len1; j++) {
                var attr = elem.attributes[j];
                if (attr.nodeName.indexOf('q-event') >= 0) {
                    /**
                     * 调用属性指令
                     */
                    var dataKey = elem.getAttribute('ng-bind') || undefined;
                    /**
                     * 进行数据初始化
                     */
                    command[attr.nodeValue].call(elem, data[dataKey]);
                    elem.command[attr.nodeValue] = data[dataKey];
                }
            }
        }
    }

    /**
     * 脏循环检测
     * @param  {[type]} elems [description]
     * @return {[type]}       [description]
     */
    var digest = function(elems) {
        /**
         * 扫描带指令的节点属性
         */
        for (var i = 0, len = elems.length; i < len; i++) {
            var elem = elems[i];
            for (var j = 0, len1 = elem.attributes.length; j < len1; j++) {
                var attr = elem.attributes[j];
                if (attr.nodeName.indexOf('q-event') >= 0) {
                    /**
                     * 调用属性指令
                     */
                    var dataKey = elem.getAttribute('ng-bind') || undefined;

                    /**
                     * 进行脏数据检测,如果数据改变,则重新执行指令,否则跳过
                     */
                    if(elem.command[attr.nodeValue] !== data[dataKey]){

                        command[attr.nodeValue].call(elem, data[dataKey]);
                        elem.command[attr.nodeValue] = data[dataKey];
                    }
                }
            }
        }
    }

    /**
     * 初始化数据
     */
    scan(elems);

    /**
     * 可以理解为做数据劫持监听
     */
    function $digest(value){
        var list = document.querySelectorAll('[ng-bind='+ value + ']');
        digest(list);
    }

    /**
     * 输入框数据绑定监听
     */
    if(document.addEventListener){
        elems[1].addEventListener('keyup', function(e) {
            data.value = e.target.value;
            $digest(e.target.getAttribute('ng-bind'));
        }, false);
    }else{
        elems[1].attachEvent('onkeyup', function(e) {
            data.value = e.target.value;
            $digest(e.target.getAttribute('ng-bind'));
        }, false);
    }

    setTimeout(function() {
        data.value = 'fuck';
        /**
         * 这里问啥还要执行$digest这里关键的是需要手动调用$digest方法来启动脏检测
         */
        $digest('value');
    }, 2000)

    </script>
</body>
</html>

3、前端数据劫持(Hijacking)

  第三种方法则是avalon等框架使用的数据劫持方式。基本思路是使用Object.defineProperty对数据对象做属性get和set的监听,当有数据读取和赋值操作时则调用节点的指令,这样使用最通用的=等号赋值就可以了。具体实现如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>data-binding-hijacking</title>
</head>

<body>
    <input q-value="value" type="text" id="input">
    <div q-text="value" id="el"></div>
    <script>


    var elems = [document.getElementById('el'), document.getElementById('input')];

    var data = {
        value: 'hello!'
    };

    var command = {
        text: function(str) {
            this.innerHTML = str;
        },
        value: function(str) {
            this.setAttribute('value', str);
        }
    };

    var scan = function() {
        /**
         * 扫描带指令的节点属性
         */
        for (var i = 0, len = elems.length; i < len; i++) {
            var elem = elems[i];
            elem.command = [];
            for (var j = 0, len1 = elem.attributes.length; j < len1; j++) {
                var attr = elem.attributes[j];
                if (attr.nodeName.indexOf('q-') >= 0) {
                    /**
                     * 调用属性指令
                     */
                    command[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
                    elem.command.push(attr.nodeName.slice(2));

                }
            }
        }
    }

    var bValue;
    /**
     * 定义属性设置劫持
     */
    var defineGetAndSet = function(obj, propName) {
        try {
            Object.defineProperty(obj, propName, {

                get: function() {
                    return bValue;
                },
                set: function(newValue) {
                    bValue = newValue;
                    scan();
                },

                enumerable: true,
                configurable: true
            });
        } catch (error) {
            console.log("browser not supported.");
        }
    }
    /**
     * 初始化数据
     */
    scan();

    /**
     * 可以理解为做数据劫持监听
     */
    defineGetAndSet(data, 'value');

    /**
     * 数据绑定监听
     */
    if(document.addEventListener){
        elems[1].addEventListener('keyup', function(e) {
            data.value = e.target.value;
        }, false);
    }else{
        elems[1].attachEvent('onkeyup', function(e) {
            data.value = e.target.value;
        }, false);
    }

    setTimeout(function() {
        data.value = 'fuck';
    }, 2000)
    </script>
</body>

</html>

  但值得注意的是defineProperty支持IE8以上的浏览器,这里可以使用____defineGetter____ 和 defineSetter 来做兼容但是浏览器兼容性的原因,直接用defineProperty就可以了。至于IE8浏览器仍需要使用其它方法来做hack。如下代码可以对IE8进行hack,defineProperty支持IE8。例如使用es5-shim.js就可以了。(IE8以下浏览器忽略)

4、小结

  首先这里的例子只是简单的实现,读者可以深入感受三种方式的异同点,复杂的框架也是通过这样的基本思路滚雪球滚大的。

演示例子

参考:

https://gist.github.com/brettz9/4093766#file_html5_dataset.js https://github.com/xufei/blog/issues/10

原文链接:https://ouvens.github.io/frontend-javascript/2015/11/29/js-data-two-ways-binding.html

© 著作权归作者所有

共有 人打赏支持
ouven

ouven

粉丝 106
博文 30
码字总数 77829
作品 0
深圳
高级程序员
加载中

评论(1)

-龙猫-
-龙猫-
用了那么久的MVVM,从来没有想过,或是去了解过双向绑定是怎么实现的。
这样的好文,给了一个非常到位的解释。学习学习了!:thumbsup:
Git@OSC 项目推荐 —— bingoJS 前端框架,文档巨详细

大家经常吐槽国内很多开源项目文档非常简陋,不少项目的确如此,包括我自己的项目,有时候就是给自己很多借口,懒得去完善文档。 但是今天在 Git@OSC 上看到 bingoJS 这个项目,着实让我非常...

红薯
2015/06/05
7.6K
35
面试官: 实现双向绑定Proxy比defineproperty优劣如何?

面试官系列(4): 实现双向绑定Proxy比defineproperty优劣如何? 往期 面试官系列(1): 如何实现深克隆 面试官系列(2): Event Bus的实现 面试官系列(3): 前端路由的实现 前言 双向绑定其实已经是...

寻找海蓝96
05/03
0
0
Vue.js 和 MVVM 小细节

MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自动传递给 View,即所谓的数据双...

智能机器人蓝梓轩
07/24
0
0
前端 MV 开发框架--bingoJS

1、关于bingoJS bingoJS是一个前端MV*开发框架,提供了前端框架所需基础要素,目前已经支持模块化开发、按需动态加载、完善的双向绑定、依赖注入等;让你在开发只关注业务代码的实现。 2、提...

front-Y
2015/06/04
2.8K
2
AngularJS学习笔记之一: AngularJS入门

一、AngularJS是什么? AngularJS是由Misko Hevery 和 Adam Abrons 两个人共同创建的,在2009年卖给了Google,它是一个构建动态Web应用的一个Javascript框架,目的是为了克服HTML在构建Web应...

阿振
2016/03/21
458
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Minifilter的动态安装、加载及卸载

MINIFILTER框架的文件系统过滤驱动,无法使用的CreateService和OpenService进行动态加载。 看了一下,使用Inf文件安装Minifilter驱动的方式是在注册表驱动服务项下比传统驱动多创建了Instanc...

simpower
23分钟前
1
0
idea新建springCloud项目(6)- Config Server使用

1.在IDEA新建springCloud项目-Config Server 修改版本,和之前建的eureka项目版本一致,修改完记得刷新: 删除掉不需要的文件: 2.把Config S 服务注册到eureka上去,配置git地址,启动项目 ...

monroeCode
29分钟前
3
0
大数据可视化项目开发总纲

第1章 开发文档总纲 1.1 开发工具清单 名称 版本 备注 Pentaho-bi server pentaho-server-ce-7.1 Pentaho Cde为其内置工具 Pentaho-prd pentaho-prd-ce-7.1 Pentaho Report Designer报表工具...

ZhangLG
29分钟前
2
0
pip安装超时问题

pip3 install --default-timeout=100 tensorflow 设置为100秒 参考: User Guide How to solve ReadTimeoutError: HTTPSConnectionPool(host='pypi.python.org', port=443) with pip?......

亚林瓜子
31分钟前
1
0
fragment 旋转时保持当前实例

设备旋转时保存Fragment的交互状态: setRetainInstance(true);

zdglf
33分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部