上一篇寫了實現 MVVM 框架的一些基本概念node
本篇用代碼來實現一個完整的 MVVM 框架segmentfault
假設有以下代碼,data
裏面的name
會和試圖中的{{name}}
——一一映射,修改data
的值,會直接引發試圖中對應數據的變化數組
<body> <div id='app'>{{name}}</div> <script> function MVVM(){ //todo... } var vm = new MVVM({ el:'#app', data:{ name:'zhangsan' } }) </script> </body>
如何實現上述 MVVM 呢?app
回想下這篇講的觀察者模式和數據監聽:框架
簡單回答下:
上面例子中,主題應該是data
的name
屬性,觀察者是試圖裏的{{name}}
,當一開始執行 MVVM 初始化(根據el
解析模板發現{{name}}
)的時候訂閱主題,當data.name
發生改變的時候,通知觀察者更新內容,咱們能夠在一開始監控data.name
,當用戶修改data.name
的時候調用主題的subject.ontify
。mvvm
有以下 HTML函數
<div id="app"> <h1>{{name}}'is age is {{age}}</h1> </div>
從上面 HTML 中咱們看出,操做的節點是div#app
,須要的數據是name
和age
,因此實例化 MVVM 能夠須要傳遞兩個參數element
和data
this
let vm = MVVM({ element:'#app', data:{ name:'zhangsan', age:20 } }) setInterval(function(){ vm.data.age++ },2000)
咱們 MVVM 的構造函數應該怎麼寫呢?咱們只須要作兩件事情:雙向綁定
初始化是必須作的,將實例化的數據存在自身上面,後面要用,這裏就不敘述了。code
class MVVM{ constructor(options){ init(options) observe(this.data) this.compile() } init(options){ this.element = document.querySelector(options.element) this.data = options.data } }
先看compile
這個方法,它就是在編譯頁面中的節點,若是節點裏還有孩子,須要再去遍歷這些孩子,若是遍歷到文本,就進行下一步文本替換。
compile(){ //雖然這裏能夠直接對節點進行遍歷,但最好仍是分開來比較好點 this.traverse(this.el) } traverse(node){ //對節點進行遍歷,若是遇到元素節點,用遞歸繼續遍歷直到遍歷到都是文本爲止,進行下一步頁面渲染 node.childNodes.forEach(childNode=>{ if(childNode.nodeType === 1){ this.traverse(childNode) }else if(childNode.nodeType === 3){ this.renderText(childNode) } }) } renderText(textNode){ //到這一步,已經獲取到頁面中的文本了,用正則去匹配 let reg = /{{([^}]*)}}/g //正則或者能夠寫稱/{{(.+?)}}/g let match while(match = reg.exec(textNode.textContent)){ //將匹配到的內容賦值給match,match是一個數組 let raw = match[0] let key = match[1].trim() textNode.textContent = textNode.textContent.replace(raw,this.data[key]) //頁面渲染 new Observer(this,key,function(val,oldVal){ textNode.textContent = textNode.textContent.replace(oldVal,val) }) //建立一個觀察者 } }
假設用戶去修改數據時,那數據該如何進行實時的變更呢?
這裏就引入了觀察者和主題的概念,咱們在解析的過程當中建立一個個觀察者,這個觀察者就觀察這個屬性,解析到下個屬性在建立一個觀察者,並觀察這個屬性。
觀察這個屬性就是訂閱這個主題,咱們在this.compile()
解析完後建立一個觀察者,它有個方法,若是這個屬性變更,我就會修改頁面。
function observe(data){ if(!data || typeof data !== 'object')return for(let key in data){ let val = data[key] let subject = new Subject() //建立主題 if(typeof val === 'object'){ observe(val) } Object.defineProperty(data,key,{ configurable:true, enumerable:true, get(){ return val }, set(newVal){ val = newVal subject.notify() } }) } }
問題是建立了觀察者後何時去觀察這個主題?
在建立後馬上觀察這個主題,但是主題在哪?觀察者有了,就是剛剛new
的時候。主題是在observe
遍歷屬性時建立的。主題存在在observe
局部變量中,外面是訪問不到的,那觀察者怎樣訂閱這個主題呢?
思考到這裏發現行不通了,就須要換種思路了。
當建立觀察者時,會調用getValue()
,它作什麼事情呢,把我設置爲場上權限最高的觀察者,由於頁面中有不少觀察者,此時this.key
,就是我要訂閱的主題,當我調用this.vm.data[this.key]
就等於調用了observe
的get
方法,由於剛剛我已經把觀察者設置爲場上權限最高者,此時currentObserver
是存在的,這時觀察者就開始訂閱主題,訂閱的以後在把權限去掉
let currentObserver = null class Observer{ constructor(vm,key,cb){ this.subjects = {} this.vm = vm this.key = key this.cb = cb this.value = this.getValue() } getValue(){ currentObserver = this let value = this.vm.data[this.key] currentObserver = null return value } }
經過currentObserver
去訂閱主題,由於在建立觀察者時調用了getValue
方法,把currentObserver
設置爲Observer
,經過它去訂閱主題
get:function(){ if(currentObserver){ currentObserver.subscribeTo(subject) } }
主題的構造函數
let id = 0 class Subject{ constructor(){ this.id = id++ this.observers = [] } addObserver(observer){ this.observers.push(observer) } notify(){ this.observers.forEach(observer=>{ observer.update() }) } }
添加觀察者
subscribeTo(subject){ if(!this.subjects[subject.id]){ subject.addObserver(this) this.subjects[subject.id] = subject } }
更新頁面數據,舊值經過自身屬性獲取,新值經過getValue
方法獲取
update(){ let oldVal = this.value let value = this.getValue() if(value !== oldVal){ this.value = value this.cb.call(this.vm,value,oldVal) } }
最後貼上完整的單向綁定的代碼
function observe(data){ if(!data || typeof data !== 'object')return for(let key in data){ let val = data[key] let subject = new Subject() if(typeof val === 'object'){ observe(val) } Object.defineProperty(data,key,{ configurable:true, enumerable:true, get(){ if(currentObserver){ currentObserver.subscribeTo(subject) } return val }, set(newVal){ val = newVal subject.notify() } }) } } let id = 0 class Subject{ constructor(){ this.id = id++ this.observers = [] } addObserver(observer){ this.observers.push(observer) } notify(){ this.observers.forEach(observer=>{ observer.update() }) } } let currentObserver = null class Observer{ constructor(vm,key,cb){ this.subjects = {} this.vm = vm this.key = key this.cb = cb this.value = this.getValue() } update(){ let oldVal = this.value let value = this.getValue() if(value !== oldVal){ this.value = value this.cb.call(this.vm,value,oldVal) } } subscribeTo(subject){ if(!this.subjects[subject.id]){ subject.addObserver(this) this.subjects[subject.id] = subject } } getValue(){ currentObserver = this let value = this.vm.data[this.key] currentObserver = null return value } } class mvvm{ constructor(options){ this.init(options) observe(this.data) this.compile() } init(options){ this.el = document.querySelector(options.el) this.data = options.data } compile(){ this.traverse(this.el) } traverse(node){ node.childNodes.forEach(childNode=>{ if(childNode.nodeType === 1){ this.traverse(childNode) }else if(childNode.nodeType === 3){ this.renderText(childNode) } }) } renderText(textNode){ let reg = /{{([^}]*)}}/g let match while(match = reg.exec(textNode.textContent)){ let raw = match[0] let key = match[1].trim() textNode.textContent = textNode.textContent.replace(raw,this.data[key]) new Observer(this,key,function(val,oldVal){ textNode.textContent = textNode.textContent.replace(oldVal,val) }) } } } let vm = new mvvm({ el:'#app', data:{ name:'uccs', age:20 } }) setInterval(function(){ vm.data.age++ },2000)
本篇詳細講述了 MVVM 單項綁定的原理,下一篇講述雙向綁定
用原生 JS 實現 MVVM 框架MVVM 框架系列:
用原生 JS 實現 MVVM 框架1——觀察者模式和數據監控