文档章节

前端数据的双向绑定方法

ouven
 ouven
发布于 2015/12/09 11:20
字数 1544
阅读 486
收藏 9
点赞 0
评论 1

前端数据的双向绑定方法

  前端的视图层和数据层有时需要实现双向绑定(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

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

评论(1)

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

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

红薯 ⋅ 2015/06/05 ⋅ 35

面试官: 实现双向绑定Proxy比defineproperty优劣如何?

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

寻找海蓝96 ⋅ 05/03 ⋅ 0

前端 MV 开发框架--bingoJS

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

front-Y ⋅ 2015/06/04 ⋅ 2

AngularJS学习笔记之一: AngularJS入门

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

阿振 ⋅ 2016/03/21 ⋅ 0

AngularJs双向数据绑定原理

Angular JS (Angular.JS) 是一组用来开发Web页面的框架、模板以及数据绑定和丰富UI组件。它支持整个开发进程,提供web应用的架构,无需进行手工DOM操作。 AngularJS很小,只有60K,兼容主流浏...

孟飞阳 ⋅ 2016/12/12 ⋅ 0

AngularJS(一、我们为什么选择AngularJS)

在学习和使用AngularJS的过程中,一个这样的问题一直萦绕在我们心头:我们为什么选择AngularJS 首先我们来看一下官方的说明: AngularJS是为了克服HTML在构建应用上的不足而设计的。HTML是一...

雪飘七月 ⋅ 2015/04/20 ⋅ 0

一入前端深似海,从此红尘是路人系列第九弹之如何实现一个双向数据绑定

前言 简单介绍一下双向数据绑定实现的是一个什么样的东西。首先有两个东西,他们分别是: V-视图层 M-数据层 1、视图层传向数据层:V发生改变,M也发生改变 2、数据层传向视图层:M发生改变,...

qiangdada ⋅ 2016/11/21 ⋅ 7

front-Y/bingoJS

参考文档:http://bingojs.mydoc.io/ 库包下载:https://git.oschina.net/bingoJS/bingoJS/blob/master/bingo.full.zip Demo:https://git.oschina.net/bingoJS/bingoJS/tree/master/demo 更新......

front-Y ⋅ 2015/06/05 ⋅ 0

iOS 与 JS 交互手册 - JavaScriptCore

在前端快速发展的今天,JavaScript 在移动端的应用也越来越广泛,作为 iOS 开发者来说,与 JavaScript 进行交互也是不大不小的一个方面,尤其是一些 内嵌页, 和 对 JavaScript 语言来说就像...

薛定諤 ⋅ 2017/12/05 ⋅ 0

mvvm-simple双向绑定简单实现

原文链接 mvvm原理分析 JavaScript在浏览器中操作HTML经历了几个不同阶段 第一阶段 直接用浏览器提供的原生API操作DOM元素 第二阶段 jQuery的出现解决了原生API的复杂性和浏览器间的兼容性等...

bestvist ⋅ 04/17 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Android JNI 读写Bitmap的方法

Java层创建Bitmap,通过JNI将Bitmap传到C/C++进行处理 Java部分 public static native boolean greenBitmap(Bitmap bitmap); C/C++部分 JNIEXPORT jboolean JNICALL Java_com_test_Test_gree......

国仔饼 ⋅ 19分钟前 ⋅ 0

一次性让你懂async/await,解决回调地狱

什么是async? 欢迎留言讨论 async 函数是 Generator 函数的语法糖。使用 关键字 async 来表示,在函数内部使用 await 来表示异步。相较于 Generator,async 函数的改进在于下面四点: 内置执...

阿K1225 ⋅ 19分钟前 ⋅ 0

angular常用命令

.下载更新操作 1.利用npm下载angular的命令行工具AngularCLI: npm install -g @angular/cli 2.下载jquery: npm install --save jquery 3.更新npm: npm i -g npm 4.更新angular: ng update ......

消散了的诗意 ⋅ 21分钟前 ⋅ 0

window.print 页面打印

定义和用法 print() 方法用于打印当前窗口的内容。 语法 window.print(); window.print() 实际上,是浏览器打印功能菜单的一种程序调用。与点击打印功能菜单一样,不能精确分页,不能设置纸型...

初学者的优化 ⋅ 22分钟前 ⋅ 0

魔兽世界 7.0版本上 PVE装备全攻略

  T套 因为大家应该都会打穿副本的所以具体是哪个boss我就不说了。   T1: 所有套装都在【熔火之心】出   T2: 头原来是在【奥妮克希亚的巢穴】改到黑翼之巢的奈法利安了,腿是在【熔火之...

wangchen1999 ⋅ 23分钟前 ⋅ 0

java.math.BigDecimal使用小结

原文地址 java.math.BigDecimal使用小结 divide方法 使用BigDecimal.divide方法时一定要考虑: 除数是否为0 商是否是无限小数 正确的使用方式 判断除数是否为0,是0做另外的处理逻辑 调用除法...

666B ⋅ 25分钟前 ⋅ 0

关于qstring转char乱码问题。

if (OpenClipboard(NULL)) { HGLOBAL hgClip; EmptyClipboard(); QByteArray byay = FValue.toLocal8Bit(); //转latin编码 char *bochsrc_line = byay.data(); hgClip = GlobalAlloc(GMEM_DD......

backtrackx ⋅ 26分钟前 ⋅ 0

了解SSH加密和连接过程

介绍 SSH或安全shell是安全协议,也是安全管理远程服务器的最常用方式。通过使用多种加密技术,SSH提供了一种机制,用于在双方之间建立加密安全连接,对彼此进行身份验证,以及来回传递命令和...

吴伟祥 ⋅ 32分钟前 ⋅ 0

微信小程序

小程序的全局配置app.json 微信小程序的全局配置保存在app.json文件中。开发者通过使用app.json来配置页面文件(pages)的路径、窗口(window)表现、设定网络超时时间值(networkTimeout)以...

上官清偌 ⋅ 35分钟前 ⋅ 0

【转】百度坐标坐标系之间的转换(JS版代码)

/** * Created by Wandergis on 2015/7/8. * 提供了百度坐标(BD09)、国测局坐标(火星坐标,GCJ02)、和WGS84坐标系之间的转换 *///定义一些常量var x_PI = 3.1415926535897932...

HAVENT ⋅ 37分钟前 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部