Vue双向绑定实现原理(三)数据改变,更新视图

上面已经实现了单向绑定,也就是将数据通过模板编译的原理显示在了视图上,但如图3-1所示,单向数据的改变并没有再刷新到视图上。

无法更新到视图

图3-1

 

因为还没有对数据做监控,监控到改变之后,执行更新视图操作。这个概念似乎不陌生了,这就是观察者模式,有一部分也说这是发布订阅模式。

关于观察者模式和发布订阅模式,这两者有没有不同之处,总是被人议论纷纷。有的人认为两者就是一个模式,有的人认为有点不同,在我的观点是不同的。

不同的点可以参看以前Java版的《大话设计模式》。

观察者模式和发布订阅模式

观察者模式:

观察者和被观察者

代码后面奉上

发布订阅模式:

发布者 - 发布中心 - 订阅者

区别在于是否有发布中心作为二者的中介

单向数据绑定的更新操作

而下面代码实际上是利用观察者模式,没有发布中心的概念,别被命名误导。

发布者/被观察者

被 ,观察者,观察,所以需要绑定不同的观察者,也支持解绑,同时数据改变,通知所有观察者执行更新操作。

这里如果想做得更好,应该抽象出一个观察者的基类,观察者基类里面有更新视图的接口,各式各样的观察者去继承这个基类,实现这个接口。但js里很难去implement?反正我写起来有点困难

class Publish {
        constructor() {
          this.subList = [];
        }
    //绑定一个观察者,类型最好是基类,可以接受不同的类型的子类观察者
        attachSub(subscribe) {
          this.subList.push(subscribe);
        }
    //解绑
        detachSub(sub) {
          var index = this.subList.findIndex(sub);
          this.subList.splice(index, 1);
        }
    //通知所有观察者执行更新操作。
        notify() {
          this.subList.forEach(sub => {
            sub.update();
          });
        }
      }

观察者,观察到数据改变做更新操作

     class Subscribe {
          //构造时,就传入一个回调的更新操作,达到接口实现的目的
        constructor(fn) {
          this.fn = fn;
        }
        // 去执行这个回调
        update() {
          this.fn();
        }
      }
     
 let subscribe = new Subscribe(function() {
        alert("接受到通知,做更新操作");
      });
​
      let pub = new Publish();
      pub.attachSub(subscribe);
//点击某个按钮,去通知需要执行更新操纵了
      document.querySelector("#btn").addEventListener("click", function() {
        pub.notify();
      });

 

讲完了观察者,就需要将观察者应用到单向数据更新上面.在双向绑定这个原理,需要被观察的是数据.

但现在又如何监控视图的改变,将改变的数据更新到真正的数据上?

假如目前是通过输入框来进行数据的改变,这就好办了,input输入的onchange事件就能随时拿到改变的值.然后监控到数据的改变,就可以去更新视图了.

set能监控数据的改变,这里就应该通知观察者更新

  // 会在数据改变的时候直接设置
            set(newVal) {
              //数据并没有改变
              if (newVal === val) {
                return;
              }
              observe(newVal);
              val = newVal;
              publish.notify();
            }
          });

更新什么?更新视图啊,就是做之前写的模板编译的replace函数里,替换文本节点里的文字

  // 创建一个订阅/观察者,调用更新视图操作
  new Subscribe(vm, key, function(newVal) {
    if (typeof newVal === "object") newVal = JSON.stringify(newVal);
    // 如果不是对象就直接显示基本数据类型,是对象,需要把对象做一个Json类型的显示.如果不这样又会是
      //[Object object]
    node.textContent = text.replace(reg, newVal);
  });
//当然别忘了,当前replace函数里还是要做替换文本节点
              node.textContent = text.replace(reg, val);

那又是在哪里将观察者(执行更新数据的操作)绑定到被观察者(数据)上?

当然是一开始获取数据时,这个数据被获取,才证明他是被当前视图需要的,此时就可以添加到被观察者的队列中,做观察操作

