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

模拟一个MVVM框架-Vue实现思路

程序员文章站 2024-01-31 12:24:16
...

模拟一个MVVM框架-Vue实现思路

MVVM 框架介绍

M(Model,模型层 ),
V(View,视图层),
VM(ViewModel,视图模型,V 与 M 连接的桥梁)

MVVM 框架实现了数据双向绑定
当 M 层数据进行修改时,VM 层会监测到变化,并且通知 V 层进行相应的修改
修改 V 层则会通知 M 层数据进行修改
MVVM 框架实现了视图与模型层的相互解耦
模拟一个MVVM框架-Vue实现思路

几种双向数据绑定的方式

  • 发布-订阅者模式(backbone.js)

    • 一般通过 pub、sub 的方式来实现数据和视图的绑定,但是使用起来比较麻烦
  • 脏值检查(angular.js)

    • angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图。类似于通过定时器轮训检测数据是否发生了改变。
  • 数据劫持

    • vue.js 则是采用数据劫持结合发布者-订阅者模式的方式。通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
      注意:vuejs 不兼容 IE8 以以下的版本

模拟一个MVVM框架-Vue实现思路

Vue 实现思路

  • 实现一个 Compiler 模板解析器,能够对模版中的指令和插值表达式进行解析,并且赋予不同的操作
  • 实现一个 Observer 数据监听器,能够对数据对象的所有属性进行监听
  • 实现一个 Watcher 观察者,将 Compile 的解析结果,与 Observer 所观察的对象连接起来,建立关系,在 Observer 观察到对象数据变化时,接收通知,同时更新 DOM
  • 创建一个公共的入口对象,接收初始化的配置并且协调上面三个模块,也就是 vue

模拟一个MVVM框架-Vue实现思路

Complier

/**
 * Compile(解析器) 专门负责解析模板内容
 */
class Compile {
 // 接收vue中传进来的el 和 vue实例
 constructor(el, vm) {
  //判断传入的是dom对象还是选择器
  //根节点对象
  this.el = typeof el === "string" ? document.querySelector(el) : el;
  //vm实例
  this.vm = vm;
  if (this.el) {
   /**
    * 文档碎片化
    */
   // 1. 把el中所有的子节点都放到内存中
   let fragement = this.nodeFragement(this.el);
   // console.dir(fragement);
   // 2. 在内存中编译文档碎片
   this.compile(fragement);
   // 3. 把fragement 添加到dom中
   this.el.appendChild(fragement);
  }
 }
 /**
  * 核心方法
  */
 // 把el中所有的子节点都放到内存中
 nodeFragement(node) {
  // 创建一个fragement  fragement 相当于预编译将渲染dom放在内存中进行
  let fragement = document.createDocumentFragment();
  // 把el的子节点挨个添加到文档碎片中
  let childNodes = node.childNodes;
  this.toArray(childNodes).forEach((item) => {
   // 已经将所有节点都添加到文档碎片中
   fragement.appendChild(item);
  });
  return fragement;
 }
 // 在内存中编译文档碎片
 compile(fragement) {
  let childNodes = fragement.childNodes;
  this.toArray(childNodes).forEach((node) => {
   // 判断节点是文本节点还是元素节点
   // console.dir(node)
   if (this.isElementNode(node)) {
    // 如果是元素节点
    this.compileElementNode(node);
   }
   if (this.isTextNode(node)) {
    // 如果是文本节点
    this.compileTextNode(node);
   }
   // 判断节点内是否还有子节点 如果还有就递归排查
   if (node.childNodes && node.childNodes.length > 0) {
    this.compile(node);
   }
  });
 }
 // 编译元素节点
 compileElementNode(node) {
  // console.log('编译元素节点');
  // console.dir(node);
  // 获取所有属性集合转化数组
  let attributes = node.attributes;
  this.toArray(attributes).forEach((attribute) => {
   // console.dir(attribute)
   // 属性名
   let attributeName = attribute.name;
   // console.log(attributeName);
   // 属性值
   let attributeValue = attribute.value;
   // 判断是否是指令
   if (this.isDirective(attributeName)) {
    let type = attributeName.slice(2);
    //  判断是否是v-on指令
    if (this.isEventDirective(type)) {
     //使用解析工具 使用解析工具能够更好的解耦
     conpileUtils.eventConpile(node, this.vm, attributeValue, type);
    } else {
     //使用解析工具能够更好的解耦
     conpileUtils[type](node, this.vm, attributeValue);
    }
   }
  });
 }
 // 编译文本节点
 compileTextNode(node) {
  // console.log('编译文本节点');
  // 调用conpileUtils插值表达式解析器
  conpileUtils.mustache(node, this.vm);
 }

