vue的数据双向绑定的实现

几种实现双向绑定的做法

1、发布者-订阅者模式(backbone.js)
2、脏值检查(angular.js)
3、数据劫持(vue.js)

vue.js则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动是发布消息给订阅者,触发相应的监听回调。

思路整理

要实现mvvm的双向绑定,就必须实现一下几点:

  • 1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅
    者;
  • 2、实现一个指令解析器Compile,对每个元素的指令进行扫描和解析,根据指令模版替换数据,以及绑定相应的更新函数。
  • 3、实现一个Watcher,作为Observer和Compile的桥梁,能够订阅并收到每个属性的变动通知,执行指令绑定相应的回调函数,从而更新视图。

代码实现

1、实现数据监听器Observer

    function observe (obj, vm){
      if (!data || typeof data !== 'object') {
          return;
      }
      //取出所有属性遍历
      Object.keys(obj).forEach(function(key) {
        defineReactive(vm, key , obj[key]);
      });
    }
    function defineReactive(obj, key, val){
      Object.defineProperty(obj, key, {
        get: function(){
          return val;
        },
        set: function(newVal){
          if(newVal === val) return;
          val = newVal;
          console.log(val);
        }
      });
    }

2、实现指令解析器Compile。这里需要用到文档片段DocumentFragment,它可以包含多个子节点,当我们将它插入到DOM中时,只有他的子节点会插入到目标节点中。用DocumentFragement处理节点,速度和性能远远优于直接操作DOM。Vue进行编译时,就是将挂在目标的所有子节点劫持(通过append方法,原DOM中的节点会被自动删除,所以是真的劫持啊~)到DocumentFragment中,经过一番处理后,再将DocumentFragment整体返回插入挂载目标。

    /*编译模板*/
    function nodeToFragment(node, vm){
      var flag = document.createDocumentFragment();
      var child;
      while(child = node.firstChild) {
        compile(child);
        flag.appendChild(child);
      }
      return flag;
    }

    function compile (node, vm){
      var reg = /\{\{(.*)\}\}/;
      if(node.nodeType === 1){  //节点类型为元素
        var attr = node.attributes;
        for(var i = 0, alen = attr.length; i < alen; i++) {
          if(attr[i].nodeName == 'v-model' ){
            var name = attr[i].nodeValue; //获取v-model绑定的属性名
            // 对监听该node的input事件,当有输入时,把新值赋给vm的data
            node.addEventListener('input', function (e) {
              //给对应的data属性赋值,进而触发该属性的set方法
              vm[name] = e.target.value;
            })
            node.value = vm[name];
            node.removeAttribute('v-model');
          }
        }
      }
      if(node.nodeType === 3){ //节点类型为text
        if(reg.test(node.nodeValue)) {
          var name = RegExp.$1;
          name = name.trim();
          node.nodeValue = vm[name];
        }
      }
    }

由此实现了:文本框以及文本节点与vue实例中data属性的数据绑定,当输入框内容变化时,data属性中的数据同步变化。
接下来,需要实现data属性中的数据变化时,文本节点的内容同步变化。
3、这里插播一下订阅发布模式(subscribe&publish)
订阅发布模式定义了一种一对多的关系,让多个观察者同事监听某一个主题对象,这个主题对象的状态发生改变时,就会通知所有观察者对象。
发布者发出通知 => 主题对象收到通知并推送给订阅者 =》 订阅者执行响应操作
为了要实现“data属性中的数据变化时,文本节点的内容同步变化”,当set方法触发后做的第二件事就是就是作为发布者发出通知,文本节点作为订阅者,在收到通知后执行响应的更新操作。
所以基本思路是:
1、在监听数据的过程中,为data中的每一个属性生成一个主题对象dep。
2、在编译HTML的过程中,会为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会自己添加到响应属性的dep中。
目前已经实现了:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的 set 方法
接下来实现:发出通知 dep.notify() => 触发订阅者的 update 方法 => 更新视图。

    /*发布订阅者*/
    function Watcher(vm, node, name){
      Dep.target = this;
      this.vm = vm;
      this.node = node;
      this.name = name;
      this.update();
      Dep.target = null;
    }
    Watcher.prototype = {
      update: function(){
        this.get();
        if (this.nodeType == 'text') {
          this.node.nodeValue = this.value;
        }
        if (this.nodeType == 'input') {
          this.node.value = this.value;
        }
      },
      get: function(){
        this.value = this.vm[this.name];  //触发响应属性的get;
      }
    }
    function Dep(){
      this.subs = [];
    }
    Dep.prototype  = {
      addSub: function(sub){
        this.subs.push(sub);
      },
      notify: function(){
        this.subs.forEach(function(sub){
          sub.update();
        })
      }
    }

