直接上代码,如下:


<!DOCTYPE html>
<html>
<head>
	<title>vue-test</title>
</head>
<body>
  <div id="app">
    <span>title:</span><span v-text="text"></span><br>
    <span>Price:¥ </span><span v-text="price"></span><br>
    <span>Total:¥ </span><span v-text="total"></span><br>
    <button v-click="addPrice">
      加1
    </button>
    <button v-click="decPrice">
      减1
    </button>
  </div>

</body>
<script type="text/javascript">
  class Vue {
    constructor(options) {
      this.$options = options;
      this.$el = document.querySelector(options.el);
      this._data = typeof(options.data) == 'function'? options.data(): options.data;  // data可为function也可为object
      this._methods = options.methods;
      this._bindClick();

      Object.keys(this._data).forEach(key => this._proxy(key))
      observer(this._data)
      watch(this._render.bind(this), this._update.bind(this))
    }
    _proxy(key) {
      console.log('_proxy', key)
      const self = this
      Object.defineProperty(self, key, {
        configurable: true,
        enumerable: true,
        get: function proxyGetter () {
          console.log('_proxy get')
          return self._data[key]
        },
        set: function proxySetter (val) {
          console.log('_proxy set')
          self._data[key] = val
        }
      })
    }
    _bindClick() {
      console.log("_bindClick");
      var _this=this;
      let textDOMs=_this.$el.querySelectorAll('[v-click]');
      for (let i = 0; i < textDOMs.length; i++) {
        if (textDOMs[i].hasAttribute('v-click')) {
          textDOMs[i].onclick = (function () {
            let attrVal = textDOMs[i].getAttribute('v-click');
            return _this._methods[attrVal].bind(_this._data);
          })();
        }
      }
    }
    _update(target) {
      console.log("_update");
      this._render.call(this,target)
    }
    _render(target) {
      console.log("_render", target);
      let textDOMs=this.$el.querySelectorAll('[v-text]'), bindText;
      for(let i=0; i<textDOMs.length; i++){
        bindText=textDOMs[i].getAttribute('v-text');
        if(target == undefined){
          let data = this._data[bindText];
          if(data.toString()){
            textDOMs[i].innerHTML=data.toString();
          }
        }
        else if(target == bindText){
          let data = this._data[bindText];
          if(data.toString()){
            textDOMs[i].innerHTML=data.toString();
          }
        }
      }
    }
  }

  function observer(value){
    console.log('observer')
    Object.keys(value).forEach((key) => defineReactive(value, key, value[key]))
  }

  function defineReactive(obj, key, val) {
    const dep = new Dep()
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: ()=>{
        console.log('get,'+key+':', val)
        return val
      },
      set: newVal => {
        if(newVal === val)
          return
        console.log('set,'+key+'由:', val+',变更为:'+newVal)
        val = newVal
        dep.notify(key)
      }
    })
  }
  function watch(exp, cb){
    console.log('watch')
    Dep.target = new Watcher(cb);
    exp()
  }

  class Watcher {
    constructor(cb) {
      this.cb = cb
    }
  }

  class Dep {
    notify(target) {
      console.log('notify')
      Dep.target.cb(target)
    }
  }
  // Dep.target=null;

  var demo = new Vue({
    el: '#app',
    data() {
      return {
        text: "hello world",
        price: 0,
        total: 0,
      };
    },
    methods: {
      addPrice() {
        console.warn('addPrice')
        this.price = this.price + 1;
        this.total = this.price * 3.14159265768;
      },
      decPrice() {
        console.warn('decPrice')
        this.price = this.price -1;
        this.total = this.price * 3.14159265768;
      }
    }
  })
  setTimeout(function(){
    console.warn('setTimeout')
    demo.text = "hello new world"
  }, 2000)
</script>
</html>

输出结果如下:

vue的双向绑定.jpg

官方原理图:

VUE数据双向绑定.jpg