對類Vue的MVVM前端庫的實現

来源:https://www.cnblogs.com/wuvkcyan/archive/2018/09/07/9602562.html
-Advertisement-
Play Games

關於實現MVVM,網上實在是太多了,本文為個人總結,結合源碼以及一些別人的實現 關於雙向綁定 vue 數據劫持 + 訂閱 發佈 ng 臟值檢查 backbone.js 訂閱 發佈(這個沒有使用過,並不是主流的用法) 雙向綁定,從最基本的實現來說,就是在defineProperty綁定的基礎上在綁定i ...


關於實現MVVM,網上實在是太多了,本文為個人總結,結合源碼以及一些別人的實現

關於雙向綁定

  • vue 數據劫持 + 訂閱 - 發佈
  • ng 臟值檢查
  • backbone.js 訂閱-發佈(這個沒有使用過,並不是主流的用法)

雙向綁定,從最基本的實現來說,就是在defineProperty綁定的基礎上在綁定input事件,達到v-model的功能

代碼思路圖

兩個版本:

  • 簡單版本: 非常簡單,但是因為是es6,並且代碼極度簡化,所以不談功能,思路還是很清晰的
  • 標準版本: 參照了Vue的部分源碼,代碼的功能高度向上抽取,閱讀稍微有點困難,實現了基本的功能,包括計算屬性,watch,核心功能都實現沒問題,但是不支持數組

簡單版本

簡單版本的地址: 簡單版本

​ 這個MVVM也許代碼邏輯上面實現的並不完美,並不是正統的MVVM, 但是代碼很精簡,相對於源碼,要好理解很多,並且實現了v-model以及v-on methods的功能,代碼非常少,就100多行

class MVVM {
  constructor(options) {
    const {
      el,
      data,
      methods
    } = options
    this.methods = methods
    this.target = null
    this.observer(this, data)
    this.instruction(document.getElementById(el)) // 獲取掛載點
  }

  // 數據監聽器 攔截所有data數據 傳給defineProperty用於數據劫持
  observer(root, data) {
    for (const key in data) {
      this.definition(root, key, data[key])
    }
  }

  // 將攔截的數據綁定到this上面
  definition(root, key, value) {
    // if (typeof value === 'object') { // 假如value是對象則接著遞歸
    //   return this.observer(value, value)
    // }
    let dispatcher = new Dispatcher() // 調度員

    Object.defineProperty(root, key, {
      set(newValue) {
        value = newValue
        dispatcher.notify(newValue)
      },
      get() {
        dispatcher.add(this.target)
        return value
      }
    })
  }

  //指令解析器
  instruction(dom) {
    const nodes = dom.childNodes; // 返回節點的子節點集合
    // console.log(nodes); //查看節點屬性
    for (const node of nodes) { // 與for in相反 for of 獲取迭代的value值
      if (node.nodeType === 1) { // 元素節點返回1
        const attrs = node.attributes //獲取屬性

        for (const attr of attrs) {
          if (attr.name === 'v-model') {
            let value = attr.value //獲取v-model的值

            node.addEventListener('input', e => { // 鍵盤事件觸發
              this[value] = e.target.value
            })
            this.target = new Watcher(node, 'input') // 儲存到訂閱者
            this[value] // get一下,將 this.target 給調度員
          }
          if (attr.name == "@click") {
            let value = attr.value // 獲取點擊事件名
            
            node.addEventListener('click',
              this.methods[value].bind(this)
            )
          }
        }
      }

      if (node.nodeType === 3) { // 文本節點返回3
        let reg = /\{\{(.*)\}\}/; //匹配 {{  }}
        let match = node.nodeValue.match(reg)
        if (match) { // 匹配都就獲取{{}}裡面的變數
          const value = match[1].trim()
          this.target = new Watcher(node, 'text')
          this[value] = this[value] // get set更新一下數據
        }
      }
    }

  }
}

//調度員 > 調度訂閱發佈
class Dispatcher {
  constructor() {
    this.watchers = []
  }
  add(watcher) {
    this.watchers.push(watcher) // 將指令解析器解析的數據節點的訂閱者存儲進來,便於訂閱
  }
  notify(newValue) {
    this.watchers.map(watcher => watcher.update(newValue))
    // 有數據發生,也就是觸發set事件,notify事件就會將新的data交給訂閱者,訂閱者負責更新
  }
}

//訂閱發佈者 MVVM核心
class Watcher {
  constructor(node, type) {
    this.node = node
    this.type = type
  }
  update(value) {
    if (this.type === 'input') {
      this.node.value = value // 更新的數據通過訂閱者發佈到dom
    }
    if (this.type === 'text') {
      this.node.nodeValue = value
    }
  }
}
<!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">
    <input type="text" v-model="text">{{ text }}
    <br>
    <button @click="update">重置</button>
  </div>

  <script src="./index.js"></script>
  <script>
    let mvvm = new MVVM({
      el: 'app',
      data: {
        text: 'hello MVVM'
      },
      methods: {
        update() {
          this.text = ''
        }
      }
    })
  </script>
