当前位置:首页 > 博客 >vue > 正文

vue的双向数据绑定原理分析

时间:2018-09-21 11:38   作者:admin   点击:

最开始认为双向绑定很神秘,后面接触了defineProperty,以为这就是双向绑定。其实还用到了很多东西(观察者模式、脏值检测等),我花时间自己写了一个简单的demo,当然有很多问题,这只是最简单的双向绑定.

我们分析下如何实现双向数据绑定,其实就是view=>model ,然后model => view 都对应关联上其实就达到我们的需求了

  1. 首先我们分析,我们需要实现的是view=>model,也就是视图变化了,数据也要变化

    过程如图:

1012670335-5ae1b53b68074_articlex.png

    先定义一个html结构,也就是视图

    输入的值为:{{name}}
  {{name}}

构造函数

// 整个双向绑定的构造函数
function Yue (options) {
  this.$el = options.el;
  this.$data = options.data;
    
    
  var elment = document.querySelector(this.$el);
  elment.appendChild(nodeToFragment(elment, this));
}

我们分析,这个结构我们其实就是想input输入的时候,对应的变量发生改变,当然我们也不能让页面看起来一直都是{{}},需要初始化视图

// 遍历dom
function nodeToFragment (node, vm) {
  var flag = document.createDocumentFragment();
  var child;
  while (child = node.firstChild) {
      compile(child, vm);
      if (child.firstChild) {
          var dom = nodeToFragment(child, vm);
          child.appendChild(dom);
      }
      flag.appendChild(child);
  }
 return flag
}

// 初始化编译
function compile (node, vm) {
  var type = node.nodeType;
  if (type == 1) {
      // node是元素节点
      var attr = node.attributes;
      [].slice.call(attr).forEach(function (item) {
          // item是属性节点
          if (item.name == 'v-model') {
              node.addEventListener('input', function (e) {
                  if (!vm.$data[item.nodeValue]) {
                      new Error('未定义该变量'+item.nodeValue);
                      return false;
                  }
                  
                  vm.$data[item.nodeValue] = e.target.value;
              })

              
             
              node.value = vm.$data[item.nodeValue]
              node.removeAttribute('v-model')
          }
      })
  }else if (type == 3) {
      var v = node.nodeValue;
      var reg = /\{\{(\w*)\}\}/g;
      if (reg.test(v)){
       
       
      }
      
      node.nodeValue = v.replace(reg, function (str, $1) {
          return vm.$data[$1];
      })
  }
}

   

这里有个注意点,可能有人有疑问?并没有看到把dom结构删除,只是做了添加

这就是appendChild(newChild)功能了, 如果newChild存在这个文档树,它会先删除原本的dom节点,再添加到新的节点中


这时你可以发现其实已经实现了view=>model的关联了,你在input输入一个变量,可以发现变量的对应的值已经发生变化了,只是没有在视图上反应出来


2.现在来实现model => view 也就是数据变化,去更新对应的dom

改变一个实例的数据本身不难,很简单,但是你要去更新dom,这时候,就发现不太好处理了,因为我们不可能又去遍历一次dom,而且这样也不一定能准确找到与变量绑定的dom

这时候我们需要另一个概念:观察者模式

也就是订阅和发布的概念,我们需要先去把变量和dom的关系对应上,这样我们改变变量的时候,就直接去通知所有的dom

//发布 统一管理
function Dep () {
  // 发布中心
  this.dep = []
}
// 添加订阅者
Dep.prototype.addDep = function (target, vm, name) {
  this.dep.push({node: target, vm: vm, name: name})
  // Dep.target = ''
}

Dep.prototype.update = function (value) {
  this.dep.forEach(function (item) {
    // 调用观察者的实例的update
    item.value = value;
    item.update()
  })
}

// 订阅者 具体实现
function Watch (node, vm, name) {
  Dep.target = this;
  this.node = node;
  this.vm = vm;
  this.name = name;
  this.value = vm.$data[name];
  this.update()
}

Watch.prototype.update = function () {
    var node = this.node;
    if (node.nodeType === 3) {
      var that = this;
      var v = node.nodeValue;
      var _name = that.name;
      if (node.nodeValue.indexOf('{{') < 0) {
        _name = that.vm.$data[that.name]
        console.log(that.vm.$data);
      }
      // 第一次匹配{{name}} 第二次匹配的是value
      // 这本身也会有bug,只是这个地方我们实现的是双向绑定模拟(如果value和本身字符串一样的话,这样替换就会有问题)
      var reg = new RegExp('\{?\{?('+_name+')\}?\}?', 'g');
      
      node.nodeValue = v.replace(reg, function (s, $1) {
          return that.value;
      })
    } else {
      node.value = this.value
    }
}

这里如何理解这个订阅者和发布者?

举个生活中的例子:你定了银行的存取短信通知?

你就是这个订阅者(watch),订阅者有很多?你同事、朋友等等,他们收到消息后处理方式也不一样,有的删除了,有的记录了(update)

而银行就是发布者(dep),但是它也不止一个,它还有其他业务,其他业务你可能没订阅,你是不会收到这些消息的,也就是一个业务(data的变量)一个发布(dep)实例

之前的代码更改如下:主要就是添加订阅者


image.png

那么发布者?我们如何去管理发布中心,什么时候去发布?

其实我们可以知道,就是每个变量都对应一个发布中心,也就是上边举例的中的业务。每次改变这个变量就去发布,然后订阅者就去更新自己

当然我们可以自己手动去触发这个发布,也可以用es5的defineProtype或者es6的poxy去实现数据劫持

// 数据劫持
function resovle (data) {
  for (var k in data) {
      // 在这个地方读取的时候,还没有设置get,所以不会死循环
      (function (k) {
        var _value = data[k]
        var dep = new Dep()
        Object.defineProperty(data, k, {
            get: function () {
                // return data[k] 如果这样写的话,已经定义了get,所以他会自己调用自己,造成死循环
                // console.log(Dep.target )
                Dep.target && dep.addDep(Dep.target)
                return _value
            },
            set: function (v) {
                if (v === _value) {
                    return
                }
                dep.update(v, k);
                return _value = v
            }
        })
      })(k)
  }
}


可以实现简单的双向绑定,当然还有很多问题,但基础的实现了

上边的代码不一定能完整跑起来,可以查看完整代码demo