欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

记一次对vue双向绑定的理解

程序员文章站 2022-03-10 21:45:26
...

之前有看过一次vue双向绑定原理实现相关的博客,看得似懂非懂的,然后也就搁浅了。
昨天脑海里又突然燃起了要不这块搞懂的冲动,于是乎又开始了一轮博客轰炸,综合研究了多位大神写得关于vue双向绑定的实现原理,然后结合自己的一些理解,就有了这篇文章了。
关于双向绑定,我所知的两类:(小的不才,目前只接触过vue和angular)
一是angular1的脏检查机制
二是vue的数据劫持配合观察者模式(网上有很多写的都是订阅-发布模式,我特意去查看了这两种模式有什么区别,大致是说观察者模式中观察者是被动接收统一的消息,而订阅-发布模式中是订阅者是可以自定义接收行为的,而vue中watcher对象中update方法都是一致的,就是同步数据,所有鄙人觉得用观察者模式来形容可能会更贴切些,如果理解的不对,欢迎大家轻吐~)
下面我先贴一张我整理的原理图

记一次对vue双向绑定的理解
我的理解:
1.编译器会解析DOM元素,比如碰到v-model指令时,会对该DOM元素添加input监听事件,当事件发生时,给data属性值赋值,进而触发该属性的setter方法(这样就实现了从view到model的同步);除了增加监听事件外,还会实例化一个watch对象。
2.实例化一个watch对象主要有两个作用,一个是通过调用data元素的getter方法,触发Dep的add方法将watcher对象加入到订阅器中(为了防止在除watch对象中其他其他调用了getter方法,进而重复添加监听器,我们在watcher对象中给Dep添加一个不为空全局变量,当全局变量不为空时才添加,添加完后又将全局变量置为null),第二个作用是声明一个update方法,用来接收到订阅器的通知后同步数据给节点进行渲染。
3.上面我们已经在属性的getter方法中将watcher对象加入了订阅器,当model层属性值发生变化时会触发setter方法,我们在setter中再去触发订阅器的notify,并在notify中触发watcher对象的update方法(这样就实现了从model到view的同步)
从图中也可看出,主要有4大块,我们依照流程图的顺序来分析一下:

  • compile
    先撸一波
function Compile(node, vm) {
      if(node) {
        this.$frag = this.nodeToFragment(node, vm);
        return this.$frag;
      }
    }
    Compile.prototype = {
      nodeToFragment: function(node, vm) {
        var self = this;
        var frag = document.createDocumentFragment();
        var child;

        while(child = node.firstChild) {
          self.compileElement(child, vm);
          frag.append(child); // 将所有子节点添加到fragment中
        }
        return frag;
      },
      compileElement: function(node, vm) {
        var reg = /\{\{(.*)\}\}/;

        //节点类型为元素
        if(node.nodeType === 1) {
          var attr = node.attributes;
          // 解析属性
          for(var i = 0; i < attr.length; i++ ) {
            if(attr[i].nodeName == 'v-model') {
              var name = attr[i].nodeValue; // 获取v-model绑定的属性名
              node.addEventListener('input', function(e) {
                // 给相应的data属性赋值,进而触发该属性的set方法
                 vm[name]= e.target.value;
              });
              // node.value = vm[name]; // 将data的值赋给该node
              new Watcher(vm, node, name, 'value');
            }
          };
        }
        //节点类型为text
        if(node.nodeType === 3) {
          if(reg.test(node.nodeValue)) {
            var name = RegExp.$1; // 获取匹配到的字符串
            name = name.trim();
            // node.nodeValue = vm[name]; // 将data的值赋给该node
            new Watcher(vm, node, name, 'nodeValue');
          }
        }
      },
    }

代码分析:
(1)使用 DocumentFragment 处理节点,速度和性能远远优于直接操作 DOM。Vue 进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过 append 方法,DOM 中的节点会被自动删除)到 DocumentFragment 中,经过一番处理后,再将 DocumentFragment 整体返回插入挂载目标。
(2)node.nodeType判断节点类型,如果是元素的话,判断该元素有没有v-model
属性,有则监听input事件,将输入框的值同步到变量中,同时实例化一个watcher。如果是文本的话,看是不是符合{{}}正则表达式,符合则是我们需要加入订阅器的对象。

  • watcher-观察者
function Watcher(vm, node, name, type) {
      Dep.target = this;
      this.name = name;
      this.node = node;
      this.vm = vm;
      this.type = type;
      this.update();
      Dep.target = null;
    }

    Watcher.prototype = {
      update: function() {
        this.get();
        this.node[this.type] = this.value; // 订阅者执行相应操作
      },
      // 获取data的属性值
      get: function() {
        this.value = this.vm[this.name]; //触发相应属性的get
      }
    }

代码分析:
(1)实例化watcher对象是有获取对象的值this.vm[this.name],这对出发该对象的get方法,而在对象的get方法中就可以把这个观察者加入到Dep中了,后面oberver可以看到
(2)update()方法就是观察者接收到通知后用来同步数据给节点进行渲染的作用。

  • Observe-数据监测器

    function defineReactive (obj, key, val) {
      var dep = new Dep();
      Object.defineProperty(obj, key, {
        get: function() {
           //添加订阅者watcher到主题对象Dep
          if(Dep.target) {
            // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
            dep.addSub(Dep.target);
          }
          return val;
        },
        set: function (newVal) {
          if(newVal === val) return;
          val = newVal;
          console.log(val);
          // 作为发布者发出通知
          dep.notify();
        }
      })
    }
    function observe(obj, vm) {
      Object.keys(obj).forEach(function(key) {
        defineReactive(vm, key, obj[key]);
      })
    }

代码分析:
(1)数据监测器用到的知识点就是ES5的Object.defineProperty方法,改写对象的set和get方法,在set方法中通知观察该对象的所有watcher更新,在get方法中实现的是当watcher第一次调用get方法的时候把自己绑定给订阅器进行管理

  • Dep-订阅器
function Dep() {
      this.subs = [];
    }
    Dep.prototype = {
      addSub: function(sub) {
        this.subs.push(sub);
      },
      notify: function() {
        this.subs.forEach(function(sub) {
          sub.update();
        })
      }
    }

(1)它的addsub方法在observer的get方法中被调用,notify方法在observer的set方法中被调用
至此,四大块就分析完了,回过头来再去看那张图,你理解的会更深刻一点。
下面终极大boss出场了MVVM.js

function Vue(options) {
      this.data = options.data;
      var data = this.data;
      observe(data, this);
      var id = options.el;
      var dom =new Compile(document.getElementById(id),this);
      // 编译完成后,将dom返回到app中
      document.getElementById(id).appendChild(dom);
    }

index.html

<!DOCTYPE html>
  <head>
  </head>
  <body>
  <div id="app">
    <input type="text" id="a" v-model="text">
    {{text}}
  </div>
  <script src="src/Dep.js"></script>
  <script src="src/Observe.js"></script>
  <script src="src/Watcher.js"></script>
  <script src="src/Compile.js"></script>
  <script src="src/MVVM.js"></script>
  <script>
    var vm = new Vue({
      el: 'app',
      data: {
        text: 'hello world'
      }
    });
  </script>
  </body>
</html>

代码分析:
这里自定义了vue对象,解析包含的html片段,实现的双向绑定。

··························分隔符···························
参考资料:
http://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension

相关标签: vue