Implementing Bidirectional Data Binding of Vue

Posted by alco19357 on Wed, 15 May 2019 02:44:08 +0200

Reproduced at https://segmentfault.com/a/1190000014274840

1, principle

The principle of Vue's two-way data binding is well understood, mainly through the definition property property property of the Object object, rewriting the set and get functions of data. Here, the principle is not described too much, but mainly to implement an example. In order to make the code clearer, only the most basic content can be realized here. The main three commands are v-model, v-bind and v-click. Other commands can be supplemented by themselves.

Add a picture on the Internet

2, implementation

The page structure is simple, as follows

<div id="app">
    <form>
      <input type="text"  v-model="number">
      <button type="button" v-click="increment">increase</button>
    </form>
    <h3 v-bind="number"></h3>
  </div>

Contain:

 1. An input, using the v-model instruction
 2. A button, using the v-click instruction
 3. An h3, using the v-bind instruction.

We'll end up using our two-way data binding in a vue-like way, adding annotations to our data structure

var app = new myVue({
      el:'#app',
      data: {
        number: 0
      },
      methods: {
        increment: function() {
          this.number ++;
        },
      }
    })

First we need to define a myVue constructor:

function myVue(options) {
  
}

To initialize the constructor, add a _init attribute to it

function myVue(options) {
  this._init(options);
}
myVue.prototype._init = function (options) {
    this.$options = options;  // options are the structures passed in when used above, including el,data,methods
    this.$el = document.querySelector(options.el); // El is # app, and this. $el is the Element element with id app.
    this.$data = options.data; // this.$data = {number: 0}
    this.$methods = options.methods;  // this.$methods = {increment: function(){}}
  }

Next, we implement the _obverse function, process the data, and rewrite the set and get functions of the data.

And transform the _init function

 myVue.prototype._obverse = function (obj) { // obj = {number: 0}
    var value;
    for (key in obj) {  //Traversing obj objects
      if (obj.hasOwnProperty(key)) {
        value = obj[key]; 
        if (typeof value === 'object') {  //If the value is still an object, traversal processing
          this._obverse(value);
        }
        Object.defineProperty(this.$data, key, {  //crux
          enumerable: true,
          configurable: true,
          get: function () {
            console.log(`Obtain${value}`);
            return value;
          },
          set: function (newVal) {
            console.log(`To update${newVal}`);
            if (value !== newVal) {
              value = newVal;
            }
          }
        })
      }
    }
  }
 
 myVue.prototype._init = function (options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;
   
    this._obverse(this.$data);
  }

Next, we write an instruction class, Watcher, to bind update functions to update DOM elements.

function Watcher(name, el, vm, exp, attr) {
    this.name = name;         //Instruction names, such as text nodes, are set to "text"
    this.el = el;             //DOM elements corresponding to instructions
    this.vm = vm;             //The myVue instance to which the instruction belongs
    this.exp = exp;           //The corresponding value of an instruction, such as "number"
    this.attr = attr;         //The bound attribute value, in this case "innerHTML"

    this.update();
  }

  Watcher.prototype.update = function () {
    this.el[this.attr] = this.vm.$data[this.exp]; //For example, H3.innerHTML = this.data.number; when the number changes, the update function is triggered to ensure that the corresponding DOM content is updated.
  }

Update _init function and _obverse function

myVue.prototype._init = function (options) {
    //...
    this._binding = {};   //_ binding preserves the mapping relationship between model and view, which is an example of Watcher we defined earlier. When the model changes, we trigger instruction class updates to ensure that the view can also be updated in real time.
    //...
  }
 
  myVue.prototype._obverse = function (obj) {
    //...
      if (obj.hasOwnProperty(key)) {
        this._binding[key] = {    // According to the previous data, _binding = {number: _directives: []}                                                                                                                                                  
          _directives: []
        };
        //...
        var binding = this._binding[key];
        Object.defineProperty(this.$data, key, {
          //...
          set: function (newVal) {
            console.log(`To update${newVal}`);
            if (value !== newVal) {
              value = newVal;
              binding._directives.forEach(function (item) {  // When the number changes, the update of the bound Watcher class in _binding[number]._directives is triggered
                item.update();
              })
            }
          }
        })
      }
    }
  }

So how do you bind view to model? Next, we define a _compile function to parse our instructions (v-bind,v-model,v-clickde), and bind view to model in the process.

 myVue.prototype._init = function (options) {
   //...
    this._complie(this.$el);
  }
 
