最近在學習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
劫持監聽全部屬性,經過Object.defineProperty
將數據變成響應式的,同時在get
和set
上作一些手腳。segmentfault
編譯html模板,事實上咱們在使用框架時寫的html已經填充了不少框架本身的指令,語法,因此要先進行編譯替換才能正確展現視圖。瀏覽器
實現全部屬性的監聽就是經過Object.defineProperty
遞歸地定義因此屬性。每個對象都會有一個對應的Observer
實例,其中的每個屬性都對應有一個Dep
的實例dep
,dep
使用自增的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
自身添加至dep
的subs
中時會用到,exp
則是當前Watcher實例監聽的表達式,即數據的key
,cb
則是更新數據的回調。
當vm
的數據改變後,會觸發對應的set
方法,這個屬性對應的dep
會通知全部的subs
去執行自身的update
方法,而這個update
方法的內容其實只是this.cb.call(this.vm, value, oldValue)
,cb
其實是調用了updateFn
(在compiler.js
中綁定的),這時纔將DOM的數據真正更新。
compiler.js 編輯DOM模板,併爲每一個node節點
經過new Watcher
的方式將屬性表達式exp
,updateFn(真正更新DOM的函數)
與node
關聯,而後配合響應式數據就作到了view
與model
的雙向綁定。
因此整個框架的運行過程是這樣的:
observe
全部數據,改寫了每一個數據的get和set方法,併爲每一個數據關聯了一個dep(經過閉包實現)。
new Compiler
開始編譯模板,編譯過程當中,能夠提取出指令,v-text
,v-html
等,能夠分析出事件函數v-click
和綁定的表達式,這時經過self.compileText(node, RegExp.$1)
,self.compile(node)
將DOM節點和表達式創建關聯。
創建的關聯,是DOM節點和數據表達式的關聯,這一步是經過new Watcher
實現的
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.js
的node2Fragment
方法:
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一文已經很詳細了,我也是按照這個去學習的,因此我記錄的是我我的的一些思想上的總結,因此可能要先看代碼才能瞭解。分享出來,但願能有人從中受益 :)