摘要:實現(xiàn)訂閱中心和之間通信的橋梁是訂閱中心,其主要職責(zé)是在自身實例化時往屬性訂閱器里面添加自己,與建立連接自身必須有一個方法,與建立連接當(dāng)屬性變化時,中通知,然后能調(diào)用自身的方法,并觸發(fā)中綁定的回調(diào),實現(xiàn)更新。
一. 什么是mvvm
MVVM是Model-View-ViewModel的簡寫。它本質(zhì)上就是MVC 的改進版。MVVM 就是將其中的View 的狀態(tài)和行為抽象化,讓我們將視圖 UI 和業(yè)務(wù)邏輯分開。
要實現(xiàn)一個mvvm的庫,我們首先要理解清楚其實現(xiàn)的整體思路。先看看下圖的流程:
1.實現(xiàn)compile,進行模板的編譯,包括編譯元素(指令)、編譯文本等,達到初始化視圖的目的,并且還需要綁定好更新函數(shù);
2.實現(xiàn)Observe,監(jiān)聽所有的數(shù)據(jù),并對變化數(shù)據(jù)發(fā)布通知;
3.實現(xiàn)watcher,作為一個中樞,接收到observe發(fā)來的通知,并執(zhí)行compile中相應(yīng)的更新方法。
4.結(jié)合上述方法,向外暴露mvvm方法。
首先編輯一個html文件,如下:
1.實現(xiàn)一個mvvm類(入口)MVVM原理及其實現(xiàn) {{message}}
新建一個mvvm.js,將參數(shù)通過options傳入mvvm中,并取出el和data綁定到mvvm的私有變量$el和$data中。
// mvvm.js class MVVM { constructor(options) { this.$el = options.el this.$data = options.data } }2.實現(xiàn)compile(編譯模板)
新建一個compile.js文件,在mvvm.js中調(diào)用compile。compile.js接收mvvm中傳過來的el和vm實例。
// mvvm.js class MVVM { constructor(options) { this.$el = options.el this.$data = options.data // 如果有要編譯的模板 =>編譯 if(this.$el) { // 將文本+元素模板進行編譯 new Compile(this.$el, this) } } }
(1)初始化傳值
// compile.js export default class Compile { constructor(el, vm) { // 判斷是否是元素節(jié)點,是=》取該元素 否=》取文本 this.el = this.isElementNode(el) ? el:document.querySelector(el) this.vm = vm }, // 判斷是否是元素節(jié)點 isElementNode(node) { return node.nodeType === 1 } }
(2)先把真實DOM移入到內(nèi)存中 fragment,因為fragment在內(nèi)存中,操作比較快
// compile.js class Compile { constructor(el, vm) { // 判斷是否是元素節(jié)點,是=》取該元素 否=》取文本 this.el = this.isElementNode(el) ? el:document.querySelector(el) this.vm = vm // 如果這個元素能獲取到 我們才開始編譯 if(this.el) { // 1. 先把真實DOM移入到內(nèi)存中 fragment let fragment = this.node2fragment(this.el) } }, // 判斷是否是元素節(jié)點 isElementNode(node) { return node.nodeType === 1 } // 將el中的內(nèi)容全部放到內(nèi)存中 node2fragment(el) { let fragment = document.createDocumentFragment() let firstChild // 遍歷取出firstChild,直到firstChild為空 while (firstChild = el.firstChild) { fragment.appendChild(firstChild) } return fragment // 內(nèi)存中的節(jié)點 } }
(3)編譯 =》 在fragment中提取想要的元素節(jié)點 v-model 和文本節(jié)點
// compile.js class Compile { constructor(el, vm) { // 判斷是否是元素節(jié)點,是=》取該元素 否=》取文本 this.el = this.isElementNode(el) ? el:document.querySelector(el) this.vm = vm // 如果這個元素能獲取到 我們才開始編譯 if(this.el) { // 1. 先把真實DOM移入到內(nèi)存中 fragment let fragment = this.node2fragment(this.el) // 2. 編譯 =》 在fragment中提取想要的元素節(jié)點 v-model 和文本節(jié)點 this.compile(fragment) // 3. 把編譯好的fragment在放回到頁面中 this.el.appendChild(fragment) } } // 判斷是否是元素節(jié)點 isElementNode(node) { return node.nodeType === 1 } // 是不是指令 isDirective(name) { return name.includes("v-") } // 將el中的內(nèi)容全部放到內(nèi)存中 node2fragment(el) { let fragment = document.createDocumentFragment() let firstChild // 遍歷取出firstChild,直到firstChild為空 while (firstChild = el.firstChild) { fragment.appendChild(firstChild) } return fragment // 內(nèi)存中的節(jié)點 } //編譯 =》 提取想要的元素節(jié)點 v-model 和文本節(jié)點 compile(fragment) { // 需要遞歸 let childNodes = fragment.childNodes Array.from(childNodes).forEach(node => { // 是元素節(jié)點 直接調(diào)用文本編譯方法 還需要深入遞歸檢查 if(this.isElementNode(node)) { this.compileElement(node) // 遞歸深入查找子節(jié)點 this.compile(node) // 是文本節(jié)點 直接調(diào)用文本編譯方法 } else { this.compileText(node) } }) } // 編譯元素方法 compileElement(node) { let attrs = node.attributes Array.from(attrs).forEach(attr => { let attrName = attr.name // 判斷屬性名是否包含 v-指令 if(this.isDirective(attrName)) { // 取到v-指令屬性中的值(這個就是對應(yīng)data中的key) let expr = attr.value // 獲取指令類型 let [,type] = attrName.split("-") // node vm.$data expr compileUtil[type](node, this.vm, expr) } }) } // 這里需要編譯文本 compileText(node) { //取文本節(jié)點中的文本 let expr = node.textContent let reg = /{{([^}]+)}}/g if(reg.test(expr)) { // node this.vm.$data text compileUtil["text"](node, this.vm, expr) } } } // 解析不同指令或者文本編譯集合 const compileUtil = { text(node, vm, expr) { // 文本 let updater = this.updater["textUpdate"] updater && updater(node, getTextValue(vm, expr)) }, model(node, vm, expr){ // 輸入框 let updater = this.updater["modelUpdate"] updater && updater(node, getValue(vm, expr)) }, // 更新函數(shù) updater: { // 文本賦值 textUpdate(node, value) { node.textContent = value }, // 輸入框value賦值 modelUpdate(node, value) { node.value = value } } } // 輔助工具函數(shù) // 綁定key上對應(yīng)的值,從vm.$data中取到 const getValue = (vm, expr) => { expr = expr.split(".") // [message, a, b, c] return expr.reduce((prev, next) => { return prev[next] }, vm.$data) } // 獲取文本編譯后的對應(yīng)的數(shù)據(jù) const getTextValue = (vm, expr) => { return expr.replace(/{{([^}]+)}}/g, (...arguments) => { return getValue(vm, arguments[1]) }) }
(3) 將編譯后的fragment放回到dom中
let fragment = this.node2fragment(this.el) this.compile(fragment) // 3. 把編譯好的fragment在放回到頁面中 this.el.appendChild(fragment)
進行到這一步,頁面上初始化應(yīng)該渲染完成了。如下圖:
3.實現(xiàn)observe(數(shù)據(jù)監(jiān)聽/劫持)不同于發(fā)布者-訂閱者模式和臟值檢測,vue采用的observe + sub/pub 實現(xiàn)數(shù)據(jù)的劫持,通過js原生的方法Object.defineProperty()來劫持各個屬性的setter,getter,在屬性對應(yīng)數(shù)據(jù)改變時,發(fā)布消息給訂閱者,然后觸發(fā)相應(yīng)的監(jiān)聽回調(diào)。
主要內(nèi)容:observe的數(shù)據(jù)對象進行遞歸遍歷,包括子屬性對象的屬性,都加上 setter和getter。
// observe.js class Observe { constructor(data) { this.observe(data) } // 把data數(shù)據(jù)原有的屬性改成 get 和 set方法的形式 observe(data) { if(!data || typeof data!== "object") { return } console.log(data) // 將數(shù)據(jù)一一劫持 // 先獲取到data的key和value Object.keys(data).forEach((key) => { // 數(shù)據(jù)劫持 this.defineReactive(data, key, data[key]) this.observe(data[key]) // 深度遞歸劫持,保證子屬性的值也會被劫持 }) } // 定義響應(yīng)式 defineReactive(obj, key, value) { let _this = this Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 當(dāng)取值時調(diào)用 return value }, set(newValue) { //當(dāng)data屬性中設(shè)置新值得時候 更改獲取的新值 if(newValue !== value) { _this.observe(newValue) // 如果是對象繼續(xù)劫持 console.log("監(jiān)聽到值變化了,舊值:", value, " --> 新值:", newValue); value = newValue } } }) } }
完成observe.js后,修改mvvm.js文件,將屬性傳入observe中
// mvvm.js class MVVM { constructor(options) { console.log(options) this.$el = options.el this.$data = options.data // 如果有要編譯的模板 =》編譯 if(this.$el) { // 數(shù)據(jù)劫持 就是把對象的所有屬性改成 get 和 set方法 new Observe(this.$data) // 將文本+元素模板進行編譯 new Compile(this.$el, this) } } }
可以在控制臺查看到以下信息,說明劫持屬性成功。
實現(xiàn)數(shù)據(jù)劫持后,接下來的任務(wù)怎么通知訂閱者了,我們需要在監(jiān)聽數(shù)據(jù)時實現(xiàn)一個消息訂閱器,具體的方法是:定義一個數(shù)組,用來存放訂閱者,數(shù)據(jù)變動通知(notify)訂閱者,再調(diào)用訂閱者的update方法。
在observe.js添加Dep類:
//observe.js // ... let _this = this let dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 當(dāng)取值時調(diào)用 return value }, set(newValue) { //當(dāng)data屬性中設(shè)置新值得時候 更改獲取的新值 if(newValue !== value) { _this.observe(newValue) // 如果是對象繼續(xù)劫持 console.log("監(jiān)聽到值變化了,舊值:", value, " --> 新值:", newValue); value = newValue dep.notify() //通知所有人 數(shù)據(jù)更新了 } } }) // ... // 消息訂閱器Dep() class Dep { constructor() { // 訂閱的數(shù)組 this.subs = [] } addSub(watcher) { // push到訂閱數(shù)組 this.subs.push(watcher) } notify() { // 通知訂閱者,并執(zhí)行訂閱者的update回調(diào) this.subs.forEach(watcher => watcher.update()) } }
實現(xiàn)了消息訂閱器,并且能夠執(zhí)行訂閱者的回調(diào),那么訂閱者怎么獲取,并push到訂閱器數(shù)組中呢?這個要和watcher結(jié)合。
4.實現(xiàn)watcher(訂閱中心)Observer和Compile之間通信的橋梁是Watcher訂閱中心,其主要職責(zé)是:
1、在自身實例化時往屬性訂閱器(Dep)里面添加自己,與Observer建立連接;
2、自身必須有一個update()方法,與Compile建立連接;
3、當(dāng)屬性變化時,Observer中dep.notice()通知,然后能調(diào)用自身(Watcher)的update()方法,并觸發(fā)Compile中綁定的回調(diào),實現(xiàn)更新。
// watcher.js // 訂閱中心(觀察者): 給需要變化的那個元素 增加一個觀察者, 當(dāng)數(shù)據(jù)變化后,執(zhí)行對應(yīng)的方法 class Watcher { constructor(vm, expr, cb) { this.vm = vm this.expr = expr this.cb = cb // 先獲取一下老值 this.value = this.get() } getValue(vm, expr) { // 獲取實例上對應(yīng)的數(shù)據(jù) expr = expr.split(".") // [message, a, b, c] return expr.reduce((prev, next) => { return prev[next] }, vm.$data) } get() { // 獲取文本編譯后的對應(yīng)的數(shù)據(jù) // 獲取當(dāng)前訂閱者 Dep.target = this // 觸發(fā)getter,當(dāng)前訂閱者添加訂閱器中 在 劫持數(shù)據(jù)時,將訂閱者放到訂閱者數(shù)組 let value = this.getValue(this.vm, this.expr) // 重置訂閱者 Dep.target = null return value } // 對外暴露的方法 update() { let newValue = this.getValue(this.vm, this.expr) let oldValue = this.value // 更新的值 與 以前的值 進行比對, 如果發(fā)生變化就更新方法 if(newValue !== oldValue) { this.cb(newValue) } } } // observe.js // ... 省略 Object.defineProperty(data, key, { get: function() { // 在取值時將訂閱者push入訂閱者數(shù)組 Dep.target && dep.addDep(Dep.target); return val; } // ... 省略 }); // ... 省略
上面步驟搭建了watcher與observe之間的連接,還需要搭建watcher與之間的連接。
我們需要在compile中解析不同指令或者文本編譯集合的時候綁定watcher.
// compile.js // ...省略 model(node, vm, expr){ // 輸入框 let updater = this.updater["modelUpdate"] // 這里加一個監(jiān)控 數(shù)據(jù)變化了 應(yīng)該調(diào)用這個watcher的callback new Watcher(vm, expr, (newValue) => { // 當(dāng)值變化后 會調(diào)用cb ,將新值傳遞過來 updater && updater(node, this.getValue(vm, expr)) }) node.addEventListener("input", (e) => { let newValue = e.target.value this.setVal(vm, expr, newValue) }) updater && updater(node, this.getValue(vm, expr)) }, // ...省略
此時,在瀏覽器控制臺執(zhí)行下圖操作,手動改變 message 屬性的值,發(fā)現(xiàn)輸入框的值也隨之變化,v-model 綁定完成。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/102408.html
摘要:小白一枚,一直使用的是,想要多了解一些其它的框架,正好最近越來越火熱,上的數(shù)已經(jīng)超過了。框架理解說起這個模型,就不得不說框架。函數(shù)表示創(chuàng)建一個文本節(jié)點,函數(shù)表示創(chuàng)建一個數(shù)組。 小白一枚,一直使用的是React,想要多了解一些其它的框架,正好最近Vue越來越火熱,Github上的Star數(shù)已經(jīng)超過了React。而其背后蘊含的MVVM框架思想也一直跟React的組件化開發(fā)思想并駕齊驅(qū),在這...
摘要:在模式中一般把層算在層中,只有在理想的雙向綁定模式下,才會完全的消失。層將通過特定的展示出來,并在控件上綁定視圖交互事件,一般由框架自動生成在瀏覽器中。三大框架的異同三大框架都是數(shù)據(jù)驅(qū)動型的框架及是雙向數(shù)據(jù)綁定是單向數(shù)據(jù)綁定。 MVVM相關(guān)概念 1) MVVM典型特點是有四個概念:Model、View、ViewModel、綁定器。MVVM可以是單向綁定也可以是雙向綁定甚至是不綁...
摘要:工作中總結(jié)的一些比較重要的前端技能,覺得在面試中比較合適提問,即能查看出面試者的技術(shù)功底,又能考察其知識體系的廣度。異步編程的考察,其關(guān)鍵字的使用,與的關(guān)系,同時可以深入考察總共有幾種異步編程的方式。 工作中總結(jié)的一些比較重要的前端技能,覺得在面試中比較合適提問,即能查看出面試者的技術(shù)功底,又能考察其知識體系的廣度。適用于應(yīng)屆生和工作年限兩年下的同學(xué),掌握下面的知識基本滿足工作需求了。...
摘要:工作中總結(jié)的一些比較重要的前端技能,覺得在面試中比較合適提問,即能查看出面試者的技術(shù)功底,又能考察其知識體系的廣度。異步編程的考察,其關(guān)鍵字的使用,與的關(guān)系,同時可以深入考察總共有幾種異步編程的方式。 工作中總結(jié)的一些比較重要的前端技能,覺得在面試中比較合適提問,即能查看出面試者的技術(shù)功底,又能考察其知識體系的廣度。適用于應(yīng)屆生和工作年限兩年下的同學(xué),掌握下面的知識基本滿足工作需求了。...
閱讀 2158·2021-09-22 10:56
閱讀 1465·2021-09-07 10:11
閱讀 1801·2019-08-30 15:54
閱讀 2290·2019-08-30 15:44
閱讀 2307·2019-08-29 12:40
閱讀 3031·2019-08-28 18:25
閱讀 1735·2019-08-26 10:24
閱讀 3186·2019-08-23 18:39