使用proxy實現一個簡單完整的MVVM庫

前言

本文首發在 前端開發博客

MVVM 是當前時代前端平常業務開發中的必備模式(相關框架如reactvueangular 等), 使用 MVVM 能夠將開發者的精力更專一於業務上的邏輯,而不須要關心如何操做 dom。雖然如今都 9012 年了,mvvm 相關原理的介紹已經爛大街了,但出於學習基礎知識的目的(使用 proxy 實現的 vue3.0 還在開發中), 在參考了以前 vue.js 的總體思路以後,本身動手實現了一個簡易的經過 proxy 實現的 mvvmjavascript

本項目代碼已經開源在 github,項目正在持續完善中,歡迎交流學習,喜歡請點個 star 吧!

最終效果

<html>
  <body>
    <div id="app">
      <div>{{title}}</div>
    </div>
  </body>
</html>
import MVVM from '@fe_korey/mvvm';
new MVVM({
  view: document.getElementById('app'),
  model: {
    title: 'hello mvvm!'
  },
  mounted() {
    console.log('主程編譯完成,歡迎使用MVVM!');
  }
});

結構概覽

  • Complier 模塊實現解析、收集指令,並初始化視圖
  • Observer 模塊實現了數據的監聽,包括添加訂閱者和通知訂閱者
  • Parser 模塊實現解析指令,提供該指令的更新視圖的更新方法
  • Watcher 模塊實現創建指令與數據的關聯
  • Dep 模塊實現一個訂閱中心,負責收集,觸發數據模型各值的訂閱列表

流程爲:Complier收集編譯好指令後,根據指令不一樣選擇不一樣的Parser,根據ParserWatcher中訂閱數據的變化並更新初始視圖。Observer監聽數據變化而後通知給 WatcherWatcher 再將變化結果通知給對應Parser裏的 update 刷新函數進行視圖的刷新。html

mvvm.js總體流程圖

模塊詳解

Complier

  • 將整個數據模型 data 傳入Observer模塊進行數據監聽前端

    this.$data = new Observer(option.model).getData();
  • 循環遍歷整個 dom,對每一個 dom 元素的全部指令進行掃描提取vue

    function collectDir(element) {
      const children = element.childNodes;
      const childrenLen = children.length;
    
      for (let i = 0; i < childrenLen; i++) {
        const node = children[i];
        const nodeType = node.nodeType;
    
        if (nodeType !== 1 && nodeType !== 3) {
          continue;
        }
        if (hasDirective(node)) {
          this.$queue.push(node);
        }
        if (node.hasChildNodes() && !hasLateCompileChilds(node)) {
          collectDir(element);
        }
      }
    }
  • 對每一個指令進行編譯,選擇對應的解析器Parserjava

    const parser = this.selectParsers({ node, dirName, dirValue, cs: this });
  • 將獲得的解析器Parser傳入Watcher,並初始化該 dom 節點的視圖node

    const watcher = new Watcher(parser);
    parser.update({ newVal: watcher.value });
  • 全部指令解析完畢後,觸發 MVVM 編譯完成回調$mounted()react

    this.$mounted();
  • 使用文檔碎片document.createDocumentFragment()來代替真實 dom 節點片斷,待全部指令編譯完成後,再將文檔碎片追加回真實 dom 節點git

    let child;
    const fragment = document.createDocumentFragment();
    while ((child = this.$element.firstChild)) {
      fragment.appendChild(child);
    }
    //解析完後
    this.$element.appendChild(fragment);
    delete $fragment;

