使用Vue也有一段時間了,做爲一款MVVM框架,雙向綁定是其最核心的部分,因此最近動手實現了一個簡單的雙向綁定。先上最終成果圖javascript
實現MVVM主要包含兩個方面,一個是數據變化更新視圖,另外一個則是對應的試圖變化更新數據,重點在於怎麼實現數據變了,如何去更新視圖,由於視圖更新數據使用事件監聽的形式就能夠實現,好比input
標籤經過監聽input
事件就能夠實現。因此重點是如何實現數據改變動新視圖。java
實際上是經過Object.defineProperty()
對屬性進行數據劫持,設置set
函數,當數據改變後就回來觸發這個函數,因此要將一些須要更新的方法放在這裏面就能夠實現data
更新view
了。node
實現一個解析器Compile,能夠掃描和解析每一個節點的相關指令,並根據初始化模板數據以及初始化相應的訂閱器。git
{{message}}
v-model
class MVVM { constructor(options) { // 先把可用的東西掛載到實例上 this.$el = options.el; this.$data = options.data; // 判斷有沒有要編譯的模板 if(this.$el) { // 數據劫持 將對象的全部屬性,都添加 get 和 set 方法 new Observer(this.$data) // 用數據和元素進行模板編譯 new Compile(this.$el, this) } } }
class Compile { constructor(el, vm) { // 判斷el是否是元素節點 this.el = this.isElementNode(el) ? el : document.querySelector(el); this.vm = vm; if(this.el) { // 1. 先把真實的DOM移入到內存中(fragment),提升性能 let fragment = this.node2fragment(this.el) // 2. 編譯 -> 提取想要的元素節點 v-model 和 文本節點 {{}} this.compile(fragment) // 3. 把fragment塞回頁面 this.el.appendChild(fragment) } } // 對fragment進行編譯 compile(fragment) { let childNodes = fragment.childNodes; Array.from(childNodes).forEach( node => { // 遍歷fragment的元素節點 if(this.isElemenrNode(node)) { // 是元素節點,須要深度遞歸檢查 this.compile(node) // 編譯元素 this.compileElement(node) } else { // 是文本節點,編譯文本 this.compileText(node) } }) } }
class Observer { constructor(data) { this.observe(data) } observe(data) { // 要對data數據的全部屬性都改成set 和 get 的形式 if(!data || typeof data === 'object') { return ; } // 取出對象 key 值 Object.keys(data).forEach( key => { // 數據劫持 this.defineReactive(data, key, data[key]); this.observe(data[key]); // 遞歸劫持 }) } // 定義響應式(數據劫持) defineReactive(obj, key, value) { let that = this; Object.defineProperty(obj, key, { enumerable: true, // 可枚舉 configurable: true, // 屬性可以被改變 get() { // 取值時調用的方法 return value; }, set(newVal) { // 當給data屬性中設置值的時候,更改獲取的屬性的值 if(newVal !== value) { value = newVal; that.observe(newVal); // 若是是對象修改繼續劫持 } } }) } }
最後,給須要變化的元素添加一個觀察者,經過觀察者監聽數據變化以後執行對應的方法。github
class Watcher { constructor (vm, expr, cb) { this.vm = vm; this.expr = expr; this.cb = cb; // 先獲取一下老值 this.value = this.get() } getVal() { // 獲取實例上對應的數據 expr = expr.split('.'); return expr.reduce( (prev, next) => { return prev[next]; }, vm.$data) } get() { let value = this.getVal(this.vm, this.expr); return value; } // 對外暴露的方法,老值和新值比對,若是變化 update() { let newVal = this.getVal(this.vm, this.expr); let oldVal = this.value; if(newVal !== oldVal) { this.cb(newVal); // 對應watch的callback } } }
Watch 完成,須要new一下調用,首先須要在模板編譯的時候須要調用,在compile.js
:數組
CompileUtil = { getVal(vm, expr) { // 獲取實例上對應的數據 expr = expr.split('.'); return expr.reduce( (prev, next) => { return prev[next]; }, vm.$data) }, getTextVal(vm, expr) { // 獲取編譯後文本的結果 return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => { return this.getVal(vm, arguments[1]); }) }, text(node, vm, expr) { // 文本處理 let updateFn = this.updater['textUpdater'] /* Wather觀察者監聽 */ expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => { new Wathcer(vm, arguments[1], (newVal) => { // 若是數據變化,文本須要從新獲取依賴的數據,更新文本中的內容 updateFn && updateFn(node, this.getTextVal(vm, expr)) }) }) updateFn && updateFn(node, this.getTextVal(vm, expr)) }, setVal(vm, expr, value) { expr = expr.split('.'); return expr.reduce( (prev, next,currentIndex) => { if(currentIndex === expr.length - 1) { return prev[next] = value; } return prev[next]; }, vm.$data) }, model(node, vm, expr) { // 輸入框處理 let updateFn = this.updater['modelUpdater'] /* Wather觀察者監聽 */ // 這裏應該加一個監控, 數據變化,調用watch的回調 new Wathcer(vm, expr, (newVal) => { // 當值變化後會調用callback,將新值傳遞過來 updateFn && updateFn(node, this.getVal(vm, expr)); }) // 給輸入框加上input事件監聽 node.addEventListener('input', (e) => { let newVal = e.target.value; this.setVal(vm, expr, newVal) }) updateFn && updateFn(node, this.getVal(vm, expr)); }, updater: { // 文本更新 textUpdater(node, value) { node.textContent = value; }, // 輸入框更新 modelUpdater(node, value) { node.value = value; } } }
可是此時有一個問題,Watcher沒有地方調用,更新函數不會執行,因此此時須要一個發佈訂閱模式來調用監控者。app
class Dep { constructor() { // 訂閱的數組 this.subs = []; } addSub(watcher) { this.subs.push(watcher); } notify() { this.subs.forEach( watcher => { watcher.update() }) } }
此時須要修改watcher
裏 get()
這個方法:框架
get() { Dep.target = this; let value = this.getVal(this.vm, this.expr) Dep.target = null; return value; }
此時要獲得對象的值,須要被數據劫持攔截:mvvm
defineReactive(obj, key, value) { let that = this; let dep = new Dep(); // 每一個變化的數據,都會定義一個數組,這個數組存放全部更新的操做 Object.defineProperty(obj, key, { enumerable: true, // 可枚舉 configurable: true, get() { // 當取值時調用的方法 Dep.target && dep.addSub(Dep.target); // 最開始編譯的時候不會執行 return value; }, set(newVal) { // 當給data屬性中設置值的時候 更改獲取屬性的值 if(newVal != value) { that.observe(newVal); // 若是是對象繼續劫持 value = newVal; dep.notify(); // 通知全部人數據更新了 } } }); }
此時就完成了輸入框的雙向綁定。不過此時咱們取數據是以vm.$data.msg
來取到數據,理想狀況咱們是vm.msg
來取到數據,爲了實現這樣的形式,咱們使用proxy
進行一下代理實現:函數
proxyData(data) { Object.keys(data).forEach( key => { Object.defineProperty(this, key, { get() { return data[key] }, set(newVal) { data[key] = newVal } }) }) }
這下咱們就能夠直接經過vm.msg = 'hello'
的形式來進行改變和獲取模板數據了。
歡迎交流指正,原文地址:https://github.com/hu970804/MVVM