Summary of three ways to realize data bidirectional binding in javascript

  • 2021-08-03 08:13:14
  • OfStack

Bi-directional binding method of front-end data

The front-end view layer and data layer sometimes need to implement bidirectional binding (two-way-binding), such as mvvm framework, data-driven view, view state machine, etc. Several current mainstream data bidirectional binding frameworks are studied and summarized. At present, there are mainly three kinds of data bidirectional binding.

1. Manual binding

The older implementation mode is a bit like the observer programming mode. The main idea is to define get and set methods (of course, there are other methods) on the data object, manually call get or set data when calling, and start the rendering operation of UI layer after changing the data; The scene of data change driven by view is mainly applied to input, select, textarea and other elements. When UI layer changes, the data of data layer is changed by monitoring change, keypress, keyup and other events of dom. The whole process is completed by function call.


<!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(){    
      /**
       *  Scan node properties with instructions 
       */
      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){
            /**
             *  Invoke the property directive, where you can use data change detection 
             */
            command[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
            elem.command.push(attr.nodeName.slice(2));
          }
        }
      }
    }

    /**
     *  Scan after setting data 
     */
    function mvSet(key, value){
      data[key] = value;
      scan();
    }
    /**
     *  Data binding listening 
     */
    elems[1].addEventListener('keyup', function(e){
      mvSet('value', e.target.value);
    }, false);

    scan();

    /**
     *  Change Data Update View 
     */
    setTimeout(function(){
      mvSet('value', 'fuck');
    },1000)

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

2. Dirty inspection mechanism

angularjs, a typical mvvm framework, is used as a representative, and angular updates the operation of UI layer by checking dirty data. There are a few things to know about the dirty detection of angular:-The dirty detection mechanism does not use timing detection. -The timing of dirty detection is when the data changes. -angular encapsulates the commonly used dom events, xhr events, etc., and triggers the digest process entering angular. -In the digest process, all watcher are checked by traversing from rootscope. (The specific design of angular can be seen in other documents, only data binding is discussed here), then let's look at how to do dirty detection: mainly through the set data to find all the elements related to the data, and then compare the data changes, and if it changes, carry out instruction operations


<!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) {
    /**
     *  Scan node properties with instructions 
     */
    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) {
          /**
           *  Invoke attribute instructions 
           */
          var dataKey = elem.getAttribute('ng-bind') || undefined;
          /**
           *  Initialize data 
           */
          command[attr.nodeValue].call(elem, data[dataKey]);
          elem.command[attr.nodeValue] = data[dataKey];
        }
      }
    }
  }

  /**
   *  Dirty cycle detection 
   * @param {[type]} elems [description]
   * @return {[type]}    [description]
   */
  var digest = function(elems) {
    /**
     *  Scan node properties with instructions 
     */
    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) {
          /**
           *  Invoke attribute instructions 
           */
          var dataKey = elem.getAttribute('ng-bind') || undefined;

          /**
           *  Dirty data detection, if the data changes, re-execute instructions, otherwise skip 
           */
          if(elem.command[attr.nodeValue] !== data[dataKey]){

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

  /**
   *  Initialization data 
   */
  scan(elems);

  /**
   *  It can be understood as data hijacking monitoring 
   */
  function $digest(value){
    var list = document.querySelectorAll('[ng-bind='+ value + ']');
    digest(list);
  }

  /**
   *  Input box data binding listening 
   */
  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';
    /**
     *  Ask here what to execute $digest The key here is that you need to call manually $digest Method to start dirty detection 
     */
    $digest('value');
  }, 2000)

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

3. Front-end data hijacking (Hijacking)

The third method is the data hijacking method used by avalon and other frameworks. The basic idea is to use Object. defineProperty to monitor the attributes get and set of data objects, and call the instructions of nodes when there are data reading and assignment operations, so it is OK to use the most common = equal sign assignment. The specific implementation is as follows:


<!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() {
    /**
     *  Scan node properties with instructions 
     */
    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) {
          /**
           *  Invoke attribute instructions 
           */
          command[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]);
          elem.command.push(attr.nodeName.slice(2));

        }
      }
    }
  }

  var bValue;
  /**
   *  Define property setting hijacking 
   */
  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.");
    }
  }
  /**
   *  Initialization data 
   */
  scan();

  /**
   *  It can be understood as data hijacking monitoring 
   */
  defineGetAndSet(data, 'value');

  /**
   *  Data binding listening 
   */
  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>

However, it is worth noting that defineProperty supports browsers above IE8. Here, __defineGetter__ and __defineSetter__ can be used for compatibility. However, for browser compatibility reasons, defineProperty can be used directly. As for IE8 browser, we still need to use other methods to do hack. The following code can perform hack on IE8, and IE8 is supported on defineProperty. For example, use es5-shim. js. (Ignored by browsers below IE8)

4. Summary

First of all, the example here is only a simple implementation, and readers can deeply feel the similarities and differences of the three ways. The complex framework is snowballing through such basic ideas.


Related articles: