我對MVVM的學習筆記

前言

最近在學習MVVM的實現原理,恰好在sf上看到了剖析Vue原理&實現雙向綁定MVVM一文,寫的很是好,摘出Vue.js中的部分源碼,改造後完成了一個簡單的MVVM實現。實現了雙向數據綁定,我本身在學習的過程當中,也照着這篇文章中的源碼從新實現了一遍。不一樣之處在於,我儘可能將原來的實現寫成了ES6的寫法,好比使用class代替構造函數,將observer,dep,watcher,compiler分紅不一樣的模塊,而後使用import,export來互相引入,導出,最後使用rollup-babel-lib-bundler打包了一下。因此這篇文章是對上面文章的學習總結,不會寫的很細。你們也能夠讀一下上面的文章,簡單易懂。html

我從新寫過的項目地址在這裏,有興趣的能夠看看。node

總體結構

這個簡易的MVVM總共由index.js(入口文件),compiler.js,dep.js,observer.js,watcher.js幾部分組成。git

.
├── README.md
├── dest
│   ├── toy.es2015.js
│   ├── toy.js
│   └── toy.umd.js
├── examples
│   └── index.html
├── package.json
├── rollup.config.js
└── src
    ├── compiler.js
    ├── dep.js
    ├── index.js
    ├── observer.js
    └── watcher.js

index.js是整個框架的入口,好比我給這個框架起了個名字叫Toy,入口文件導出的其實就是Toy的構造函數:github

//引入其它模塊
import { observe } from './observer.js'
import { Compiler } from './compiler.js'
import { Watcher } from './watcher.js'

//具體實現
class Toy {
    constructor(options){
        //...
    }
}

//導出模塊
export { Toy }

初始化的過程分兩步:json

  1. 劫持監聽全部屬性,經過Object.defineProperty將數據變成響應式的,同時在getset上作一些手腳。segmentfault

  2. 編譯html模板,事實上咱們在使用框架時寫的html已經填充了不少框架本身的指令,語法,因此要先進行編譯替換才能正確展現視圖。瀏覽器

實現全部屬性的監聽就是經過Object.defineProperty遞歸地定義因此屬性。每個對象都會有一個對應的Observer實例,其中的每個屬性都對應有一個Dep的實例depdep使用自增的uid標識,做用是記錄這個屬性被那些訂閱者(Watcher的實例)訂閱了,好在屬性變化時,經過遍歷dep.subs去通知全部訂閱了這個屬性的watcher去作對應的更新。babel

實現Compiler就是對帶有框架特殊API的模板進行編譯,指令解析。同時將DOM與數據關聯起來(實際上是經過Watcher實現的)。閉包

本質上說

每一個部分負責的事情我是這樣理解的:app

  • index.js 框架的入口,提供對外的構造函數。

  • observer.js 將數據變成響應式,同時經過dep收集依賴(Watcher實例)。

  • dep.js 收集依賴用的,在get中收集依賴,在set中通知對應依賴更新。

  • watcher.js 數據的訂閱者,一個Watcher的實例由vm,exp,cb,deps等幾部分組成,vm是對ViewModel的引用,觸發get方法將watcher自身添加至depsubs中時會用到,exp則是當前Watcher實例監聽的表達式,即數據的keycb則是更新數據的回調。
    vm的數據改變後,會觸發對應的set方法,這個屬性對應的dep會通知全部的subs去執行自身的update方法,而這個update方法的內容其實只是this.cb.call(this.vm, value, oldValue)cb其實是調用了updateFn(在compiler.js中綁定的),這時纔將DOM的數據真正更新。

  • compiler.js 編輯DOM模板,併爲每一個node節點經過new Watcher的方式將屬性表達式expupdateFn(真正更新DOM的函數)node關聯,而後配合響應式數據就作到了viewmodel的雙向綁定。

因此整個框架的運行過程是這樣的:

  1. observe全部數據,改寫了每一個數據的get和set方法,併爲每一個數據關聯了一個dep(經過閉包實現)。

  2. new Compiler開始編譯模板,編譯過程當中,能夠提取出指令,v-text,v-html等,能夠分析出事件函數v-click和綁定的表達式,這時經過self.compileText(node, RegExp.$1),self.compile(node)將DOM節點和表達式創建關聯。

  3. 創建的關聯,是DOM節點和數據表達式的關聯,這一步是經過new Watcher實現的

  4. new Watcher的時候,Watcher實例會將Dep.target這個全局屬性指向自身,而後出發一下須要監聽屬性的getter,這時dep會將Watcher實例添加到它的subs中,Watcher實例也會標記一下這個dep已經添加過本身了,防止重複添加。這時dep和Watcher實例已經關聯起來了,數據的變化能夠通知到對應的Watcher實例,Watcher實例的update方法會正確地更新DOM。

其實到這裏,數據的雙向綁定就已經實現了。

過程當中學習到的一些細節

記錄一些在學習過程當中遇到的小tips,其實都是很基礎的東西。

  • Node.textContent: 表示一個節點及其內部節點的文本內容。以前一直都是用innerText的,看了MDN才知道innerText原來是IE私有的,textContent纔是標準屬性。並且innerText受樣式影響,還會觸發重排,因此仍是用textContent代替吧。

  • Node.appendChild: 這個API有一個頗有意思的行爲:若是被插入的節點已經存在於當前文檔的文檔樹中,則那個節點會首先從原先的位置移除,而後再插入到新的位置.,當時我在看compiler.jsnode2Fragment方法:

node2Fragment(el){
    let fragment = document.createDocumentFragment()
    let child
    while(child = el.firstChild){
        fragment.appendChild(child)
    }
    return fragment
}

當時很不解爲何while循環能成按照預期執行,我在瀏覽器屢次調用el.firstChild拿到的也始終是第一個子節點,看了這個API的文檔才發現還有這麼個行爲!

  • Node.attributes: 能夠方便地獲取DOM節點的屬性,返回值是一個對象,其中name是屬性名,value是屬性值。

最後

終於明白了簡易MVVM框架的運做原理,也發現了一些底層API的知識,寫成一些總結,這篇文章中沒有貼不少代碼去說實現,由於剖析Vue原理&實現雙向綁定MVVM一文已經很詳細了,我也是按照這個去學習的,因此我記錄的是我我的的一些思想上的總結,因此可能要先看代碼才能瞭解。分享出來,但願能有人從中受益 :)

相關文章
相關標籤/搜索