Vue2.x数据双向绑定原理

2,332 阅读4分钟

个人理解: 数据双向绑定就是让数据Model展示到视图View上,同时视图View的变化改变数据Model。

在Vue中代码,很轻松就能实现输入框绑定数据,并随输入而动态改变数据

      <input v-model="msg" />
      <p>{{msg}}</p>

效果图

abc.gif

光是用还不够,接下来我们来了解Vue实现图示效果的逻辑思维,这样更能理解有时数据渲染与个人预期不同的原因。

数据绑定

常见的架构模式:

  • MVC:通过业务逻辑的Controller层,对应用数据Model进行更改,然后在View视图展示(jsp页面,ejs嵌入式js模板引擎)。
  • MVP:P指的是Presenter,负责View和Model之间数据流,作为二者之间的中间人
  • MVVM:MVVM 可以分解成(Model-View-ViewModel),ViewModel是P的进阶版,通过事件监听响应View中用户修改Model的数据,减少DOM的操作。

它们设计的目标都是为了解决Model和View的耦合问题

目前前端框架基本上都是采用 MVVM 模式实现双向绑定,Vue 自然也不例外。但是各个框架实现双向绑定的方法略有所不同,目前大概有三种实现方式。

  • 发布订阅模式
  • Angular 的脏查机制
  • 数据劫持

Vue 则采用的是数据劫持与发布订阅相结合的方式实现双向绑定,数据劫持主要通过 Object.defineProperty 来实现

Object.defineProperty

具体可以到官网查看

Vue主要用到了其中的 set 和 get 方法

get

属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined

set

属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined

分析实现

MVVM模式在于数据和视图保持同步,一方改变就会更新另一方

如何检测数据变化

视图的标签发生变化从而去更新数据,这个利用标签绑定到对应的事件监听即可;而数据的变化利用Object.definePropertyset方法,属性发生变化自动触发set函数,在函数内部通知更新视图

image-20211211112624718.png

实现

根据描述和分析,Vue的双向绑定是通过数据劫持和发布订阅模式来实现的。数据劫持是通过Object.defineProperty方法实现的,而发布订阅则需要监听器Observer监听标签属性的变化,Watcher订阅者来更新视图,同时还需要compile对Vue的特定指令进行解析。

  • Observer监听器: 用来监听属性的变化通知订阅者
  • Watcher订阅者:收到属性的变化,然后更新视图
  • compile解析器:解析Vue的特定指令,初始化并绑定标签属性到订阅者(如{{}},v-model)

image-20211211155552213.png

模拟实现双向绑定

MyVue实例

// vue接收一个对象参数
      function MyVue(options = {}) {
        this.$options = options;
        // 获取到 el元素
        this.$el = document.querySelector(options.el);
        this._data = options.data;
        // 设置订阅池保存订阅器
        this._watcherTpl = {};
        // 设置observer函数,对数据重写,实现数据变化的监听
        this._observer(this._data);
        // 编译模板和指令,生成订阅器发布订阅
        this._compile(this.$el);
      }

创建vm实例

const vm = new MyVue({
        el: "#app",
        data: {
          msg: "这是仿·Vue",
        },
      });

监听器 Observer

将实例中的data对象进行遍历:

  • 在订阅器中添加对应的属性,并赋值一个初始的订阅器属性为数组
  • 给每个属性使用Object.defineProperty添加set、get方法
      MyVue.prototype._observer = function (obj) {
        Object.keys(obj).forEach((key) => {
          // 添加每个属性的订阅器位置
          this._watcherTpl[key] = {
            _directives: [],
          };
          // 方便调用
          let watcherTpl = this._watcherTpl[key];
          // 获取当前key的值
          let value = obj[key];
          Object.defineProperty(this._data, key, {
            configurable: true,
            enumerable: true,
            get() {
              // console.log(`${key}获取的值是:${value}`);
              return value;
            },
​
            set(newVal) {
              // 数据发生了变化才触发更新
              if (newVal === value) return;
              value = newVal;
              //触发更新
              // console.log("将订阅池对应的属性进行更新");
              watcherTpl._directives.forEach(item => {
                item.update()
              })
            },
          });
        });
      };

订阅者 Watcher

将对应元素节点的属性值关联到实例的data的属性值

      function Watcher(el, vm, val, attr) {
        this.el = el;
        this.vm = vm;
        this.val = val;
        this.attr = attr;
        this.update();
      }
​
      Watcher.prototype.update = function () {
        this.el[this.attr] = this.vm._data[this.val];
      };

解析器 Compile

解析指令初始化模板,添加标签属性到订阅者并绑定更新函数

示例解析Input和模板语法{{}}

      MyVue.prototype._compile = function (el) {
        var nodes = el.children;
        var len = nodes.length;
        for (var i = 0; i < len; i++) {
          var node = nodes[i];
          // 如果节点存在子节点则进行递归
          if (node.children.length) this._compile(node);
​
          // 判断是否有v-model指令并且是输入框
          if (node.hasAttribute("v-model") && node.tagName === "INPUT") {
            node.addEventListener("input", ((key) => {
                var attrVal = node.getAttribute("v-model");
                // 创建watcher对象, 并且将订阅器根据属性放入对应的订阅器集合
                var watcher = new Watcher(node, this, attrVal, "value");
                this._watcherTpl[attrVal]._directives.push(watcher);
                return () => {
                  this._data[attrVal] = nodes[key].value;
                };
              })(i)
            );
          }
​
          // 正则匹配模板语法 {{}}
          var reg = /{{\s*(.*?)\s*}}/igs;
          var txt = node.textContent;
          if (reg.test(txt)) {
            node.textContent = txt.replace(reg, (matched, placeholder) => {
              
              let attVal = placeholder;
              var watcher = new Watcher(node, this, attVal, "innerHTML");
              this._watcherTpl[attVal]._directives.push(watcher)
              return placeholder.split(".").reduce((val, key) => {
                return val[key];
              }, this._data);
            });
          }
        }
      };

效果

HTML

<div id="app">
      <input v-model="msg" />
      <p>{{msg}}</p>
</div>

js

//将MyVue、Observer、Watcher、Compile依次放进来const vm = new MyVue({
        el: "#app",
        data: {
          msg: "这是仿·Vue",
        },
      });

就能实现简单的双向绑定逻辑