 /**
  * 工具方法
  */
 // 转化数组
 toArray(arr) {
  // 将伪数组转化为数组
  return [].slice.call(arr);
 }
 // 判断是否元素节点
 isElementNode(node) {
  // nodetype 属性 1元素节点 3文本节点
  return node.nodeType === 1;
 }
 // 判断是否文本节点
 isTextNode(node) {
  // nodetype 属性 1元素节点 3文本节点
  return node.nodeType === 3;
 }
 //判断是不是v-开头
 isDirective(attributeName) {
  return attributeName.startsWith("v-");
 }
 // 判断是不是on指令事件指令
 isEventDirective(type) {
  return type.split(":")[0] === "on";
 }
}
/**
 * conpile解析工具 能够更好的解耦方便后期维护
 * 把所有跟解析有关的工具就放到这里
 */
let conpileUtils = {
 // 把下面的一坨分配到这个工具里
 //   if (type ==='text') {
 //     // document.body.textContent
 //     node.textContent = this.vm.$data[attributeValue]
 //   }
 //   if (type ==='html') {
 //     // document.body.innerHTML
 //     node.innerHTML = this.vm.$data[attributeValue]
 //   }
 //   if (type ==='model') {
 //     // document.body.innerHTML
 //     node.value = this.vm.$data[attributeValue]
 //   }
 // 解析普通指令
 // text
 // expr 就是data中绑定的属性名,也就是v-xxx=的值
 text(node, vm, expr) {
  node.textContent = this.getVmValue(vm, expr);
  //当值改变时会调用watcher中的updata,updata会调用回调函数并把新旧值作为实参传入
  //使用了订阅者模式每次new Watcher的时候都会讲实例存到Dep.subs中 当值发生改变会统一调用实例的updata方法
  new Watcher(vm, expr, (newValue) => {
   // 拿到新值更新视图
   node.textContent = newValue;
  });
 },
 // hmtl
 html(node, vm, expr) {
  node.innerHTML = this.getVmValue(vm, expr);
  //当值改变时会调用watcher中的updata,updata会调用回调函数并把新旧值作为实参传入
  //使用了订阅者模式每次new Watcher的时候都会讲实例存到Dep.subs中 当值发生改变会统一调用实例的updata方法
  new Watcher(vm, expr, (newValue) => {
   // 拿到新值更新视图
   node.innerHTML = newValue;
  });
 },
 // model
 model(node, vm, expr) {
  node.value = this.getVmValue(vm, expr);
  let that = this;
  // 监听输入框的input事件 当发生改变时就让 vm.$data[expr] = 输入值
  node.addEventListener("input", function () {
   // 调用setVmValue 方法,对象类型也能设置
   that.setVmValue(vm, expr, this.value);
  });
  //当值改变时会调用watcher中的updata,updata会调用回调函数并把新旧值作为实参传入
  //使用了订阅者模式每次new Watcher的时候都会讲实例存到Dep.subs中 当值发生改变会统一调用实例的updata方法
  new Watcher(vm, expr, (newValue) => {
   // 拿到新值更新视图
   node.value = newValue;
  });
 },
 // 解析事件指令
 eventConpile(node, vm, expr, type) {
  // 分割事件名
  let eventName = type.split(":")[1];
  // 判断是否有methods并且有内部处理的方法
  let mothod = vm.$methods && vm.$methods[expr];
  // 如果有事件名,并且在methods中声明的处理方法再执行下面的操作
  // 要不然bind就会报错 bind只能给函数调用
  if (eventName && mothod) {
   // 给当前节点注册事件,使用vue实例的事件,并且将this指向vue实例
   node.addEventListener(eventName, vm.$methods[expr].bind(vm));
  }
 },
 // 解析文本节点插值表达式,小胡子语法
 mustache(node, vm) {
  // 获取节点的文本内容
  let text = node.textContent;
  // console.log(text);
  // 创建一个正则验证 并且将匹配正则的内容分组
  let reg = /\{\{(.+)\}\}/;
  if (reg.test(text)) {
   // RegExp.$1可以获取分组的第一组内容
   let key = RegExp.$1;
   // 配备正则的内容替换为data中的数据
   // 在插值表达式中的数据就是data中的属性名所以紫瑶把key传入getVmValue即可
   node.textContent = text.replace(reg, this.getVmValue(vm, key));
   //当值改变时会调用watcher中的updata,updata会调用回调函数并把新旧值作为实参传入
   //使用了订阅者模式每次new Watcher的时候都会讲实例存到Dep.subs中 当值发生改变会统一调用实例的updata方法
   new Watcher(vm, key, (newValue) => {
    // 拿到新值更新视图
    node.textContent = text.replace(reg, newValue);
   });
  }
 },
 // 设计一个方法用于解析data中的普通数据类型和复杂数据类型
 getVmValue(vm, expr) {
  let data = vm.$data;
  expr.split(".").forEach((key) => {
   // 在这里如果是单纯字符串就只会遍历一次
   // 如果是对象就会遍历两次第一次data会变成key
   // 第二次data就会等于每一项的值
   data = data[key];
  });
  return data;
 },
 setVmValue(vm, expr, value) {
  let data = vm.$data;
  let arr = expr.split(".");
  arr.forEach((key, index) => {
   // 当item不是arr中最后一个的时候
   // debugger
   if (index < arr.length - 1) {
    // 让data变成最后需要设置的对象
    data = data[key];
   } else {
    // 是最后一个的时候就是需要设置的属性了
    data[key] = value;
   }
  });
 },
};

