The concept of two-way data binding is familiar. In short, data changes update views, and views change update data. In order to achieve this effect, in Vue, a combination of data hijacking and publishing subscriber pattern is adopted to achieve it.

Implement data hijacking with Object.defineProperty() to listen for changes to the data.

The published subscriber pattern is implemented through the publisher Dep() subscriber Watcher to achieve the decoupling of updates between views and data.

There are many examples on the web of how to implement a simple two-way data binding, but I won’t list them. Here I draw a picture of the principle of bidirectional binding as I understand it:

Then, I posted the code in modules (I didn’t write the code, I learned it from others).

observer()

function observer(data) {
  if(! data ||typeofdata ! = ='object') {
    return;
  }
  Object.keys(data).forEach(key= > {
    var dep = new Dep();
    var value = data[key];

    observer(value);
    Object.defineProperty(data, key, {
      configurable: true.enumerable: true,
      get() {
        if(Dep.target) {
          dep.addSub(Dep.target)
        }
        return value;
      },
      set(newValue) {
        if (value === newValue) {
          return; } value = newValue; dep.notify(); }})})}Copy the code

Dep()

function Dep() {
  this.subs = [];
}
Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  },
  notify: function() {
    this.subs.forEach(sub= > {
      sub.update();
    });
  }
}
Dep.target = null;
Copy the code

Watcher

function Watcher(vm, exp, cb) {
  this.vm = vm;
  this.exp = exp;
  this.cb = cb;
  this.value = this.get();
}
Watcher.prototype = {
  get: function() {
    Dep.target = this;
    var value = this.vm._data;
    this.exp.split('. ').forEach(key= > {
      value = value[key]
    })
    Dep.target = null;
    return value;
  },
  update: function() {
    this.run();
  },
  run: function() {
    var value = this.vm._data;
    this.exp.split('. ').forEach(key= > {
      value = value[key]
    })
    if(value ! = =this.value) {
      this.value = value;
      this.cb.call(this.vm, value); }}}Copy the code

Compile

function Compile(el, vm) {
  this.el = document.querySelector(el);
  this.vm = vm;
  this.init();
}
Compile.prototype = {
  init: function() {
    this.fragment = this.node2Fragment(this.el);
    this.compile(this.fragment);
    this.el.appendChild(this.fragment);
  },
  node2Fragment: function(el) {
    var fragment = document.createDocumentFragment();
    var child = el.firstChild;
    while(child) {
      fragment.appendChild(child);
      child = el.firstChild;
    }
    return fragment;
  },
  compile: function(el) {
    var childNodes = el.childNodes;
    var that = this;
    Array.prototype.slice.call(childNodes).forEach(node= > {
      // if(that.isElementNode(node)) {
      // that.compileElement(node)
      // }
      if (that.isTextNode(node)) {
        that.compileText(node)
      }
      if(node.childNodes && node.childNodes.length) {
        that.compile(node)
      }
    })
  },

  compileElement: function(node) {
    var attributes = node.attributes;
    var that = this;

    Array.prototype.forEach.call(attributes, function(attr) {
      if(that.isDirective(attr)) {
        if(that.isModelDirective) {
          that.compileModel()
        }
        if(that.isHtmlDirective) {
          that.compileHtml()
        }
        if(that.isEventDirective) {
          that.compileEvnet()
        }
      }

      // if(that.isShortEventDirective(attr)) {
      // that.compileEvnet()
      // }
    });
  },

  compileText: function(node) {
    var that = this;
    var reg = / \ {\ {(. *) \} \} /;
    if(reg.test(node.textContent)) {
      var exp = reg.exec(node.textContent)[1].trim()
      var val = that.vm._data;
      exp.split('. ').forEach(key= > {
        val = val[key]
      })
      that.updateText(node, val);
      new Watcher(that.vm, exp, function(value) {
        that.updateText(node, value)
      })
    }
  },

  compileModel: function() {},
  compileHtml: function() {},
  compileEvnet: function() {},
  
  updateText: function(node, value) {
    node.textContent = value
  },

  isDirective: function(attr) {
    return attr.indexof('v-') = = =0;
  },
  isEventDirective: function(attr) {
    return attr.indexof('on:') = = =0;
  },
  isShortEventDirective: function(attr) {
    return attr.indexof(The '@') = = =0;
  },
  isHtmlDirective: function(dir) {
    return dir.indexof('html') = = =0;
  },
  isModelDirective: function(dir) {
    return dir.indexof('model') = = =0;
  },

  isElementNode: function(node) {
    return node.nodeType === 1;
  },
  isTextNode: function(node) {
    return node.nodeType === 3; }}Copy the code

Vue


function Vue(options = {}) {
  this.$options = options;
  this.$el = document.getElementById(options.el);
  this._data = options.data;
  let data = this._data;
  this._proxyData(options.data);

  observer(data);
  this.methods = options.methods;
  new Compile(options.el, this)
}
Vue.prototype = {
  _proxyData: function(data) {
    var that = this;
    Object.keys(data).forEach(key= > {
      Object.defineProperty(this, key, {
        configurable: true.enumerable: false,
        get() {
          returnthat._data[key]; }, set(newValue) { that._data[key] = newValue; },})})}}Copy the code

test

<div id="app">
    <div>
      <span>{{ msg }}</span>
      <br>
      <span>{{ f.name }}</span>
  </div>
</div>
Copy the code
var vm = new Vue({
  el: '#app'.data: {
    msg: '11'.f: {
      name: 1
    },
    dom: '<strong></strong>'
  },
  methods: {
    clickMe() {
      console.log(123); }}})Copy the code

Enter in the browser console

vm.msg = 333;
vm.f.name = 'xxxxx'
Copy the code

You can see the data change