javascript
用原生 JS 实现 MVVM 框架2——单向绑定
上一篇寫了實現 MVVM 框架的一些基本概念
本篇用代碼來實現一個完整的 MVVM 框架
思考
假設有如下代碼,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 呢?
回想下這篇講的觀察者模式和數據監聽:
簡單回答下:
上面例子中,主題應該是data的name屬性,觀察者是試圖里的{{name}},當一開始執行 MVVM 初始化(根據el解析模板發現{{name}})的時候訂閱主題,當data.name發生改變的時候,通知觀察者更新內容,我們可以在一開始監控data.name,當用戶修改data.name的時候調用主題的subject.ontify。
單向綁定
有如下 HTML
<div id="app"><h1>{{name}}'is age is {{age}}</h1> </div>從上面 HTML 中我們看出,操作的節點是div#app,需要的數據是name和age,所以實例化 MVVM 可以需要傳遞兩個參數element和data
let vm = MVVM({element:'#app',data:{name:'zhangsan',age:20} }) setInterval(function(){vm.data.age++ },2000)我們 MVVM 的構造函數應該怎么寫呢?我們只需要做兩件事情:
初始化是必須做的,將實例化的數據存在自身上面,后面要用,這里就不敘述了。
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 //正則或者可以寫稱/{{(.+?)}}/glet matchwhile(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')returnfor(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 = newValsubject.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 = vmthis.key = keythis.cb = cbthis.value = this.getValue()}getValue(){currentObserver = thislet value = this.vm.data[this.key]currentObserver = nullreturn 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.valuelet value = this.getValue()if(value !== oldVal){this.value = valuethis.cb.call(this.vm,value,oldVal)} }最后貼上完整的單向綁定的代碼
function observe(data){if(!data || typeof data !== 'object')returnfor(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 = newValsubject.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 = vmthis.key = keythis.cb = cbthis.value = this.getValue()}update(){let oldVal = this.valuelet value = this.getValue()if(value !== oldVal){this.value = valuethis.cb.call(this.vm,value,oldVal)}}subscribeTo(subject){if(!this.subjects[subject.id]){subject.addObserver(this)this.subjects[subject.id] = subject}}getValue(){currentObserver = thislet value = this.vm.data[this.key]currentObserver = nullreturn 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 = /{{([^}]*)}}/glet matchwhile(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——觀察者模式和數據監控
總結
以上是生活随笔為你收集整理的用原生 JS 实现 MVVM 框架2——单向绑定的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [bzoj2242][SDOI2011]
- 下一篇: gradle idea java ssm