</body>

</html>

這個版本的MVVM因為代碼比較少,並且是ES6的原因,思路非常清晰

我們來看看從new MVVM開始,他都做了什麼

解讀簡單版本

new MVVM

首先,通過解構獲取所有的new MVVM傳進來的對象

class MVVM {
  constructor(options) {
    const {
      el,
      data,
      methods
    } = options
    this.methods = methods // 提取methods,便於後面將this給methods
    this.target = null // 後面有用
    this.observer(this, data)
    this.instruction(document.getElementById(el)) // 獲取掛載點
  }

屬性劫持

開始執行this.observer observer是一個數據監聽器,將data的數據全部攔截下來

observer(root, data) {
    for (const key in data) {
      this.definition(root, key, data[key])
    }
  }

在this.definition裡面把data數據都劫持到this上面

 definition(root, key, value) {
    if (typeof value === 'object') { // 假如value是對象則接著遞歸
      return this.observer(value, value)
    }
    let dispatcher = new Dispatcher() // 調度員

    Object.defineProperty(root, key, {
      set(newValue) {
        value = newValue
        dispatcher.notify(newValue)
      },
      get() {
        dispatcher.add(this.target)
        return value
      }
    })
  }

此時data的數據變化我們已經可以監聽到了,但是我們監聽到後還要與頁面進行實時相應,所以這裡我們使用調度員,在頁面初始化的時候get(),這樣this.target,也就是後面的指令解析器解析出來的v-model這樣的指令儲存到調度員裡面,主要請看後面的解析器的代碼

指令解析器

指令解析器通過執行 this.instruction(document.getElementById(el)) 獲取掛載點

instruction(dom) {
    const nodes = dom.childNodes; // 返回節點的子節點集合
    // console.log(nodes); //查看節點屬性
    for (const node of nodes) { // 與for in相反 for of 獲取迭代的value值
      if (node.nodeType === 1) { // 元素節點返回1
        const attrs = node.attributes //獲取屬性

        for (const attr of attrs) {
          if (attr.name === 'v-model') {
            let value = attr.value //獲取v-model的值

            node.addEventListener('input', e => { // 鍵盤事件觸發
              this[value] = e.target.value
            })
            this.target = new Watcher(node, 'input') // 儲存到訂閱者
            this[value] // get一下,將 this.target 給調度員
          }
          if (attr.name == "@click") {
            let value = attr.value // 獲取點擊事件名
            
            node.addEventListener('click',
              this.methods[value].bind(this)
            )
          }
        }
      }

      if (node.nodeType === 3) { // 文本節點返回3
        let reg = /\{\{(.*)\}\}/; //匹配 {{  }}
        let match = node.nodeValue.match(reg)
        if (match) { // 匹配都就獲取{{}}裡面的變數
          const value = match[1].trim()
          this.target = new Watcher(node, 'text')
          this[value] = this[value] // get set更新一下數據
        }
      }
    }

  }

這裡代碼首先解析出來我們自定義的屬性然後,我們將@click的事件直接指向methods,methds就已經實現了

現在代碼模型是這樣

調度員Dispatcher與訂閱者Watcher

我們需要將Dispatcher和Watcher聯繫起來

於是我們之前創建的變數this.target開始發揮他的作用了

正執行解析器裡面使用this.target將node節點,以及觸發關鍵詞存儲到當前的watcher 訂閱,然後我們獲取一下數據

this.target = new Watcher(node, 'input') // 儲存到訂閱者
this[value] // get一下,將 this.target 給調度員

在執行this[value]的時候,觸發了get事件

get() {
   dispatcher.add(this.target)
   return value
}

這get事件裡面,我們將watcher訂閱者告知到調度員,調度員將訂閱事件存儲起來

//調度員 > 調度訂閱發佈
class Dispatcher {
  constructor() {
    this.watchers = []
  }
  add(watcher) {
    this.watchers.push(watcher) // 將指令解析器解析的數據節點的訂閱者存儲進來,便於訂閱
  }
  notify(newValue) {
    this.watchers.map(watcher => watcher.update(newValue))
    // 有數據發生,也就是觸發set事件,notify事件就會將新的data交給訂閱者,訂閱者負責更新
  }
}

與input不太一樣的是文本節點不僅需要獲取,還需要set一下,因為要讓訂閱者更新node節點

this.target = new Watcher(node, 'text')
this[value] = this[value] // get set更新一下數據

所以在訂閱者就添加了該事件,然後執行set

set(newValue) {
        value = newValue
        dispatcher.notify(newValue)
      },

notfiy執行,訂閱發佈者執行update更新node節點信息

class Watcher {
  constructor(node, type) {
    this.node = node
    this.type = type
  }
  update(value) {
    if (this.type === 'input') {
      this.node.value = value // 更新的數據通過訂閱者發佈到dom
    }
    if (this.type === 'text') {
      this.node.nodeValue = value
    }
  }
}

頁面初始化完畢

更新數據

node.addEventListener('input', e => { // 鍵盤事件觸發
   this[value] = e.target.value
})

this[value]也就是data數據發生變化,觸發set事件,既然觸發notfiy事件,notfiy遍歷所有節點,在遍歷的節點裡面根據頁面初始化的時候訂閱的觸發類型.進行頁面的刷新

現在可以完成的看看new MVVM的實現過程了

最簡單版本的MVVM完成

標準版本

標準版本額外實現了component,watch,因為模塊化代碼很碎的關係,看起來還是有難度的

從理念上來說,實現的思想基本是一樣的,可以參照上面的圖示,都是開始的時候都是攔截屬性,解析指令

代碼有將近300行,所以就貼一個地址標準版本MVVM

執行順序

