10分鐘瞭解MVVM,實現簡易MVVM

久宇詩發表於2023-02-17

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 id="app">
      <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>

相關文章