2

双向数据绑定的概念,相信大家都耳熟能详,简单来说,数据变化更新视图,视图变化更新数据。为了实现这一效果,在 Vue 中,采用了 数据劫持结合发布订阅者模式 的方式来实现。

通过 Object.defineProperty() 实现数据劫持,监听数据的变化。

通过 发布者Dep() 订阅者Watcher 实现发布订阅者模式,达到视图与数据之间相互更新的解耦。

关于如何实现一个简单的数据双向绑定,网上有很多例子,我就不列举了。我这里将我理解的双向绑定原理图画了一下:

然后,我分模块贴下代码(代码不是我写的,我也是找了别人的学习)

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'

可以看到数据变化了


zhangjinpei
103 声望6 粉丝

做一枚精致的前端er