摘要:上一篇寫了實(shí)現(xiàn)框架的一些基本概念本篇用代碼來實(shí)現(xiàn)一個完整的框架思考假設(shè)有如下代碼,里面的會和試圖中的一一映射,修改的值,會直接引起試圖中對應(yīng)數(shù)據(jù)的變化如何實(shí)現(xiàn)上述呢回想下這篇講的觀察者模式和數(shù)據(jù)監(jiān)聽主題是什么觀察者是什么觀察者何時(shí)訂閱主題主
上一篇寫了實(shí)現(xiàn) MVVM 框架的一些基本概念
本篇用代碼來實(shí)現(xiàn)一個完整的 MVVM 框架
思考假設(shè)有如下代碼,data里面的name會和試圖中的{{name}}——一一映射,修改data的值,會直接引起試圖中對應(yīng)數(shù)據(jù)的變化
{{name}}
如何實(shí)現(xiàn)上述 MVVM 呢?
回想下這篇講的觀察者模式和數(shù)據(jù)監(jiān)聽:
主題(subject)是什么?
觀察者(observer)是什么?
觀察者何時(shí)訂閱主題?
主題何時(shí)通知更新?
簡單回答下:
上面例子中,主題應(yīng)該是data的name屬性,觀察者是試圖里的{{name}},當(dāng)一開始執(zhí)行 MVVM 初始化(根據(jù)el解析模板發(fā)現(xiàn){{name}})的時(shí)候訂閱主題,當(dāng)data.name發(fā)生改變的時(shí)候,通知觀察者更新內(nèi)容,我們可以在一開始監(jiān)控data.name,當(dāng)用戶修改data.name的時(shí)候調(diào)用主題的subject.ontify。
有如下 HTML
{{name}}"is age is {{age}}
從上面 HTML 中我們看出,操作的節(jié)點(diǎn)是div#app,需要的數(shù)據(jù)是name和age,所以實(shí)例化 MVVM 可以需要傳遞兩個參數(shù)element和data
let vm = MVVM({ element:"#app", data:{ name:"zhangsan", age:20 } }) setInterval(function(){ vm.data.age++ },2000)
我們 MVVM 的構(gòu)造函數(shù)應(yīng)該怎么寫呢?我們只需要做兩件事情:
我們需要觀察這些數(shù)據(jù),當(dāng)以后這些數(shù)據(jù)變動時(shí),會做一些事情去調(diào)用
需要解析這個模板,把模板中的一些符號替換成對應(yīng)的數(shù)據(jù)
初始化是必須做的,將實(shí)例化的數(shù)據(jù)存在自身上面,后面要用,這里就不敘述了。
class MVVM{ constructor(options){ init(options) observe(this.data) this.compile() } init(options){ this.element = document.querySelector(options.element) this.data = options.data } }
先看compile這個方法,它就是在編譯頁面中的節(jié)點(diǎn),如果節(jié)點(diǎn)里還有孩子,需要再去遍歷這些孩子,如果遍歷到文本,就進(jìn)行下一步文本替換。
compile(){ //雖然這里可以直接對節(jié)點(diǎn)進(jìn)行遍歷,但最好還是分開來比較好點(diǎn) this.traverse(this.el) } traverse(node){ //對節(jié)點(diǎn)進(jìn)行遍歷,如果遇到元素節(jié)點(diǎn),用遞歸繼續(xù)遍歷直到遍歷到都是文本為止,進(jìn)行下一步頁面渲染 node.childNodes.forEach(childNode=>{ if(childNode.nodeType === 1){ this.traverse(childNode) }else if(childNode.nodeType === 3){ this.renderText(childNode) } }) } renderText(textNode){ //到這一步,已經(jīng)獲取到頁面中的文本了,用正則去匹配 let reg = /{{([^}]*)}}/g //正則或者可以寫稱/{{(.+?)}}/g let match while(match = reg.exec(textNode.textContent)){ //將匹配到的內(nèi)容賦值給match,match是一個數(shù)組 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) }) //創(chuàng)建一個觀察者 } }
假設(shè)用戶去修改數(shù)據(jù)時(shí),那數(shù)據(jù)該如何進(jìn)行實(shí)時(shí)的變動呢?
這里就引入了觀察者和主題的概念,我們在解析的過程中創(chuàng)建一個個觀察者,這個觀察者就觀察這個屬性,解析到下個屬性在創(chuàng)建一個觀察者,并觀察這個屬性。
觀察這個屬性就是訂閱這個主題,我們在this.compile()解析完后創(chuàng)建一個觀察者,它有個方法,如果這個屬性變動,我就會修改頁面。
function observe(data){ if(!data || typeof data !== "object")return for(let key in data){ let val = data[key] let subject = new Subject() //創(chuàng)建主題 if(typeof val === "object"){ observe(val) } Object.defineProperty(data,key,{ configurable:true, enumerable:true, get(){ return val }, set(newVal){ val = newVal subject.notify() } }) } }
問題是創(chuàng)建了觀察者后什么時(shí)候去觀察這個主題?
在創(chuàng)建后立刻觀察這個主題,可是主題在哪?觀察者有了,就是剛剛new的時(shí)候。主題是在observe遍歷屬性時(shí)創(chuàng)建的。主題存在在observe局部變量中,外面是訪問不到的,那觀察者怎樣訂閱這個主題呢?
思考到這里發(fā)現(xiàn)行不通了,就需要換種思路了。
當(dāng)創(chuàng)建觀察者時(shí),會調(diào)用getValue(),它做什么事情呢,把我設(shè)置為場上權(quán)限最高的觀察者,因?yàn)轫撁嬷杏泻芏嘤^察者,此時(shí)this.key,就是我要訂閱的主題,當(dāng)我調(diào)用this.vm.data[this.key]就等于調(diào)用了observe的get方法,因?yàn)閯倓偽乙呀?jīng)把觀察者設(shè)置為場上權(quán)限最高者,此時(shí)currentObserver是存在的,這時(shí)觀察者就開始訂閱主題,訂閱的之后在把權(quán)限去掉
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去訂閱主題,因?yàn)樵趧?chuàng)建觀察者時(shí)調(diào)用了getValue方法,把currentObserver設(shè)置為Observer,通過它去訂閱主題
get:function(){ if(currentObserver){ currentObserver.subscribeTo(subject) } }
主題的構(gòu)造函數(shù)
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 } }
更新頁面數(shù)據(jù),舊值通過自身屬性獲取,新值通過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)
本篇詳細(xì)講述了 MVVM 單項(xiàng)綁定的原理,下一篇講述雙向綁定
用原生 JS 實(shí)現(xiàn) MVVM 框架MVVM 框架系列:
用原生 JS 實(shí)現(xiàn) MVVM 框架1——觀察者模式和數(shù)據(jù)監(jiān)控
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/97279.html
摘要:模塊則負(fù)責(zé)維護(hù),以及各個模塊間的調(diào)度思考題了解了的實(shí)現(xiàn)機(jī)制,你能否自己動手也試著用百來行代碼實(shí)現(xiàn)一個庫呢好了本教程第一部分設(shè)計(jì)篇就寫到這里,具體請移步下一篇教學(xué)向行代碼教你實(shí)現(xiàn)一個低配版的庫代碼篇我會用給出一版實(shí)現(xiàn)。 適讀人群 本文適合對MVVM有一定了解(如有主流框架ng,vue等使用經(jīng)驗(yàn)配合本文服用則效果更佳),雖然會用這類框架,但是對框架底層核心實(shí)現(xiàn)又不太清楚,或者能說出個所以然...
摘要:綁定實(shí)現(xiàn)的歷史綁定的基礎(chǔ)是事件。但臟檢查機(jī)制隨之帶來的就是性能問題。是谷歌對于簡化雙向綁定機(jī)制的嘗試,在中引入。掙扎了一段時(shí)間后谷歌團(tuán)隊(duì)宣布收回的提議,并在中完全刪除了實(shí)現(xiàn)。自然全軍覆沒其他各大瀏覽器實(shí)現(xiàn)的時(shí)間也較晚。 綁定實(shí)現(xiàn)的歷史 綁定的基礎(chǔ)是 propertyChange 事件。如何得知 viewModel 成員值的改變一直是開發(fā) MVVM 框架的首要問題。主流框架的處理有一下三...
摘要:綁定實(shí)現(xiàn)的歷史綁定的基礎(chǔ)是事件。但臟檢查機(jī)制隨之帶來的就是性能問題。是谷歌對于簡化雙向綁定機(jī)制的嘗試,在中引入。掙扎了一段時(shí)間后谷歌團(tuán)隊(duì)宣布收回的提議,并在中完全刪除了實(shí)現(xiàn)。自然全軍覆沒其他各大瀏覽器實(shí)現(xiàn)的時(shí)間也較晚。 綁定實(shí)現(xiàn)的歷史 綁定的基礎(chǔ)是 propertyChange 事件。如何得知 viewModel 成員值的改變一直是開發(fā) MVVM 框架的首要問題。主流框架的處理有一下三...
摘要:是分發(fā)器,是數(shù)據(jù)與邏輯處理器,會在注冊針對各個命令字的響應(yīng)回調(diào)函數(shù)。當(dāng)按如下方式觸發(fā)回調(diào)時(shí),回調(diào)函數(shù)具備事件的特性。 本系列博文從 Shadow Widget 作者的視角,解釋該框架的設(shè)計(jì)要點(diǎn)。本篇解釋 Shadow Widget 在 MVC、MVVM、Flux 框架之間如何做選擇。 showImg(https://segmentfault.com/img/bVOODj?w=380&h...
閱讀 2649·2021-11-12 10:36
閱讀 2292·2021-08-23 09:47
閱讀 1735·2019-08-30 15:44
閱讀 1432·2019-08-30 14:10
閱讀 2265·2019-08-29 16:52
閱讀 2364·2019-08-29 16:40
閱讀 1611·2019-08-29 16:17
閱讀 2439·2019-08-26 13:21