vue双向绑定代码实现

vue数据双向绑定是通过数据劫持结合发布-订阅模式实现的,具体不再赘述,一图以蔽之:

vue双向绑定代码实现_第1张图片

下面就展示一下具体实现:

监听器:劫持并监听所有属性,如果数据变化,通知订阅器。

/**
 * 监听器
 * @param {Object} data vue数据对象
 */
 function observe(data) {
  if(!data || typeof data != 'object') {
    return ;
  }
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key]);
  })
}

function defineReactive(data, key, val) {
  observe(val);
  var dep = new Dep();
  Object.defineProperty(data,key,{
    configurable: true,
    enumerable: true,
    set: function(newVal) {
      val = newVal; // val默认变量
      console.log('属性:' + key + '已经被监听,现在的值为:'+ newVal);
      dep.notify();
    },
    get: function() {
      if (Dep.target) {
        dep.subs.push(Dep.target);
      }
      return val;
    }
  })
}

订阅器:收集订阅者,如果收到数据变化,通知订阅者。

/**
 * 订阅器
 */
function Dep() {
  this.subs = [];
  this.target = null;
}

Dep.prototype = {
  constructor: Dep,
  addSub: function(sub) { // 添加订阅者
    this.subs.push(sub);
  },
  notify: function() { // 通知订阅者
    this.subs.forEach(function(sub) {
      sub.update();
    })
  }
}

订阅者:如果收到数据变化,调用相应回调函数,更新视图

/**
 * 订阅者
 * @param {Object} vm vue对象
 * @param {String} exp 属性名
 * @param {Function} cb 回调函数
 */
function Watcher(vm, exp, cb) {
  this.vm = vm;
  this.exp = exp;
  this.cb = cb;
  this.value = this.get();
}

Watcher.prototype = {
  constructor: Watcher,
  update: function() {
    this.run();
  },
  run: function() {
    var newVal = this.vm.data[this.exp];
    var oldVal = this.value;
    if(newVal != oldVal) {
      this.cb.call(this.vm, newVal, oldVal);
    }
  },
  get: function() {
    Dep.target = this;
    var value = this.vm.data[this.exp];
    Dep.target = null;
    return value;
  }
}

到这为止,已经可以实现一个简单的双向绑定了,让我们来测试一下:


  
    
hello word

 

最后,我们来实现一个编译器

编译器:扫描和解析每个节点元素,替换模版数据,绑定事件监听函数,初始化订阅者

/**
 * 编译器
 * @param {String} el 根元素
 * @param {Object} vm vue对象
 */
function Compile(el, vm) {
  this.el = document.querySelector(el);
  this.vm = vm;
  this.fragment = null;
  this.init();
}