myVue.prototype._complie = function (root) { root by id by app Of Element Element, our root element
    var _this = this;
    var nodes = root.children;
    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i];
      if (node.children.length) {  // All elements are traversed and processed
        this._complie(node);
      }

      if (node.hasAttribute('v-click')) {  // If there is a v-click attribute, we listen for its onclick event and trigger the increment event, number+.
        node.onclick = (function () {
          var attrVal = nodes[i].getAttribute('v-click');
          return _this.$methods[attrVal].bind(_this.$data);  //bind is to keep the scope of data consistent with that of method functions
        })();
      }

      if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) { // If there is a v-model attribute and the element is INPUT or TEXTAREA, we listen for its input event
        node.addEventListener('input', (function(key) {  
          var attrVal = node.getAttribute('v-model');
           //_ this._binding['number']._directives = a Watcher instance]
           // Where Watcher.prototype.update = function (){
           //    Node ['vaule']= _this. $data ['number']; this keeps the value of node consistent with number
           // }
          _this._binding[attrVal]._directives.push(new Watcher(  
            'input',
            node,
            _this,
            attrVal,
            'value'
          ))

          return function() {
            _this.$data[attrVal] =  nodes[key].value; // The value of number is consistent with the value of node, and bidirectional binding has been implemented.
          }
        })(i));
      } 

      if (node.hasAttribute('v-bind')) { // If there is a v-bind attribute, we just need to update the value of node to the value of number in data in time.
        var attrVal = node.getAttribute('v-bind');
        _this._binding[attrVal]._directives.push(new Watcher(
          'text',
          node,
          _this,
          attrVal,
          'innerHTML'
        ))
      }
    }
  }

So far, we have implemented a simple two-way binding function of vue, including three instructions: v-bind, V-model and v-click. The effect is as follows

Attach all the code, less than 150 lines

<!DOCTYPE html>
<head>
  <title>myVue</title>
</head>
<style>
  #app {
    text-align: center;
  }
</style>
<body>
  <div id="app">
    <form>
      <input type="text"  v-model="number">
      <button type="button" v-click="increment">increase</button>
    </form>
    <h3 v-bind="number"></h3>
  </div>
</body>

<script>
  function myVue(options) {
    this._init(options);
  }

  myVue.prototype._init = function (options) {
    this.$options = options;
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    this.$methods = options.methods;

    this._binding = {};
    this._obverse(this.$data);
    this._complie(this.$el);
  }
 
  myVue.prototype._obverse = function (obj) {
    var value;
    for (key in obj) {
      if (obj.hasOwnProperty(key)) {
        this._binding[key] = {                                                                                                                                                          
          _directives: []
        };
        value = obj[key];
        if (typeof value === 'object') {
          this._obverse(value);
        }
        var binding = this._binding[key];
        Object.defineProperty(this.$data, key, {
          enumerable: true,
          configurable: true,
          get: function () {
            console.log(`Obtain${value}`);
            return value;
          },
          set: function (newVal) {
            console.log(`To update${newVal}`);
            if (value !== newVal) {
              value = newVal;
              binding._directives.forEach(function (item) {
                item.update();
              })
            }
          }
        })
      }
    }
  }

  myVue.prototype._complie = function (root) {
    var _this = this;
    var nodes = root.children;
    for (var i = 0; i < nodes.length; i++) {
      var node = nodes[i];
      if (node.children.length) {
        this._complie(node);
      }

      if (node.hasAttribute('v-click')) {
        node.onclick = (function () {
          var attrVal = nodes[i].getAttribute('v-click');
          return _this.$methods[attrVal].bind(_this.$data);
        })();
      }

      if (node.hasAttribute('v-model') && (node.tagName == 'INPUT' || node.tagName == 'TEXTAREA')) {
        node.addEventListener('input', (function(key) {
          var attrVal = node.getAttribute('v-model');
          _this._binding[attrVal]._directives.push(new Watcher(
            'input',
            node,
            _this,
            attrVal,
            'value'
          ))

          return function() {
            _this.$data[attrVal] =  nodes[key].value;
          }
        })(i));
      } 

      if (node.hasAttribute('v-bind')) {
        var attrVal = node.getAttribute('v-bind');
        _this._binding[attrVal]._directives.push(new Watcher(
          'text',
          node,
          _this,
          attrVal,
          'innerHTML'
        ))
      }
    }
  }

  function Watcher(name, el, vm, exp, attr) {
    this.name = name;         //Instruction names, such as text nodes, are set to "text"
    this.el = el;             //DOM elements corresponding to instructions
    this.vm = vm;             //The myVue instance to which the instruction belongs
    this.exp = exp;           //The corresponding value of an instruction, such as "number"
    this.attr = attr;         //The bound attribute value, in this case "innerHTML"

    this.update();
  }

  Watcher.prototype.update = function () {
    this.el[this.attr] = this.vm.$data[this.exp];
  }

  window.onload = function() {
    var app = new myVue({
      el:'#app',
      data: {
        number: 0
      },
      methods: {
        increment: function() {
          this.number ++;
        },
      }
    })
  }
</script>



Topics: Attribute Vue less