// 一旦调用get说明值有被使用,就需要添加进被观察者的队列中,需要被观察
get() {
  Publish.target && publish.attachSub(Publish.target);
  return val;
},

每个数据都应该有一个观察者实例对不对?就像A检测B,C检测D ,一对一的关系.A如果既观察B又观察D,就会有更新操作的混乱.

所以每个数据都有一个观察者实例.更新操作需要当前vue实例vm,这样可以拿到data,也需要新的值newVal,还需要知道属性名,还有更新的回调函数.

抽象出来.

class Subscribe {
        constructor(vm, key, fn) {
          this.vm = vm;
          this.key = key;
          this.fn = fn;
          Publish.target = this;
​
          let val = this.vm; //从vm中去拿
          let arr = this.key.split("."); // [name,firstName]
​
          arr.forEach(function(k) {
            val = val[k];
          });
          // Publish.target = null;
        }
​
        update() {
          let val = this.vm; //从vm中去拿
          let arr = this.key.split("."); // [name,firstName]
          arr.forEach(function(k) {
            val = val[k];
          });
​
          this.fn(val);
        }
      }
      //这是针对对象是数据类型,如果data中的属性是对象,对象包裹对象,需要递归
      // 发布 - 订阅模式
      class Publish {
        constructor() {
          this.subList = [];
        }
        attachSub(subscribe) {
          this.subList.push(subscribe);
        }
        detachSub(sub) {
          var index = this.subList.findIndex(sub);
          this.subList.splice(index, 1);
        }
        notify() {
          this.subList.forEach(sub => {
            sub.update();
          });
        }

说的很绕,望见谅

完整代码如下:

<!DOCTYPE html>
<html>
  <head>
    <title>数据改变,视图更新</title>
  </head>
  <body>
    <div id="app">
      <p>姓名是{{ name.firstName }}</p>
      <div>年龄是{{ age }}</div>
      {{ name }}
    </div>
    <script type="text/javascript">
      function Vue(options = {}) {
        this.$options = options; // 将所有属性挂载在vue实例$options上
        var data = (this._data = this.$options.data);
​
        observe(this._data);
        // data挂载在vue上面
        for (let key in data) {
          Object.defineProperty(this, key, {
            enumrable: true,
            get() {
              // 当访问this实例上的name时,this不是去它本身上找到这个属性,而是从this._data上去寻找
              return this._data[key];
            },
            set(newVal) {
              this._data[key] = newVal;
            }
          });
        }
        // 识别大括号,将文本节点替换
        new Compile(this.$options.el, this);
      }
​
      function Compile(el, vm) {
        vm.$el = document.querySelector(el);
        // 这一段打印有问题
​
        //documentFragment是文档碎片
        let fragment = document.createDocumentFragment();
        // 这一步是遍历app根节点下的所有节点,因为是DOM树的形式,将节点拆分,挂在新的空白文档上,也就是孩子节点的父节点指向改变了,让fragment作为他们的根节点
        while ((child = vm.$el.firstChild)) {
          fragment.appendChild(child);
        }
        // app下的孩子节点挂在fragment下,app下就没有节点,无法显示了了,所以需要显示回来
        // var n = 1;
        replace(fragment);
        function replace(fragment) {
          Array.from(fragment.childNodes).forEach(function(node) {
            // console.log(n)
            // n++;
            // console.log(node.textContent)//标签与标签之间有text节点,注释也算一个标签{{name}}
​
            let text = node.textContent;
            text = text.trim();
​
            let reg = /\{\{(.*)\}\}/;
​
            // 是文本节点 又匹配双大括号语法
            if (node.nodeType === 3 && reg.test(text)) {
              //打印匹配到的第一个 name.firstName age name
              let key = RegExp.$1;
              key = key.trim();
              // console.log(vm._data[key])
              // node.textContent =text.replace(reg,vm._data[key]) //这样拿不到对象的对象,因为name.firstName 不可能通过字面量拿到
              // 要去遍历key,先拿到对象name,再拿到firstName
              let arr = key.split("."); // [name,firstName]
              let val = vm; //从vm中去拿
              arr.forEach(function(k) {
                val = val[k];
              });
              console.log(val);
              // 如果给的就是个对象,那么就序列化显示一下
              if (typeof val === "object") val = JSON.stringify(val);
              // 如果不是就直接显示基本数据类型
​
              // 创建一个订阅/观察者,调用更新视图操作
              new Subscribe(vm, key, function(newVal) {
                if (typeof newVal === "object") newVal = JSON.stringify(newVal);
                // 如果不是就直接显示基本数据类型
                node.textContent = text.replace(reg, newVal);
              });
              node.textContent = text.replace(reg, val);
            }
            // 如果不是文本节点,是标签,则需要遍历标签下面的孩子节点是否有文本节点
            if (node.childNodes) {
              replace(node);
            }
          });
        }
        vm.$el.appendChild(fragment);
      }
​
      function Observe(data) {
        // 创建观察者/发布中心 观察所有被观察者/订阅者
        let publish = new Publish();
        for (let key in data) {
          let val = data[key];
          // 如果data中包含属性是对象,则需要递归对象的中属性,进行数据劫持
          // 如果data中的属性就是普通数据类型,递归退出 -- 递归出口
          observe(val);
          Object.defineProperty(data, key, {
            enumrable: true,
            // 一旦调用get说明值有被使用,就需要添加进被观察者的队列中,需要被观察
            get() {
              Publish.target && publish.attachSub(Publish.target);
              return val;
            },
            // 会在数据改变的时候直接设置
            set(newVal) {
              //数据并没有改变
              if (newVal === val) {
                return;
              }
              observe(newVal);
              val = newVal;
              publish.notify();
            }
          });
        }
      }
​
      /**
       *观察对象变化,并且为data对象创建属性
       *@{data} 被观察的对象或属性
       */
      function observe(data) {
        if (typeof data !== "object") return null;
        return new Observe(data);
      }
​
      //这是针对对象是数据类型,如果data中的属性是对象,对象包裹对象,需要递归
      // 发布 - 订阅模式
      class Publish {
        constructor() {
          this.subList = [];
        }
        attachSub(subscribe) {
          this.subList.push(subscribe);
        }
        detachSub(sub) {
          var index = this.subList.findIndex(sub);
          this.subList.splice(index, 1);
        }
        notify() {
          this.subList.forEach(sub => {
            sub.update();
          });
        }
      }
​
      class Subscribe {
        constructor(vm, key, fn) {
          this.vm = vm;
          this.key = key;
          this.fn = fn;
          Publish.target = this;
​
          let val = this.vm; //从vm中去拿
          let arr = this.key.split("."); // [name,firstName]
​
          arr.forEach(function(k) {
            val = val[k];
          });
          // Publish.target = null;
        }
​
        update() {
          let val = this.vm; //从vm中去拿
          let arr = this.key.split("."); // [name,firstName]
          arr.forEach(function(k) {
            val = val[k];
          });
​
          this.fn(val);
        }
      }
    </script>
    <script type="text/javascript">
      let vm = new Vue({
        el: "#app",
        data: {
          name: {
            firstName: "姓氏章",
            lastName: "名字"
          },
          age: 12 //通过Obj.defineProperty实现()或者Obj.defineProperties()实现
        }
      });
    </script>
  </body>
</html>
​

可实现下图效果:

更新视图实现

现在的功能可以做获取数据(从本地或服务端),更新视图。之后就是在标签内识别v-model指令,然后识别指令之后的变量,也就是data的key.就差不多了,唯一需要注意的是深度响应的问题,也就是对象的对象的对象或数组之类的问题,还有一些不是通过options配置对象的操作,导致无法…………响应。

我在干什么?我写这个博客的时候,忘了接约好的面试电话。阿西吧,完全忘了有这回事,打回去,打不通了,有些时候真的很讨厌自己。手动再见。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值