Compile.prototype = {
  constructor: Compile,
  init: function() {
    if (this.el) {
      this.fragment = this.nodeToFragment(this.el); // 移除页面元素生成文档碎片
      this.compileElement(this.fragment); // 编译文档碎片
      this.el.appendChild(this.fragment);
    } else {
      console.log('DOM Selector is not exist');
    }
  },
  /**
   * 页面DOM节点转化成文档碎片
   */
  nodeToFragment: function(el) {
    var fragment = document.createDocumentFragment();
    var child = el.firstChild;
    while(child) {
      fragment.appendChild(child); // append后,原el上的子节点被删除了,挂载在文档碎片上
      child = el.firstChild;
    }
    return fragment;
  },
  /**
   * 编译文档碎片,遍历到当前是文本节点,则编译文本节点;如果当前是元素节点,并且存在子节点,则继续递归遍历
   */
  compileElement: function(fragment) {
    var childNodes = fragment.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
      var reg = /\{\{\s*((?:.|\n)+?)\s*\}\}/g;
      var text = node.textContent;

      if (self.isElementNode(node)) {
        self.compileAttr(node);
      } else if (self.isTextNode(node) && reg.test(text)) { // test() 方法用于检测一个字符串是否匹配某个模式
        reg.lastIndex = 0
        self.compileText(node, reg.exec(text)[1]); // exec() 方法用于检索字符串中的正则表达式的匹配
      }

      if (node.childNodes && node.childNodes.length) { // 递归遍历
        self.compileElement(node);
      }
    })
  },
  /**
   * 编译属性
   */
  compileAttr: function(node) {
    var self = this;
    var nodeAttrs = node.attributes;

    Array.prototype.forEach.call(nodeAttrs, function(attr) {
      var attrName = attr.name; // attrName是DOM属性名,而exp是vue对象属性名
      
      if (self.isDirective(attrName)) { // 只对vue本身指令进行操作
        var exp = attr.value; // 属性名或函数名
        if (self.isOnDirective(attrName)) { // v-on指令
          self.compileOn(node, self.vm, attrName, exp);
        } else if (self.isBindDirective(attrName)) { // v-bind指令
          self.compileBind(node, self.vm, attrName, exp);
        } else if (self.isModelDirective(attrName)) { // v-model
          self.compileModel(node, self.vm, attrName, exp);
        }
        
        node.removeAttribute(attrName);
      }
    })
  },
  /**
   * 编译v-on指令
   */
  compileOn: function(node, vm, attrName, exp) {
    var onReg = /^v-on:|^@/;
    var eventType = attrName.replace(onReg, '');
    var cb = vm.methods[exp];

    node.addEventListener(eventType, cb.bind(vm), false);
  },
  /**
   * 编译v-bind指令
   */
  compileBind: function(node, vm, attrName, exp) {
    var bindReg = /^v-bind:|^:/;
    var attr = attrName.replace(bindReg, '');
    
    node.setAttribute(attr, vm.data[exp]);

    new Watcher(vm, exp, function(val) {
      node.setAttribute(attr, val);
    });
  },
  /**
   * 编译v-model指令
   */
  compileModel: function(node, vm, attrName, exp) {
    var self = this;
    var modelReg = /^v-model/;
    var attr = attrName.replace(modelReg, '');
    var val = vm.data[exp];
    
    self.updateModel(node, val); // 初始化视图

    new Watcher(vm, exp, function(value) { // 添加一个订阅者到订阅器
     self.updateModel(node, value);
    });

    node.addEventListener('input', function(e) { // 绑定input事件
      var newVal = e.target.value;
      if (val == newVal) {
        return;
      }
      self.vm.data[exp] = newVal;
    }, false);
  },

  /**
   * 属性是否是vue指令,包括v-xxx:,:xxx,@xxx
   */
  isDirective: function(attrName) {
    var dirReg = /^v-|^:|^@/;
    return dirReg.test(attrName);
  },
  /**
   * 属性是否是v-on指令
   */
  isOnDirective: function(attrName) {
    var onReg = /^v-on:|^@/;
    return onReg.test(attrName);
  },
  /**
   * 属性是否是v-bind指令
   */
  isBindDirective: function(attrName) {
    var bindReg = /^v-bind:|^:/;
    return bindReg.test(attrName);
  },
  /**
   * 属性是否是v-model指令
   */
  isModelDirective: function(attrName) {
    var modelReg = /^v-model/;
    return modelReg.test(attrName);
  },
  /**
   * 编译文档碎片节点文本,即对标记替换
   */
  compileText: function(node, exp) {
    var self = this;
    var initText = this.vm.data[exp];

    this.updateText(node, initText); // 初始化视图

    new Watcher(this.vm, exp, function(val) {
      self.updateText(node, val); // node?
    });
  },
  /**
   * 更新文本节点
   */
  updateText(node, val) {
    node.textContent = typeof val == 'undefined'? '': val;
  },
  updateModel(node, val, oldVal) {
    node.value = typeof val == 'undefined'? '': val;
  },
  /**
   * 判断元素节点
   */
  isElementNode(node) {
    return node.nodeType == 1;
  },
  /**
   * 判断文本节点
   */
  isTextNode(node) {
    return node.nodeType == 3;
  }
}

有了解析器,我们就可以测试完整版的双向绑定了!


  
    
    
  
  
    

{{ name }}

大功告成!

 

参考:https://segmentfault.com/a/1190000013276124

https://www.cnblogs.com/canfoo/p/6891868.html

你可能感兴趣的:(vue双向绑定代码实现)