Parser

  • Complier模塊編譯後的指令,選擇不一樣聽解析器解析,目前包括ClassParser,DisplayParser,ForParser,IfParser,StyleParser,TextParser,ModelParser,OnParser,OtherParser等解析模塊。es6

    switch (name) {
      case 'text':
        parser = new TextParser({ node, dirValue, cs });
        break;
      case 'style':
        parser = new StyleParser({ node, dirValue, cs });
        break;
      case 'class':
        parser = new ClassParser({ node, dirValue, cs });
        break;
      case 'for':
        parser = new ForParser({ node, dirValue, cs });
        break;
      case 'on':
        parser = new OnParser({ node, dirName, dirValue, cs });
        break;
      case 'display':
        parser = new DisplayParser({ node, dirName, dirValue, cs });
        break;
      case 'if':
        parser = new IfParser({ node, dirValue, cs });
        break;
      case 'model':
        parser = new ModelParser({ node, dirValue, cs });
        break;
      default:
        parser = new OtherParser({ node, dirName, dirValue, cs });
    }
  • 不一樣的解析器提供不一樣的視圖刷新函數update(),經過update更新dom視圖github

    //text.js
    function update(newVal) {
      this.el.textContent = _toString(newVal);
    }
  • OnParser 解析事件綁定,與數據模型中的 methods字段對應

    //詳見 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/on.ts
    el.addEventListener(handlerType, e => {
      handlerFn(scope, e);
    });
  • ForParser 解析數組

    //詳見 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/for.ts
  • ModelParser 解析雙向綁定,目前支持input[text/password] & textarea,input[radio],input[checkbox],select四種狀況的雙向綁定,雙綁原理:

    • 數據變化更新表單:跟其餘指令更新視圖同樣,經過update方法觸發更新表單的value

      function update({ newVal }) {
        this.model.el.value = _toString(newVal);
      }
    • 表單變化更新數據:監聽表單變化事件如input,change,在回調裏set數據模型

      this.model.el.addEventListener('input', e => {
        model.watcher.set(e.target.value);
      });

Observer

  • MVVM 模型中的核心,通常經過 Object.definePropertygetset 方法進行數據的監聽,在 get 裏添加訂閱者,set 裏通知訂閱者更新視圖。在本項目採用 Proxy 來實現數據監聽,好處有三:

    • Proxy 能夠直接監聽對象而非屬性
    • Proxy 能夠直接監聽數組的變化
    • Proxy 有多達 13 種攔截方法,查閱
      而劣勢是兼容性問題,且沒法經過 polyfill 磨平。查閱兼容性
  • 注意 Proxy 只會監聽自身的每個屬性,若是屬性是對象,則該對象不會被監聽,因此須要遞歸監聽
  • 設置監聽後,返回一個 Proxy 替代原數據對象
var proxy = new Proxy(data, {
  get: function(target, key, receiver) {
    //若是知足條件則添加訂閱者
    dep.addDep(curWatcher);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    //若是知足條件則通知訂閱者
    dep.notfiy();
    return Reflect.set(target, key, value, receiver);
  }
});

Watcher

  • Complier 模塊裏對每個解析後的 Parser 進行指令與數據模型直接的綁定,並觸發 Observerget 監聽,添加訂閱者(Watcher

    this._getter(this.parser.dirValue)(this.scope || this.parser.cs.$data);
  • 當數據模型變化時,就會觸發 -> Observerset 監聽 -> Depnotfiy 方法(通知訂閱者的全部訂閱列表) -> 執行訂閱列表全部 Watcherupdate 方法 -> 執行對應 Parserupdate -> 完成更新視圖
  • Watcher 裏的 set 方法用於設置雙向綁定值,注意訪問層級

Dep

  • MVVM 的訂閱中心,在這裏收集數據模型的每一個屬性的訂閱列表
  • 包含添加訂閱者,通知訂閱者等方法
  • 本質是一種發佈/訂閱模式
class Dep {
  constructor() {
    this.dependList = [];
  }
  addDep() {
    this.dependList.push(dep);
  }
  notfiy() {
    this.dependList.forEach(item => {
      item.update();
    });
  }
}

後記

目前該 mvvm 項目只實現了數據綁定視圖更新的功能,經過這個簡易輪子的實現,對 dom 操做,proxy發佈訂閱模式等若干基礎知識都進行了再次理解,查漏補缺。同時歡迎你們一塊兒探討交流,後面會繼續完善!

相關文章
相關標籤/搜索