  1. new MVVM
  2. 獲取$options = 所以參數
  3. 獲取data,便於後面劫持
  4. 因為是es5,後面forEach內部指向window,這不是我們想要的,所以存儲當前this 為me
  5. _proxyData劫持所有data數據
  6. 初始化計算屬性
  7. 通過Object.key()獲取計算屬性的屬性名
  8. 初始化計算屬性將計算屬性掛載到vm上
  9. 開始observer監聽數據
  10. 判斷data是否存在
  11. 存在就new Observer(創建監聽器)
  12. 數據全部進行進行defineProperty存取監聽處理,讓後面的數據變動都觸發這個的get/set
  13. 開始獲取掛載點
  14. 使用querySelector對象解析el
  15. 創建一個虛擬節點,並存儲當前的dom
  16. 解析虛擬dom
  17. 使用childNodes解析對象
  18. 因為是es5,所以使用[].slice.call將對象轉數組
  19. 獲取到後進行 {{ }}匹配 指令的匹配 以及遞歸子節點
  20. 指令的匹配: 匹配到指令因為不知道多少個指令名稱,所以這裡還是使用[].slice.call迴圈遍歷
  21. 解析到有 v-的指令使用substring(2)截取後面的屬性名稱
  22. 再判斷是不是指令v-on 這裡就是匹配on關鍵字,匹配到了就是事件指令,匹配不到就是普通指令
  23. 普通指令解析{{ data }} _getVMValget會觸發MVVM的_proxyData事件 在_proxyData事件裡面觸發data的get事件
  24. 這時候到了observer的defineReactive的get裡面獲取到了數據,因為沒有Dispatcher.target,所以不進行會觸發調度員
  25. 至此_getVMVal獲取到了數據
  26. modelUpdater進行Dom上面的數據更新
  27. 數據開始進行訂閱,在訂閱裡面留一個回調函數用於更新dom
  28. 在watcher(訂閱者)獲取this訂閱的屬性回調
  29. 在this.getter這個屬性上面返回一個匿名函數,用於獲取data的值
  30. 觸發get事件,將當前watcher的this存儲到Dispatcher.garget上面
  31. 給this.getters,callvm的的this,執行匿名函數,獲取劫持下來的data,又觸發了MVVM的_proxyData的get事件,繼而有觸發了observer的defineReactive的get事件,不過這一次Dispatcher.target有值,執行了depend事件
  32. depend裡面執行了自己的addDep事件,並且將Observer自己的this傳進去
  33. addDep裡面執行了DispatcheraddSub事件,
  34. addUsb事件裡面將訂閱存儲到Dispatcher裡面的this.watchers裡面的
  35. 訂閱完成,後面將這些自定義的指令進行移除
  36. 重覆操作,解析所有指令,v-on:click = "data"直接執行methods[data].bind(vm)

更新數據:

  1. 觸發input事件
  2. 觸發_setVMVal事件
  3. 觸發MVVM的set事件
  4. 觸發observer的set事件
  5. 觸發dep.notify()
  6. 觸發watcher的run方法
  7. 觸發new Watcher的回調 this.cb
  8. 觸發compile裡面的updaterFn 事件
  9. 更新視圖

component的實現

計算屬性的觸發 查看這個例子

computed: {
        getHelloWord: function () {
          return this.someStr + this.child.someStr;
        }
      },

其實計算屬性就是defineproperty的一個延伸

  1. 首先compile裡面解析獲取到{{ getHelloword }}'
  2. 執行updater[textUpdater]
  3. 執行_getVMVal獲取計算屬性的返回值
  4. 獲取vm[component]就會執行下麵的get事件
Object.defineProperty(me, key, {
          get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
          set: function () {}
        })

是function執行computed[getHelloword],也就是return 的 函數

this.someStr + this.child.someStr;
  1. 依次獲取data,觸發mvvm的get 以及observer的get,

初始化完成,到這裡還沒有綁定數據,僅僅是初始化完成了

