In today’s front-end interviews, vue’s two-way data binding has become a very easy point to learn, if not write it out on the spot, then at least explain how it works. In this article I will write an example of two-way data binding modeled after Vue, called myVue. Combined with notes, I hope to make you gain.

1, the principle of

The principle of Vue’s two-way data binding is well understood, mainly through the defineProperty attribute of the Object Object, rewriting the set and get functions of data to achieve, here does not do too much description of the principle, mainly to achieve an instance. In order to make the code more clear, only the most basic content will be implemented here, mainly implement v-model, V-bind and V-click three commands, other commands can also be supplemented.

Add an image from the web

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>
Copy the code

Contains:

1. An input using the V-model directive 2. A button using the V-click directive 3. An H3, using the V-bind directive.Copy the code

We’ll end up using our two-way data binding in a vUe-like fashion, adding annotations in conjunction with our data structure

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

First we need to define a myVue constructor:

function myVue(options) {}Copy the code

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, and methods
    this.$el = document.querySelector(options.el); // this.$el is the Element whose id is app
    this.$data = options.data; // this.$data = {number: 0}
    this.$methods = options.methods;  // this.$methods = {increment: function(){}}
  }
Copy the code

Next, the _obverse function is implemented to process data and rewrite data’s set and get functions

And change the _init function

 myVue.prototype._obverse = function (obj) { // obj = {number: 0}
    var value;
    for (key in obj) {  // Iterate over the obj object
      if (obj.hasOwnProperty(key)) {
        value = obj[key]; 
        if (typeof value === 'object') {  // If the value is still an object, the process is iterated
          this._obverse(value);
        }
        Object.defineProperty(this.$data, key, {  / / key
          enumerable: true.configurable: true.get: function () {
            console.log(` access${value}`);
            return value;
          },
          set: function (newVal) {
            console.log(Updated `${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);
  }
Copy the code

Next we write an instruction class Watcher that binds the update function to the DOM element

function Watcher(name, el, vm, exp, attr) {
    this.name = name;         // Instruction name, such as text node, set to "text"
    this.el = el;             // The DOM element corresponding to the directive
    this.vm = vm;             // The myVue instance to which the directive belongs
    this.exp = exp;           // The value corresponding to the instruction, such as "number"
    this.attr = attr;         // The value of the binding property, in this case "innerHTML"

    this.update();
  }

  Watcher.prototype.update = function () {
    this.el[this.attr] = this.vm.$data[this.exp]; H3.innerHTML = this.data. Number; This update function is triggered when the number changes, ensuring that the corresponding DOM content is updated.
  }
Copy the code

Update _init function and _obverse function

myVue.prototype._init = function (options) {
    / /...
    this._binding = {};   //_binding holds the mapping between model and View, which is the Watcher instance we defined earlier. When the Model changes, we will trigger an update of the instruction classes in it, ensuring that the View is updated in real time
    / /...
  }
 
  myVue.prototype._obverse = function (obj) {
    / /...
      if (obj.hasOwnProperty(key)) {
        this._binding[key] = {    // According to previous data, _binding = {number: _directives: []}
          _directives: []
        };
        / /...
        var binding = this._binding[key];
        Object.defineProperty(this.$data, key, {
          / /...
          set: function (newVal) {
            console.log(Updated `${newVal}`);
            if(value ! == newVal) { value = newVal; binding._directives.forEach(function (item) {  // When number changes, trigger an update of the bound Watcher class in _binding[number]._directivesitem.update(); })}})}}}Copy the code

So how do you bind the View to the Model? Next we define a _compile function that parses our instructions (V-bind, V-model, V-clickde), etc., and binds the View to the model in the process.

 myVue.prototype._init = function (options) {
   / /...
    this._complie(this.$el);
  }
 
myVue.prototype._complie = function (root) {root is the Element with id app, which is our root Elementvar _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 iterated over and processed
        this._complie(node);
      }

      if (node.hasAttribute('v-click')) {  Increment increment increment increment increment increment increment increment increment increment increment increment increment increment
        node.onclick = (function () {
          var attrVal = nodes[i].getAttribute('v-click');
          return _this.$methods[attrVal].bind(_this.$data);  // Bind keeps the scope of data consistent with the scope of method}) (); }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']. _cache = [a Watcher instance]
           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; // Make the value of number consistent with the value of node. Bidirectional binding is implemented
          }
        })(i));
      } 

      if (node.hasAttribute('v-bind')) { // If we have v-bind, we can update node to the value of number in data
        var attrVal = node.getAttribute('v-bind');
        _this._binding[attrVal]._directives.push(new Watcher(
          'text',
          node,
          _this,
          attrVal,
          'innerHTML'))}}}Copy the code

So far, we have implemented a simple vUE bidirectional binding function, including v-bind, V-model, V-click three instructions. The effect is shown below.

Attach the entire 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"> </button> </form> <h3 V-bind ="number"></h3> <form> <input type="text" V-model ="count"> <button Type ="button" v-click="incre"> add </button> </form> <h3 v-bind="count"></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 _this = this; Object.keys(obj).forEach(function (key) { if (obj.hasOwnProperty(key)) { _this._binding[key] = { _directives: [] }; console.log(_this._binding[key]) var 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(' ${key} gets ${value} '); return value; }, set: function (newVal) {console.log(' ${key} updates ${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 name, such as text node, set to "text" this.el = el; // The DOM element corresponding to the directive this.vm = vm; // this. Exp = exp; Attr = "number" this.attr = attr; // The binding property 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, count: 0, }, methods: { increment: function() { this.number ++; }, incre: function() { this.count ++; } } }) } </script>Copy the code

If you like, please follow me on Github and give me a Star, I will share some JS knowledge regularly, ^_^