javascript实现数据双向绑定的三种方式小结


Posted in Javascript onMarch 09, 2017

前端数据的双向绑定方法

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

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、小结

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jquery $.getJSON()跨域请求
Dec 21 Javascript
Jqgrid表格随窗口大小改变而改变的简单实例
Dec 28 Javascript
JavaScript验证18位身份证号码最后一位正确性的实现代码
Aug 07 Javascript
javascript实现点击单选按钮链接转向对应网址的方法
Aug 12 Javascript
JavaScript驾驭网页-DOM
Mar 24 Javascript
jQuery实现花式轮播之圣诞节礼物传送效果
Dec 25 Javascript
JS小数转换为整数的方法分析
Jan 07 Javascript
使用Vue.js开发微信小程序开源框架mpvue解析
Mar 20 Javascript
layui 监听表格复选框选中值的方法
Aug 15 Javascript
微信接入之获取用户头像的方法步骤
Sep 23 Javascript
javascript实现倒计时效果
Feb 17 Javascript
JS前端宏任务微任务及Event Loop使用详解
Jul 23 Javascript
jQuery插件HighCharts实现2D柱状图、折线图的组合多轴图效果示例【附demo源码下载】
Mar 09 #Javascript
Vue监听数据对象变化源码
Mar 09 #Javascript
html+javascript+bootstrap实现层级多选框全层全选和多选功能
Mar 09 #Javascript
Node.js常用工具之util模块
Mar 09 #Javascript
js遍历json对象所有key及根据动态key获取值的方法(必看)
Mar 09 #Javascript
jQuery插件HighCharts实现的2D回归直线散点效果示例【附demo源码下载】
Mar 09 #Javascript
js实现简单的二级联动效果
Mar 09 #Javascript
You might like
PHP CodeIgniter框架的工作原理研究
2015/03/30 PHP
Symfony2开发之控制器用法实例分析
2016/02/05 PHP
PHP微信分享开发详解
2017/01/14 PHP
php解析mht文件转换成html的实例
2017/03/13 PHP
php面向对象程序设计入门教程
2019/06/22 PHP
jQuery 翻牌或百叶窗效果(内容三秒自动切换)
2012/06/14 Javascript
一个JS函数搞定网页标题(title)闪动效果
2014/05/13 Javascript
jQuery实现在最后一个元素之前插入新元素的方法
2015/07/18 Javascript
JS如何实现文本框随文本的长度而增长
2015/07/30 Javascript
javascript跨域的方法汇总
2015/10/23 Javascript
JQuery插件Marquee.js实现无缝滚动效果
2016/04/26 Javascript
angularjs实现猜数字大小功能
2020/05/20 Javascript
Angularjs之如何在跨域请求中传输Cookie的方法
2018/06/01 Javascript
解决angularJS中input标签的ng-change事件无效问题
2018/09/13 Javascript
微信小程序-form表单提交代码实例
2019/04/29 Javascript
Vue 使用计时器实现跑马灯效果的实例代码
2019/07/11 Javascript
[02:29]大剑、皮鞭、女装,这届DOTA2勇士令状里都有
2020/07/17 DOTA
二种python发送邮件实例讲解(python发邮件附件可以使用email模块实现)
2013/12/03 Python
Python变量和数据类型详解
2017/02/15 Python
python ChainMap 合并字典的实现步骤
2019/06/11 Python
Python中栈、队列与优先级队列的实现方法
2019/06/30 Python
python读取Excel表格文件的方法
2019/09/02 Python
通过实例解析Python调用json模块
2019/12/11 Python
Python实现把类当做字典来访问
2019/12/16 Python
Django通过设置CORS解决跨域问题
2020/11/26 Python
python中函数返回多个结果的实例方法
2020/12/16 Python
德国最大的拼图在线商店:Puzzle.de
2016/12/17 全球购物
四年大学生活的个人自我评价
2013/12/11 职场文书
成功经营餐厅的创业计划书范文
2013/12/26 职场文书
出纳担保书范文
2014/04/02 职场文书
学年个人总结范文
2015/03/05 职场文书
个人原因辞职信模板
2015/05/13 职场文书
2016年校园重阳节广播稿
2015/12/18 职场文书
Golang并发操作中常见的读写锁详析
2021/08/30 Golang
基于Python实现对比Exce的工具
2022/04/07 Python
详解如何使用Nginx解决跨域问题
2022/05/06 Servers