你尚未登录,仅允许查看本站部分内容。请登录使用邀请码注册
Fonda

JavaScript数据双向绑定的简单演示 1个回复 @ Javascript

Fonda 发布于 1 年前

对于前端,有时候需要实现视图层和数据层的双向绑定(two-way-binding), 例如当前流行的各种框架类库:Vue.jsAngular.jsReact.js。 然而,他们最原始的实现方式其实都相对比较简单,只不过是后来随着各种Bug的出现,才一如滚雪球般地被不断优化和壮大。

所以,不要畏惧,多多学习并摄取它们的精华。

这里, 我也希望通过简单的思路让你对数据的双向绑定有个大概了解,然后去看各种MVVM框架中对于数据双向绑定的实现才不会一头雾水。

首先 先复习个知识点

Nodes 和 Elements 的区别

Element继承了Node,而Element是众多Node类型中的其中一个, nodeType === 1, 所以,属于Node的属性可以用于Element,但Element的属性无法用于Node,听起来好拗口,看一下代码吧.

<div class="demo">
  <p>大</p>
  <p>轰</p>        
</div>

-----------------------------------

var el = document.querySelector('.demo');

// true
console.log(el.children[0] instanceof Node);
// true
console.log(el.children[0] instanceof Element);

// true
console.log(el.childNodes[0] instanceof Node);
// false
console.log(el.childNodes[0] instanceof Element);

// undefined
console.log(typeof el.childNodes[0].children);
// object
console.log(typeof el.childNodes[0].childNodes);

什么是数据和视图的双向绑定?

双向绑定对于理解Flux等架构所提倡的单向数据流特性有很好的帮助, 简单点说,就是将数据的变化绑定到UI, 同时UI的变化又和数据同步。这样一来,假如你是全站Ajax,你不用去管数据对UI的影响,同时也不用去管UI变化造成的数据变化,统一了数据操作的入口,非常方便维护。

总而言之,双向数据绑定的底层实现大概有两种:

1、手动绑定,同时使用dirty check去循环监听。(Angular.js为代表)

2、前端数据劫持。(使用defineProperty, Vue.js貌似就是使用这种)

手动绑定 循环监听触发 (一)

这种方法的实现类似订阅者模式,实现思路是通过DOM的keydown keyup keypress change等事件触发dirty check(当然你也可以用setTimeout),然后循环监听并将value写入某实例变量里面,同时更新DOM。

若是有时间,推荐去看一下Angular.js文档中关于digest / $watch的介绍。

如果使用jQuery实现起来更加简单明了。 传送门在此

<div class="app">
  <input type="text" h-model="test">

  <p h-text="test"></p>
</div
var Hue = function(opt) {
  var el = document.querySelector(opt.el);
  var data = opt.data || {};

  return new Hue.prototype.init(el, data);
};

Hue.prototype = {
  constructor: Hue,

  // 要实现类数组,必须要有这两个属性
  length: 0,
  splice: [].splice,

  init: function(el, data) {
    this.el = el;
    this.data = data;

    // 返回所有含有``h-*``属性的元素
    this.elems = this.bindNodesArr(el);

    // 初始化,同时防止位空时出现undefined
    this.digest(true);

    return this;
  },

  bindNodesArr: function(el) {
    var arr = [],
        _this = this,
        childs = el.childNodes,
        len = childs.length,
        i, j,
        attr,
        lenAttr;

    if (childs.length) {
      for (i = 0; i < len; i++) {
        el = childs[i];

        if (el.nodeType === 1) {
          for (j = 0, lenAttr = el.attributes.length; j < lenAttr; j++) {

            attr = el.attributes[j];
            if (attr.nodeName.indexOf('h-') >= 0) {
              arr.push(el);
              switch (attr.nodeName.slice(2)) {

                // 监听model绑定的事件
                case 'model':
                  if (document.addEventListener) {
                    el.addEventListener('keydown', function() {
                      _this.digest();
                    }, false);
                  } else {
                    el.attachEvent('onkeydown', function() {
                      _this.digest();
                    }, false);
                  }
                  break;

                default:
                  break;
              }

              break;
            }
          }

          arr = arr.concat(this.bindNodesArr(el));
        }
      }
    }

    return arr;
  },

  digest: function(bool, ms) {
    var i, j,
        len, len2;

    var elems = this.elems,
        data = this.data;

    bool = bool === true ? true : false;
    ms = ms || 0;

    // 这里不能传参bool/ms, 否则bool和ms都会为undefined,因为setTimeout里的function作用域是指向window的
    setTimeout(function() {
      for (i = 0, len = elems.length; i < len; i++) {
        el = elems[i];
        attrs = el.attributes;
        for (j = 0, len2 = attrs.length; j < len2; j++) {
          var attr = attrs[j];
          if (attr.nodeName.indexOf('h-') >= 0) {

            switch (attr.nodeName.slice(2)) {
              case 'model':
                if (bool) {
                  el.value = data[attr.nodeValue] || '';

                  // dirty check
                } else if (el.value !== data[attr.nodeValue]) {
                  data[attr.nodeValue] = el.value || '';
                }
                break;

              case 'text':

                // dirty check
                if (el.innerHTML !== data[attr.nodeValue]) {
                  el.innerHTML = data[attr.nodeValue] || '';
                }
                break;

              default:
                console.error('Error: There is only h-model and h-text !');
                break;
            }
          }
        }
      }
    }, ms);
  }
}