Observer

class Observer {
 constructor(data) {
  this.data = data;
  this.walk(data);
 }
 /**
  * 核心方法
  */
 // 遍历data把每个属性或属性中的属性都添加劫持
 walk(data) {
  // 如果没有data或者data不是一个对象就return, 不需要劫持
  if (!data || typeof data !== "object") {
   return;
  }
  // 遍历属性
  Object.keys(data).forEach((key) => {
   this.defineReactive(data, key, data[key]);
   // 优化:如果data中的数据时对象那就直接用自己在递归劫持一遍
   // 如果就是简单数据类型,进来后会直接return掉
   this.walk(data[key]);
  });
 }
 // 用来劫持的方法
 defineReactive(data, key, value) {
  let that = this;
  // 在每个属性中创建一个dep用来监听数据改变
  let dep = new Dep();
  Object.defineProperty(data, key, {
   configurable: true, //当且仅当该属性为 true 时,该属性描述符才能够被改变
   enumerable: true, //当且仅当该属性的为true时,该属性才才能够被遍历(就是被枚举)
   get() {
    // 如果有Dep.target中不是null
    // 就把Dep.target(watcher)添加到当前的Dep实例的订阅者列表中
    Dep.target && dep.addSub(Dep.target);
    console.log(`${key}属性被访问了`, value);
    return value;
   },
   set(newValue) {
    console.log(`${key}属性被设置了`, newValue);
    value = newValue;
    //优化:如果新值是个对象就用walk在劫持一遍
    //不是那也不用担心,方法就直接被return了
    that.walk(newValue);
    //当值发生改变调用watcher的updata方法,催动视图更新
    //这里使用Dep的通知方法,通知每个订阅者(每个watcher实例)调用自己的updata方法
    dep.notify();
   },
  });
 }
}

Watcher

/**
 * 创建一个Watcher 用于关联Observer和Compile
 */
