MVVM

MVVM(Model-View-ViewModel)是对 MVC(Model-View-Control)的进一步改进。

  • View:视图层(template)
  • ViewModel:业务逻辑层(vue实例)
  • Model:数据层(data)

MVVM 将数据双向绑定(data-binding)作为核心思想,View 和 Model 之间没有联系,它们通过 ViewModel
这个桥梁进行交互

Model 和 ViewModel 之间的交互是双向的,因此 View 的变化会自动同步到 Model,而 Model的变化也会立即反映到 View 上显示

当用户操作 View,ViewModel 感知到变化,然后通知 Model 发生相应改变;反之当 Model 发生改变,ViewModel也能感知到变化,使 View 作出相应更新

双向绑定的核心: Object.defineProperty()

Object.defineProperty(obj, prop, descriptor) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

obj:要定义属性的对象
prop:要定义或修改的属性的名称或 Symbol
descriptor:要定义或修改的属性描述符
返回值:被传递给函数的对象

js通过Object.defineProperty方法简单的实现双向绑定

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <input type="text" id="app">
    <span id="childSpan"></span>
</body>
<script>
  var obj = {}
  var initValue='初始值'
  Object.defineProperty(obj,'initValue',{
    get(){
      console.log('获取obj最新的值');
      return initValue
    },
    set(newVal){
      initValue = newVal
      console.log('设置最新的值');
      // 获取到最新的值  然后将最新的值赋值给我们的span
      document.getElementById('childSpan').innerHTML = initValue
      console.log(obj.initValue);
    }
 })
 document.addEventListener('keyup', function (e) {
     obj.initValue = e.target.value; //监听文本框里面的值 获取最新的值 然后赋值给obj 
})
</script>
</html>

vue2响应式原理

vue2的双向数据绑定(又称响应式)原理,是通过数据劫持结合发布订阅模式的方式来实现的,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变。

官网上写追踪数据变化原理

  • 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property ,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。
  • 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在data中的 property 被访问和修改时通知变更。
  • 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
  • 在这里插入图片描述

vue2双向数据绑定过程

在这里插入图片描述

  1. Observer数据劫持,递归遍历所有data属性,全部转化为getter/setter,当获取属性值的时候,触发getter,更新属性值的时候触发setter
  2. Compile解析模板指令,初始化视图,将模板指令转化为数据,这个时候会获取数据触发属性的getter,在getter中收集依赖,collect as Dependency,并添加订阅者Watcher,每次获取数据都会生成一个订阅者,每个订阅者自身都有一个update函数,能够更新虚拟Dom节点;
  3. 当修改属性值的时候,会触发setter,依赖会立马通知Notify所有订阅者调用自身的update回调函数,重新渲染虚拟DOM,更新视图

vue2双向数据绑定原理源码