// 参考jQuery ...
Hue.prototype.init.prototype = Hue.prototype;


// test...
Hue({
  el: '.app',
  data: {
    demo: '大轰'
  }
});

前端数据劫持 (二)

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

<div class="app">
  <input type="text" h-model="test">

  <p h-text="test"></p>
</div
(function(window, undefined) {

  var Hue = function(opt) {
    var el = document.querySelector(opt.el);
    var data = opt.data || {};

    return Hue.prototype.init(el, data);
  };

  Hue.prototype = {
    constructor: Hue,

  // 要实现类数组,必须要有这两个属性
    length: 0,
    splice: [].splice,

    init: function(el, data) {
      this.el = el;
      this.data = data;

      // 返回所有含有``h-*``属性的元素
      this.elems = this.bindNodesArr(el);

      this.bindText();
      this.bindModel();

      return this;
    },

    // 返回所有含有``h-*``属性的元素 [array]
    bindNodesArr: function(el) {
      var arr = [],
          childs = el.childNodes,
          len = childs.length,
          i, j,
          attr,
          lenAttr;

      if (len) {
        for (i = 0; i < len; i++) {
          el = childs[i];

          if (el.nodeType === 1) {
            for (j = 0, lenAttr = el.attributes.length; j < lenAttr; j++) {
              attr = el.attributes[j];
              if (attr.nodeName.indexOf('h-') >= 0) {
                arr.push(el);
                break;
              }
            }

            arr = arr.concat(this.bindNodesArr(el));
          }
        }
      }
      return arr;
    },

    // **前端数据劫持**
    defineObj: function(obj, prop, value) {
      var _value = value || '',
          _this = this;

      try {
        Object.defineProperty(obj, prop, {
          get: function() {
            return _value;
          },
          set: function(newVal) {
            _value = newVal;
            _this.bindText();
          },
          enumerable: true,
          configurable: true
        });
      } catch (error) {

      // IE8+ 才开始支持defineProperty,这也是Vue.js不支持IE8的原因
        console.log("Browser must be IE8+ !");
      }
    },

    bindModel: function() {
      var modelDOMs = this.el.querySelectorAll('[h-model]'),
          lenModel = modelDOMs.length;

      var _this = this,
          i,

      // h-model属性值
      propModel;

      for (i = 0; i < lenModel; i++) {
        propModel = modelDOMs[i].getAttribute('h-model');

        // 因为define model后, model值会为空显得不那么友好...  所以加这一段
        // tofix.........
        modelDOMs[i].value = this.data[propModel] || '';

      // 前端数据劫持
        this.defineObj(this.data, propModel);
        if (document.addEventListener) {
        // 这里不能是keydown, 否则model和text会有一个字符差bug,因为keydown时,e.target.value还未变化
          modelDOMs[i].addEventListener('keyup', function(e) {
            e = e || window.event;
            _this.data[propModel] = e.target.value;
          }, false);
        } else {
          modelDOMs[i].attachEvent('onkeyup', function(e) {
            e = e || window.event;
            _this.data[propModel] = e.target.value;
          }, false);
        }

      }

    },
    bindText: function() {
      var textDOMs = this.el.querySelectorAll('[h-text]'),
          lenText = textDOMs.length,
          prppText,
          j;

      for (j = 0; j < lenText; j++) {
        propText = textDOMs[j].getAttribute('h-text');

        textDOMs[j].innerHTML = this.data[propText] || '';
      }
    }
  }

  Hue.prototype.init.prototype = Hue.prototype;
  window.Hue = Hue;

})(window);


// test...
Hue({
  el: '.app',
  data: {
  demo: '大轰大轰'
  }
});

GitHub

多多少少会有些逻辑错误,还望大神们多多指点。

参考资料:

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

谈谈JavaScript中的双向数据绑定

非常简单的js双向数据绑定框架(三)

  • vczhan

    使用Object.defineProperty实现的方式真是优雅啊

    #1
登录后回复,如无账号,请使用邀请码注册