vue雙向數據綁定原理圖(簡易)

雙向數據綁定的概念,相信你們都耳熟能詳,簡單來講,數據變化更新視圖,視圖變化更新數據。爲了實現這一效果,在 Vue 中,採用了 數據劫持結合發佈訂閱者模式 的方式來實現。html

經過 Object.defineProperty() 實現數據劫持,監聽數據的變化。node

經過 發佈者Dep() 訂閱者Watcher 實現發佈訂閱者模式,達到視圖與數據之間相互更新的解耦。瀏覽器

關於如何實現一個簡單的數據雙向綁定,網上有不少例子,我就不列舉了。我這裏將我理解的雙向綁定原理圖畫了一下:app

而後,我分模塊貼下代碼(代碼不是我寫的,我也是找了別人的學習)dom

observer()

function observer(data) {
  if(!data || typeof data !== '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();
      },
    })
  })
}

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;

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);
    }
  }
}

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('@') === 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;
  }
}

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() {
          return that._data[key];
        },
        set(newValue) {
          that._data[key] = newValue;
        },
      })
    })
  }
}

測試

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

在 瀏覽器控制檯 輸入學習

vm.msg = 333;
vm.f.name = 'xxxxx'

能夠看到數據變化了測試

相關文章
相關標籤/搜索