class Vue {
  // 参数为对象实例 这个对象用于告知vue需要挂载到哪个元素并挂载数据
  constructor(obj_instance) {
    // 给实例赋值对象的data属性
    this.$data = obj_instance.data;
    // 进行数据劫持 监听对象里属性的变化
    Observer(this.$data);
    Complie(obj_instance.el, this);
  }
}
//模板解析 —— 替换DOM内容 把vue实例上的数据解析到页面上
// 接收两个参数 1.vue实例挂载的元素<div id="app"> 2.vue实例
function Complie(element, vm) {
  vm.$el = document.querySelector(element);
  // 使用文档碎片来临时存放DOM元素 减少DOM更新
  const fragment = document.createDocumentFragment();
  let child;
  // 将页面里的子节点循环放入文档碎片
  while ((child = vm.$el.firstChild)) {
    fragment.appendChild(child);
  }
  fragment_compile(fragment);
  // 替换fragment里文本节点的内容
  function fragment_compile(node) {
    // 使用正则表达式去匹配并替换节点里的{{}}
    const pattern = /\{\{\s*(\S+)\s*\}\}/;
    if (node.nodeType === 3) {
      // 提前保存文本内容 否则文本在被替换一次后 后续的操作都会不生效
      // 打工人: {{name}}  => 打工人:西维 如果不保存后续修改name会匹配不到{{name}} 因为已经被替换
      const texts = node.nodeValue;
      // 获取正则表达式匹配文本字符串获得的所有结果
      const result_regex = pattern.exec(node.nodeValue);
      if (result_regex) {
        const arr = result_regex[1].split("."); // more.salary => ['more', 'salary']
        // 使用reduce归并获取属性对应的值 = vm.$data['more'] => vm.$data['more']['salary']
        const value = arr.reduce((total, current) => total[current], vm.$data);
        node.nodeValue = texts.replace(pattern, value);
        // 在节点值替换内容时 即模板解析的时候 添加订阅者
        // 在替换文档碎片内容时告诉订阅者如何更新 即告诉Watcher如何更新自己
        new Watcher(vm, result_regex[1], (newVal) => {
          node.nodeValue = texts.replace(pattern, newVal);
        });
      }
    }
    // 替换绑定了v-model属性的input节点的内容
    if (node.nodeType === 1 && node.nodeName === "INPUT") {
      const attr = Array.from(node.attributes);
      attr.forEach((item) => {
        if (item.nodeName === "v-model") {
          const value = item.nodeValue
            .split(".")
            .reduce((total, current) => total[current], vm.$data);
          node.value = value;
          new Watcher(vm, item.nodeValue, (newVal) => {
            node.value = newVal;
          });
          node.addEventListener("input", (e) => {
            // ['more', 'salary']
            const arr1 = item.nodeValue.split(".");
            // ['more']
            const arr2 = arr1.slice(0, arr1.length - 1);
            // vm.$data.more
            const final = arr2.reduce(
              (total, current) => total[current],
              vm.$data
            );
            // vm.$data.more['salary'] = e.target.value
            final[arr1[arr1.length - 1]] = e.target.value;
          });
        }
      });
    }
    // 对子节点的所有子节点也进行替换内容操作
    node.childNodes.forEach((child) => fragment_compile(child));
  }
  // 操作完成后将文档碎片添加到页面
  // 此时已经能将vm的数据渲染到页面上 但还未实现数据变动的及时更新
  vm.$el.appendChild(fragment);
}
//数据劫持 —— 监听实例里的数据
function Observer(data_instance) {
  // 递归出口
  if (!data_instance || typeof data_instance !== "object") return;
  // 每次数据劫持一个对象时都创建Dependency实例 用于区分哪个对象对应哪个依赖实例和收集依赖
  const dependency = new Dependency();
  Object.keys(data_instance).forEach((key) => {
    // 使用defineProperty后属性里的值会被修改 需要提前保存属性的值
    let value = data_instance[key];
    // 递归劫持data里的子属性
    Observer(value);
    Object.defineProperty(data_instance, key, {
      enumerable: true,
      configurable: true,
      // 收集数据依赖
      get() {
        // console.log(`获取了属性值 ${value}`);
        Dependency.target && dependency.addSub(Dependency.target);
        return value;
      },
      // 触发视图更新
      set(newVal) {
        console.log(`修改了属性值`);
        value = newVal;
        // 处理赋值是对象时的情况
        Observer(newVal);
        dependency.notify();
      },
    });
  });
}
//依赖 —— 实现发布-订阅模式 用于存放订阅者和通知订阅者更新
class Dependency {
  constructor() {
    this.subscribers = []; // 用于收集依赖data的订阅者信息
  }
  addSub(sub) {
    this.subscribers.push(sub);
    console.log(this.subscribers)
  }
  notify() {
    console.log(this.subscribers)
    this.subscribers.forEach((sub) => sub.update());
  }
}
// 订阅者
class Watcher {
  // 需要vue实例上的属性 以获取更新什么数据
  constructor(vm, key, callback) {
    this.vm = vm;
    this.key = key;
    this.callback = callback;
    //临时属性 —— 触发getter 把订阅者实例存储到Dependency实例的subscribers里面
    Dependency.target = this;
    key.split(".").reduce((total, current) => total[current], vm.$data);
    Dependency.target = null; // 防止订阅者多次加入到依赖实例数组里
  }
  update() {
    const value = this.key
      .split(".")
      .reduce((total, current) => total[current], this.vm.$data);
    this.callback(value);
  }
}

检测变化的注意事项

  1. Vue 无法检测
  2. property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
    比如:
var vm = new Vue({
  data:{
    formData:{
    	acNo: "89111"
    },
    list: []
  }
})

// `vm.formData.acNo` 是响应式的(当acNo变化的时候,视图有用到acNo的地方会自动更新)
vm.formData.acName = "小小"
// `vm.formData.acName` 是非响应式的(当a变化的时候,视图有用到b的地方不会自动更新)
  • 对于已经创建的实例对象,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。
Vue.set(vm.formData,'acName ',"小小")
//或者
this.$set(this.formData,'acName',"小小")
  • 数组

    Vue 不能检测以下数组的变动:
    当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
    当你修改数组的长度时,例如:vm.items.length = newLength

// Vue.set
this.$set(this.list, 2, "新值")
// Array.prototype.splice
this.list.splice(2, 1, "新值")
Logo

前往低代码交流专区

更多推荐