10分钟了解MVVM,实现简易MVVM

时间:2023-02-17 12:09:42

MVVM 是 Model-View-ViewModel 缩写,也就是把 MVC 中的 Controller 演变成 ViewModel。Model 层代表数据模型,View 代表 UI 组件,ViewModel 是 View 和 Model 层的桥梁,数据会绑定到 viewModel 层并自动将数据渲染到页面中,视图变化的时候会通知 viewModel 层更新数据。

  1. Model: 代表数据模型,也可以在 Model 中定义数据修改和操作的业务逻辑。我们可以把 Model 称为数据层,因为它仅仅关注数据本身,不关心任何行为
  2. View: 用户操作界面。当 ViewModel 对 Model 进行更新的时候,会通过数据绑定更新到 View
  3. ViewModel: 业务逻辑层,View 需要什么数据,ViewModel 要提供这个数据;View 有某些操作,ViewModel 就要响应这些操作,所以可以说它是 Model for View.

总结: MVVM 模式简化了界面与业务的依赖,解决了数据频繁更新。MVVM 在使用当中,利用双向绑定技术,使得 Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。

实现一个简易的 MVVM 分为这么几步来

1.类 Vue:这个类接收的是一个 options。
   el属性:根元素的id
   data属性:双向绑定的数据。
2.Dep 类:
   subNode数组:存放所依赖这个属性  的依赖,
   addSub方法:添加依赖的
   removeSub方法:删除,
   updata方法:遍历更新它subs中的所有依赖
   target静态属性:用来表示 '当前的观察者' ,依赖收集的时候可以将它添加到dep. subNode中



   静态的是指向类自身,而不是指向实例对象,主要是归属不同,这是静态属性的核心
3.observe 方法:监听数据变化(参数:data,也就是 options.data)
   遍历data:使用Object.defineProperty()来重写它的get和set,
   使用new Dep():实例化一个dep对象,
   get时:addSub 添加 '当前的观察者Dep.target' 完成依赖收集,
   set时:dep.updata 通知每一个依赖它的观察者进行更新
4.compile 方法:来将 HTML 模版和数据结合起来(参数:node 节点)。
   遍历它的所有子级,判断是否有firstElmentChild
       有:进行递归调用compile方法,
       没有:child.innderHTML用判断是否有(/\{\{(.*)\}\}/)需要双向绑定的数据,
           有:new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')替换msg变量。
5.将 Dep.target 指向当前的这个 child,
  调用this.opt.data[key]:触发这个数据的get来对当前的child进行依赖收集,
       目的:下次数据变化通知child进行视图更新,
   将Dep.target指为null(其实在Vue中是有一个targetStack栈用来存放target的指向的)
6.监听 document 的 DOMContentLoaded
   回调函数中实例化这个Vue对象就可以了

需要注意的点:

childNodes 会获取到所有的子节点以及文本节点(包括元素标签中的空白节点)
firstElementChild 表示获取元素的第一个字元素节点,以此来区分是不是元素节点,如果是的话则调用 compile 进行递归调用,否则用正则匹配

<!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>MVVM</title>
  </head>
  <body>
    <div >
      <h3>姓名</h3>
      <p>{{name}}</p>
      <h3>年龄</h3>
      <p>{{age}}</p>
    </div>
  </body>
</html>
<script>
  document.addEventListener(
    "DOMContentLoaded",
    function () {
      let opt = { el: "#app", data: { name: "等待修改...", age: 20 } };
      let vm = new Vue(opt);
      setTimeout(() => {
        opt.data.name = "jing";
      }, 2000);
    },
    false
  );
  class Vue {
    constructor(opt) {
      this.opt = opt;
      this.observer(opt.data);
      let root = document.querySelector(opt.el);
      this.compile(root);
    }
    observer(data) {
      Object.keys(data).forEach((key) => {
        let obv = new Dep();
        data["_" + key] = data[key];

        Object.defineProperty(data, key, {
          get() {
            Dep.target && obv.addSubNode(Dep.target);
            return data["_" + key];
          },
          set(newVal) {
            obv.update(newVal);
            data["_" + key] = newVal;
          },
        });
      });
    }
    compile(node) {
      [].forEach.call(node.childNodes, (child) => {
        if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) {
          let key = RegExp.$1.trim();
          child.innerHTML = child.innerHTML.replace(
            new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"),
            this.opt.data[key]
          );
          Dep.target = child;
          this.opt.data[key];
          Dep.target = null;
        } else if (child.firstElementChild) this.compile(child);
      });
    }
  }

  class Dep {
    constructor() {
      this.subNode = [];
    }
    addSubNode(node) {
      this.subNode.push(node);
    }
    update(newVal) {
      this.subNode.forEach((node) => {
        node.innerHTML = newVal;
      });
    }
  }
</script>