class Watcher {
 // vm就是vue实例,expr是data中的key,cd是需要执行的回调函数
 constructor(vm, expr, cd) {
  this.vm = vm;
  this.expr = expr;
  this.cd = cd;
  // 把当前的watcher 实例存到 Dep.target中
  Dep.target = this;
  // getVmValue 会进到observe中的get中 所以在get中就可以访问到 Dep.target
  // 把旧值(初始值)存起来, 在updata中用来对比
  this.oldValue = this.getVmValue(vm, expr);
  // 进到get中存储完当前watcher实例后,清空Dep.target方便下次存储
  Dep.target = null;
 }
 /**
  * 数据更新调用updata方法
  */
 upData() {
  let oldValue = this.oldValue;
  /**
   * 关于这里为什么是新值,因为调用这个方法就说明数据已经更新了
   * 这里传进来的vm就是更新后的vm了,所以调用getVmValue就拿到新值了
   */
  let newValue = this.getVmValue(this.vm, this.expr);
  // 如果新值和旧值不相对,就调用cd 回调函数将newValue和oldValue传进去
  if (newValue !== oldValue) {
   this.cd(newValue, oldValue);
  }
 }
 // 设计一个方法用于解析data中的普通数据类型和复杂数据类型
 // 将complie中的方法复制过来用
 getVmValue(vm, expr) {
  let data = vm.$data;
  expr.split(".").forEach((key) => {
   // 在这里如果是单纯字符串就只会遍历一次
   // 如果是对象就会遍历两次第一次data会变成key
   // 第二次data就会等于每一项的值
   data = data[key];
  });
  return data;
 }
}
/**
 * 订阅者、发布者模式
 * 创建一个Dep对象用于管理这个订阅者也就是每个watcher实例
 * 并且在改变后通知每个订阅者
 */

class Dep {
 constructor() {
  // 用于储存所有订阅者
  this.subs = [];
 }
 // 新增订阅者方法
 addSub(watcher) {
  // 新增订阅者放到数组中
  this.subs.push(watcher);
 }
 // 通知所有的订阅者更新
 notify() {
  this.subs.forEach((sub) => {
   // 调用upData方法
   sub.upData();
  });
 }
}

发布-订阅者模式

  • 发布-订阅者模式,也叫观察者模式
  • 它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合。
  • 例子:微信公众号
    • 订阅者:只需要要订阅微信公众号
    • 发布者(公众号):发布新文章的时候,推送给所有订阅者
    • 优点:
      • 解耦合
      • 订阅者不用每次去查看公众号是否有新的文章
      • 发布者不用关心谁订阅了它,只要给所有订阅者推送即可

Dep 中使用的就是发布-订阅者模式

html

<!DOCTYPE html>
<html lang="en">
 <head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  <title>Document</title>
 </head>

 <body>
  <div id="app">
   <p v-text="msg"></p>
   <p>{{msg}}</p>
   <p>{{car.brand}}</p>
   <p v-html="msg"></p>
   <input type="text" v-model="msg" />
   <button v-on:click="clickFn">按钮</button>
  </div>
  <script src="./src/watcher.js"></script>
  <script src="./src/observe.js"></script>
  <script src="./src/compile.js"></script>
  <script src="./src/vue.js"></script>
  <script>
   const vm = new Vue({
    el: "#app",
    data: {
     msg: "hello vue111",
     car: {
      brand: "宝马",
     },
    },
    methods: {
     clickFn() {
      // 在vue的methods中this应该指向当前实例
      this.msg = "哈哈";
      this.car.brand = "奔驰";
     },
    },
   });
  </script>
 </body>
</html>
  <script src="./src/observe.js"></script>
  <script src="./src/compile.js"></script>
  <script src="./src/vue.js"></script>
  <script>
   const vm = new Vue({
    el: "#app",
    data: {
     msg: "hello vue111",
     car: {
      brand: "宝马",
     },
    },
    methods: {
     clickFn() {
      // 在vue的methods中this应该指向当前实例
      this.msg = "哈哈";
      this.car.brand = "奔驰";
     },
    },
   });
  </script>
 </body>
</html>