  1. 開始訂閱該事件 new Watcher()
  2. component不是函數所以不是function 執行this.parseGetter(expOrFn);
  3. 返回一個覆蓋expOrrn的匿名函數
  4. 開始初始化 執行get()
  5. 存儲當前this,開始獲取vm[getHelloword]
  6. 觸發component[getHelloword]
  7. 開始執行MVVM的get this.someStr
  8. 到MVVM的get 到 observer的get 因為 Dispatcher.target存著 getHelloWord 的 this.depend ()所以執行
  9. Dispatcher的depend(),執行watcher的addDep(),執行 Dispatcher的addSub() 將當前的watcher存儲到監聽器
  10. 開始get第二個數據 this.child.someStr,同理也將getHelloWord的this存入了當前的Dispatcher
  11. 開始get第三個數據 this.child,同理也將getHelloWord的this存入了當前的Dispatcher

這個執行順序有點迷,第二第三方反來了

this.parseGetter(expOrFn);就執行完畢了

目前來看為什麼component會實時屬性數據?

因為component的依賴屬性一旦發生變化都會更新 getHelloword 的 watcher ,隨之執行回調更新dom

watch的實現

watch的實現相對來說要簡單很多

  1. 我們只要將watch監聽的數據告訴訂閱者就可以了
  2. 這樣,wacth更新了
  3. 觸發set,set觸發notify
  4. notify更新watcher
  5. watcher執行run
  6. run方法去執行watch的回調
  7. 即完成了watch的監聽
watch: function (key, cb) {
    new Watcher(this, key, cb)
},

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 使用SSMS資料庫管理工具修改UNIQUE約束 1、連接資料庫,選擇數據表-》右鍵點擊-》選擇設計(或者展開鍵,選擇要修改的鍵,右鍵點擊,選擇修改,後面步驟相同)。 2、選擇要修改的數據列-》右鍵點擊-》選擇索引/鍵。 3、在索引/鍵彈出框中-》選擇要修改的唯一約束-》選擇約束類型-》點擊列。 4、 ...
  • 一:個人看法 Mysql Group Replication 隨著5.7發佈3年了。作為技術愛好者。mgr 是繼 oracle database rac 之後。 又一個“真正” 的群集,怎麼做到“真正” ? 怎麼做到解決複製的延遲,怎麼做到強數據一致性?基於全局的GTID就能解決? 圍繞這些問題進行 ...
  • 最近很多人問小編現在學習大數據這麼多,他們都是如何學習的呢。很多初學者在萌生向大數據方向發展的想法之後,不免產生一些疑問,應該怎樣入門?應該學習哪些技術?學習路線又是什麼?今天小編特意為大家整理了一份大數據從入門到精通的學習路線。並且附帶學習資料和視頻。希望能夠幫助到大家。大數據學習資料分享群:11 ...
  • 從安裝操作系統到完成oracle安裝 1、安裝centos7 下載CentOS7 iso安裝包,配置虛擬機,由於只進行oracle安裝練習,隨便配置20G空間。選擇安裝文件。 開機,開始安裝系統: 直接選擇安裝選項: 等待載入: 選擇英文: 直接選自動分配磁碟: 選擇進行最小化安裝,節約空間。 設置 ...
  • React Native開發封裝Toast與載入Loading組件 在App開發中,我們避免不了使用的兩個組件,一個Toast,一個網路載入Loading,在RN開發中,也是一樣,React Native官方並沒有提供者這兩個常用組件,需要開發者自己根據需求來自定義。作者就在其他組件的基礎上在進行二 ...
  • 有時候在安裝apk的時候會出現解析軟體包出錯 (Android studio)解決方法如下: 關閉Instant Run功能: File-Settings-...看下圖: 將紅色框內的勾取消。 如果還是不行,那麼便在工程的gradle.properties里添加 android.injected.t ...
  • 1、檢查\app\src\main\AndroidMainfest.xml中是否有testOnly屬性為true,如果有去掉或者改為false 2、檢查Android Studio和gradle版本是否為alpha版本,換為穩定版本 3、檢查是否關閉Instant Run功能,關掉此功能 4、檢查是 ...
  • 一、具體問題 開發的過程中,發現某個界面部分圖片的顯示出現了問題只顯示占點陣圖片,取出圖片的url在瀏覽器卻是能打開的,各種嘗試甚至找同行的朋友幫忙在他們項目里展示都會存在問題,最終發現通過第三方框架SDWebImage或者YYWebImage下載帶有逗號的url圖片鏈接都會下載失敗,在下載方法完成的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...