vue的双向绑定

95 阅读6分钟

(一)MVVM介绍

MVVM是Model-View-ViewModel的简写,即模型-视图-视图模型,MVVM是双向、自动的,也就是数据发生变化自动同步视图,视图发生变化自动同步数据。

  • Model:数据模型),后端传递的数据(data,props,computed等部分)

  • View:代表 UI 组件,它负责将数据模型转化成 UI 展现出来(template),通常是DOM层,主要作用是给用户展示各种信息。

  • ViewModel:是一个同步View 和 Model的对象。MVVM模式的核心,它是连接Model和View的桥梁。

    • 实现了Data Binding(数据绑定),将Model的改变实时反应到View中,即将后端传递的数据转化成所看到的页面
    • 实现了DOM Listeners(DOM监听),当dom发生事件时,在需要的情况下改变对应的data,即将所看到的页面转化成后端的数据

开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

(二)vue的双向绑定原理

vue数据的双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。
(1)发布订阅–观察者模式

  1. 监听器 Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
  2. 订阅者 Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
  3. 解析器 Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
    (2)数据劫持
    通过 Object.defineProperty() 方法实现的,vue 在内部会把定义在 data 中的属性通过这个方法全部转为 getter/setter,在数据变化时发布消息给订阅者,触发相应的监听回调,也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变

(三)vue的双向绑定实现

  • 初始化阶段:

    • Observer 劫持 data 对象的属性,给每个属性添加 getter 和 setter 方法。
    • Compile 遍历 DOM,解析指令,如 v-model 和 v-bind。
    • 为每个解析到的指令创建对应的 Watcher,并添加到对应属性的 Dep 中。
    • 将数据渲染到页面。
  • 视图到数据:

    • 当用户与页面上的表单元素交互时,事件监听器(如 v-model 的输入监听器)触发并修改 data 对象的值。
    • 由于表单元素绑定了 data 对象的某个属性,因此修改 data 的值会触发该属性的 setter 方法(Observer 执行 set 和 get)。
  • 数据到视图:

    • 当后端传来新数据或者数据发生变化时,Observer 会执行 setter 方法,通知 Dep 更新视图。
    • Dep 根据通知找到对应的一组 Watcher,并调用它们的 update 方法来更新视图

实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者

实现一个订阅者Watcher,每个Watcher都绑定一个更新函数,Watcher可以收到属性的变化通知并执行相应的函数,从而更新视图。由于 data 的某个 key 在一个视图中可能出现多次,所以每个 key 都需要一个管家 Dep 来管理多个Watcher

实现一个消息订阅器 Dep,主要收集订阅者,当 Observe 监听到发生变化,就通知 Dep 再去通知Watcher去触发更新。

实现一个解析器Compile,可以扫描和解析每个节点的相关指令,若节点存在指令,则Compile初始化这类节点的模板数据(使其显示在视图上),以及初始化相应的订阅者

Compile实现

页面的初始化
实现页面的初始化,即解析出v-text和v-model指令,并将data中的数据渲染到页面
解析各种指令成真正的html,解析和绑定dom
将页面上的数据变更同步到vue实例中
在input元素上绑定一个input事件,将data中的相应数据修改为input中的值

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) {
      console.log([child])
      self.compileElement(child, vm);
      frag.append(child); // 将所有子节点添加到fragment中
    }
    return frag;
  },
  compileElement: function(node, vm) {
    var reg = /{{(.*)}}/;
    //节点类型为元素(input元素这里)
    if(node.nodeType === 1) {
      var attr = node.attributes;
      // 解析属性
      for(var i = 0; i < attr.length; i++ ) {
        if(attr[i].nodeName == 'v-model') {//遍历属性节点找到v-model的属性
          var name = attr[i].nodeValue; // 获取v-model绑定的属性名
          node.addEventListener('input', function(e) {
            // 给相应的data属性赋值,进而触发该属性的set方法
            vm[name]= e.target.value;
                    });
            new Watcher(vm, node, name, 'value');//创建新的watcher,会触发函数向对应属性的dep数组中添加订阅者,
          }
        };
      }
      //节点类型为text
      if(node.nodeType === 3) {
        if(reg.test(node.nodeValue)) {
          var name = RegExp.$1; // 获取匹配到的字符串
          name = name.trim();
          new Watcher(vm, node, name, 'nodeValue');
      }
    }
  }
}

Observer实现

将vue实例中的数据渲染到页面上
当data中的数据发生更新的时候,绑定了该数据的元素会在页面上自动更新视图
用来实现对每个vue中的data中定义的属性循环用Object.defineProperty()实现数据劫持,以便利用其中的settergetter,然后通知订阅者

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();//通知后dep会循环调用各自的update方法更新视图
    }
  })
}
function observe(obj, vm) {
  Object.keys(obj).forEach(function(key) {
    defineReactive(vm, key, obj[key]);
  })
}

Watcher实现

vue实例中data数据变更 页面上数据同步变更
通过Compile获取dom的指令生成Watcher,将其通过到对应dataObserver,添加到指定的Dep中,将指令和data对应

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() {
    console.log(1)
    this.value = this.vm[this.name]; //触发相应属性的get
  }
}

Dep实现

为每个属性添加订阅者列表
在vue中v-modelv-name{{}}等都可以对数据进行显示,也就是说假如一个属性都通过这三个指令了,那么每当这个属性改变的时候,相应的这个三个指令的html视图也必须改变,于是vue中就是每当有这样的可能用到双向绑定的指令,就在一个Dep中增加一个订阅者,其订阅者只是更新自己的指令对应的数据,也就是v-model='name'{{name}}有两个对应的订阅者,各自管理自己的地方。每当属性的set方法触发,就循环更新Dep中的订阅者

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

416.png

总结

首先为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;然后在编译的时候在该属性的数组dep中添加订阅者,v-model会添加一个订阅者,{{}}也会,v-bind也会,只要用到该属性的指令理论上都会,接着为input添加监听事件,修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。