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 相关文章推荐
JavaScript修改浏览器tab标题小技巧
Jan 06 Javascript
五种js判断是否为整数类型方式
Dec 03 Javascript
javascript加载xml 并解析各节点的值(实现方法)
Oct 12 Javascript
概述jQuery中的ajax方法
Dec 16 Javascript
微信小程序开发之好友列表字母列表跳转对应位置
Sep 26 Javascript
vue计算属性时v-for处理数组时遇到的一个bug问题
Jan 21 Javascript
JS实现的集合去重,交集,并集,差集功能示例
Mar 13 Javascript
JavaScript实现新年倒计时效果
Nov 17 Javascript
Vue CL3 配置路径别名详解
May 30 Javascript
自定义javascript验证框架示例【附源码下载】
May 31 Javascript
微信自定义分享链接信息(标题,图片和内容)实现过程详解
Sep 04 Javascript
为nuxt项目写一个面包屑cli工具实现自动生成页面与面包屑配置
Sep 29 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不用第三变量交换2个变量的值的解决方法
2013/06/02 PHP
php实现粘贴截图并完成上传功能
2015/05/17 PHP
php面向对象之反射功能与用法分析
2017/03/29 PHP
Thinkphp结合AJAX长轮询实现PC与APP推送详解
2017/07/31 PHP
js计算精度问题小结
2013/04/22 Javascript
js函数模拟显示桌面.scf程序示例
2014/04/20 Javascript
node.js中的querystring.parse方法使用说明
2014/12/10 Javascript
js使用Array.prototype.sort()对数组对象排序的方法
2015/01/28 Javascript
JS处理json日期格式化问题
2015/10/01 Javascript
JavaScript使用DeviceOne开发实战(四)仿优酷视频应用
2015/12/02 Javascript
JavaScript Array对象详解
2016/03/01 Javascript
关于JS中的apply,call,bind的深入解析
2016/04/05 Javascript
JavaScript trim 实现去除字符串首尾指定字符的简单方法
2016/12/27 Javascript
jQuery仿IOS弹出框插件
2017/02/18 Javascript
整理关于Bootstrap过渡动画的慕课笔记
2017/03/29 Javascript
基于Vue的延迟加载插件vue-view-lazy
2018/05/21 Javascript
bootstrapTable+ajax加载数据 refresh更新数据
2018/08/31 Javascript
[03:15]2014DOTA2国际邀请赛 专访国士无双信心满满
2014/07/12 DOTA
[45:50]完美世界DOTA2联赛PWL S3 CPG vs Forest 第二场 12.16
2020/12/17 DOTA
Python运算符重载详解及实例代码
2017/03/07 Python
Python数据可视化正态分布简单分析及实现代码
2017/12/04 Python
python http接口自动化脚本详解
2018/01/02 Python
利用Python复制文件的9种方法总结
2019/09/02 Python
python上传时包含boundary时的解决方法
2020/04/08 Python
利用python+ffmpeg合并B站视频及格式转换的实例代码
2020/11/24 Python
Python爬虫之Selenium中frame/iframe表单嵌套页面
2020/12/04 Python
Python 可视化神器Plotly详解
2020/12/26 Python
挪威户外活动服装和装备购物网站:Bergfreunde挪威
2016/10/20 全球购物
Airbnb爱彼迎官网:成为爱彼迎房东,赚取收入
2019/03/14 全球购物
武汉英思工程科技有限公司&ndash;ORACLE面试测试题目
2012/04/30 面试题
最经典的商业地产项目广告词
2014/03/13 职场文书
学生党员批评与自我批评
2014/10/15 职场文书
升职自我推荐信范文
2015/03/25 职场文书
劳动合同变更协议书范本
2019/04/18 职场文书
python基于OpenCV模板匹配识别图片中的数字
2021/03/31 Python
Java数组与堆栈相关知识总结
2021/06/29 Java/Android