为了给每一个属性生成一个主题对象,所以原来的observe新增new Dep(),为了在属性改变时发出通知,还需要在set方法里执行dep.notify()方法;

image.png

在编译HTML时,为每一个数据绑定的节点生成一个订阅者。


image.png

完整代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>双向绑定</title>
</head>
<body>
<div id="app">
     <input type="text" v-model="text">
     {{ text }}
</div>

<script type="text/javascript">
    /*监听数据*/
    function observe (obj, vm){
      if (!obj || typeof obj !== 'object') {
          return;
      }
      //取出所有属性遍历
      Object.keys(obj).forEach(function(key) {
        defineReactive(vm, key , obj[key]);
      });
    }
    function defineReactive(obj, key, val){
      //为每一个属性生成一个主题对象。
      var dep = new Dep();
      Object.defineProperty(obj, key, {
        get: function(){
          // 添加订阅者 watcher 到主题对象 Dep
          if(Dep.target) dep.addSub(Dep.target);
          return val;
        },
        set: function(newVal){
          if(newVal === val) return;
          val = newVal;
          dep.notify(); //数据改变时发出通知
          console.log(val);
        }
      });
    }

    /*编译模板*/
    function nodeToFragment(node, vm){
      var flag = document.createDocumentFragment();
      var child;
      while(child = node.firstChild) {
        compile(child, vm);
        flag.appendChild(child);
      }
      return flag;
    }

    function compile (node, vm){
      var reg = /\{\{(.*)\}\}/;
      if(node.nodeType === 1){  //节点类型为元素
        var attr = node.attributes;
        for(var i = 0, alen = attr.length; i < alen; i++) {
          if(attr[i].nodeName == 'v-model' ){
            var name = attr[i].nodeValue; //获取v-model绑定的属性名
            // 对监听该node的input事件,当有输入时,把新值赋给vm的data
            node.addEventListener('input', function (e) {
              //给对应的data属性赋值,进而触发该属性的set方法
              vm[name] = e.target.value;
            })
            // node.value = vm[name];
            node.removeAttribute('v-model');
          }
        }
        new Watcher(vm, node, name, 'input');
      }
      if(node.nodeType === 3){ //节点类型为text
        if(reg.test(node.nodeValue)) {
          var name = RegExp.$1;
          name = name.trim();
          // node.nodeValue = vm[name];
          new Watcher(vm, node, name, 'text');
        }
      }
    }
    Watcher.prototype = {
      update: function(){
        this.get();
        if (this.nodeType == 'text') {
          this.node.nodeValue = this.value;
        }
        if (this.nodeType == 'input') {
          this.node.value = this.value;
        }
      },
      get: function(){
        this.value = this.vm[this.name];  //触发响应属性的get;
      }
    }
    /*发布订阅者*/
    function Watcher(vm, node, name, nodeType){
      Dep.target = this;
      this.vm = vm;
      this.node = node;
      this.nodeType = nodeType;
      this.name = name;
      this.update();
      Dep.target = null;
    }

    function Dep(){
      this.subs = [];
    }
    Dep.prototype  = {
      addSub: function(sub){
        this.subs.push(sub);
      },
      notify: function(){
        this.subs.forEach(function(sub){
          sub.update();
        })
      }
    }
    var vm = new Vue({
      el: 'app',
      data: {
        text: 'hello world'
      }
    })
    function Vue (options) {
      this.data = options.data;
      var data = this.data;

      observe(data, this);

      var id = options.el;
      var dom = nodeToFragment(document.getElementById(id), this);

      // 编译完成后,将 dom 返回到 app 中
      document.getElementById(id).appendChild(dom); 
    }
</script>
</body>
</html>

参考:
https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension
https://github.com/DMQ/mvvm

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容