对类Vue的MVVM前端库的实现
關(guān)于實現(xiàn)MVVM,網(wǎng)上實在是太多了,本文為個人總結(jié),結(jié)合源碼以及一些別人的實現(xiàn)
關(guān)于雙向綁定
- vue 數(shù)據(jù)劫持 + 訂閱 - 發(fā)布
- ng 臟值檢查
- backbone.js 訂閱-發(fā)布(這個沒有使用過,并不是主流的用法)
雙向綁定,從最基本的實現(xiàn)來說,就是在defineProperty綁定的基礎(chǔ)上在綁定input事件,達到v-model的功能
代碼思路圖
兩個版本:
- 簡單版本: 非常簡單,但是因為是es6,并且代碼極度簡化,所以不談功能,思路還是很清晰的
- 標準版本: 參照了Vue的部分源碼,代碼的功能高度向上抽取,閱讀稍微有點困難,實現(xiàn)了基本的功能,包括計算屬性,watch,核心功能都實現(xiàn)沒問題,但是不支持數(shù)組
簡單版本
簡單版本的地址: 簡單版本
? 這個MVVM也許代碼邏輯上面實現(xiàn)的并不完美,并不是正統(tǒng)的MVVM, 但是代碼很精簡,相對于源碼,要好理解很多,并且實現(xiàn)了v-model以及v-on methods的功能,代碼非常少,就100多行
class MVVM {constructor(options) {const {el,data,methods} = optionsthis.methods = methodsthis.target = nullthis.observer(this, data)this.instruction(document.getElementById(el)) // 獲取掛載點}// 數(shù)據(jù)監(jiān)聽器 攔截所有data數(shù)據(jù) 傳給defineProperty用于數(shù)據(jù)劫持observer(root, data) {for (const key in data) {this.definition(root, key, data[key])}}// 將攔截的數(shù)據(jù)綁定到this上面definition(root, key, value) {// if (typeof value === 'object') { // 假如value是對象則接著遞歸// return this.observer(value, value)// }let dispatcher = new Dispatcher() // 調(diào)度員Object.defineProperty(root, key, {set(newValue) {value = newValuedispatcher.notify(newValue)},get() {dispatcher.add(this.target)return value}})}//指令解析器instruction(dom) {const nodes = dom.childNodes; // 返回節(jié)點的子節(jié)點集合// console.log(nodes); //查看節(jié)點屬性for (const node of nodes) { // 與for in相反 for of 獲取迭代的value值if (node.nodeType === 1) { // 元素節(jié)點返回1const attrs = node.attributes //獲取屬性for (const attr of attrs) {if (attr.name === 'v-model') {let value = attr.value //獲取v-model的值node.addEventListener('input', e => { // 鍵盤事件觸發(fā)this[value] = e.target.value})this.target = new Watcher(node, 'input') // 儲存到訂閱者this[value] // get一下,將 this.target 給調(diào)度員}if (attr.name == "@click") {let value = attr.value // 獲取點擊事件名node.addEventListener('click',this.methods[value].bind(this))}}}if (node.nodeType === 3) { // 文本節(jié)點返回3let reg = /\{\{(.*)\}\}/; //匹配 {{ }}let match = node.nodeValue.match(reg)if (match) { // 匹配都就獲取{{}}里面的變量const value = match[1].trim()this.target = new Watcher(node, 'text')this[value] = this[value] // get set更新一下數(shù)據(jù)}}}} }//調(diào)度員 > 調(diào)度訂閱發(fā)布 class Dispatcher {constructor() {this.watchers = []}add(watcher) {this.watchers.push(watcher) // 將指令解析器解析的數(shù)據(jù)節(jié)點的訂閱者存儲進來,便于訂閱}notify(newValue) {this.watchers.map(watcher => watcher.update(newValue))// 有數(shù)據(jù)發(fā)生,也就是觸發(fā)set事件,notify事件就會將新的data交給訂閱者,訂閱者負責(zé)更新} }//訂閱發(fā)布者 MVVM核心 class Watcher {constructor(node, type) {this.node = nodethis.type = type}update(value) {if (this.type === 'input') {this.node.value = value // 更新的數(shù)據(jù)通過訂閱者發(fā)布到dom}if (this.type === 'text') {this.node.nodeValue = value}} } <!DOCTYPE html> <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>MVVM</title> </head><body><div id="app"><input type="text" v-model="text">{{ text }}<br><button @click="update">重置</button></div><script src="./index.js"></script><script>let mvvm = new MVVM({el: 'app',data: {text: 'hello MVVM'},methods: {update() {this.text = ''}}})</script> </body></html>這個版本的MVVM因為代碼比較少,并且是ES6的原因,思路非常清晰
我們來看看從new MVVM開始,他都做了什么
解讀簡單版本
new MVVM
首先,通過解構(gòu)獲取所有的new MVVM傳進來的對象
class MVVM {constructor(options) {const {el,data,methods} = optionsthis.methods = methods // 提取methods,便于后面將this給methodsthis.target = null // 后面有用this.observer(this, data)this.instruction(document.getElementById(el)) // 獲取掛載點}屬性劫持
開始執(zhí)行this.observer observer是一個數(shù)據(jù)監(jiān)聽器,將data的數(shù)據(jù)全部攔截下來
observer(root, data) {for (const key in data) {this.definition(root, key, data[key])}}在this.definition里面把data數(shù)據(jù)都劫持到this上面
definition(root, key, value) {if (typeof value === 'object') { // 假如value是對象則接著遞歸return this.observer(value, value)}let dispatcher = new Dispatcher() // 調(diào)度員Object.defineProperty(root, key, {set(newValue) {value = newValuedispatcher.notify(newValue)},get() {dispatcher.add(this.target)return value}})}此時data的數(shù)據(jù)變化我們已經(jīng)可以監(jiān)聽到了,但是我們監(jiān)聽到后還要與頁面進行實時相應(yīng),所以這里我們使用調(diào)度員,在頁面初始化的時候get(),這樣this.target,也就是后面的指令解析器解析出來的v-model這樣的指令儲存到調(diào)度員里面,主要請看后面的解析器的代碼
指令解析器
指令解析器通過執(zhí)行 this.instruction(document.getElementById(el)) 獲取掛載點
instruction(dom) {const nodes = dom.childNodes; // 返回節(jié)點的子節(jié)點集合// console.log(nodes); //查看節(jié)點屬性for (const node of nodes) { // 與for in相反 for of 獲取迭代的value值if (node.nodeType === 1) { // 元素節(jié)點返回1const attrs = node.attributes //獲取屬性for (const attr of attrs) {if (attr.name === 'v-model') {let value = attr.value //獲取v-model的值node.addEventListener('input', e => { // 鍵盤事件觸發(fā)this[value] = e.target.value})this.target = new Watcher(node, 'input') // 儲存到訂閱者this[value] // get一下,將 this.target 給調(diào)度員}if (attr.name == "@click") {let value = attr.value // 獲取點擊事件名node.addEventListener('click',this.methods[value].bind(this))}}}if (node.nodeType === 3) { // 文本節(jié)點返回3let reg = /\{\{(.*)\}\}/; //匹配 {{ }}let match = node.nodeValue.match(reg)if (match) { // 匹配都就獲取{{}}里面的變量const value = match[1].trim()this.target = new Watcher(node, 'text')this[value] = this[value] // get set更新一下數(shù)據(jù)}}}}這里代碼首先解析出來我們自定義的屬性然后,我們將@click的事件直接指向methods,methds就已經(jīng)實現(xiàn)了
現(xiàn)在代碼模型是這樣
調(diào)度員Dispatcher與訂閱者Watcher
我們需要將Dispatcher和Watcher聯(lián)系起來
于是我們之前創(chuàng)建的變量this.target開始發(fā)揮他的作用了
正執(zhí)行解析器里面使用this.target將node節(jié)點,以及觸發(fā)關(guān)鍵詞存儲到當(dāng)前的watcher 訂閱,然后我們獲取一下數(shù)據(jù)
this.target = new Watcher(node, 'input') // 儲存到訂閱者 this[value] // get一下,將 this.target 給調(diào)度員在執(zhí)行this[value]的時候,觸發(fā)了get事件
get() {dispatcher.add(this.target)return value }這get事件里面,我們將watcher訂閱者告知到調(diào)度員,調(diào)度員將訂閱事件存儲起來
//調(diào)度員 > 調(diào)度訂閱發(fā)布 class Dispatcher {constructor() {this.watchers = []}add(watcher) {this.watchers.push(watcher) // 將指令解析器解析的數(shù)據(jù)節(jié)點的訂閱者存儲進來,便于訂閱}notify(newValue) {this.watchers.map(watcher => watcher.update(newValue))// 有數(shù)據(jù)發(fā)生,也就是觸發(fā)set事件,notify事件就會將新的data交給訂閱者,訂閱者負責(zé)更新} }與input不太一樣的是文本節(jié)點不僅需要獲取,還需要set一下,因為要讓訂閱者更新node節(jié)點
this.target = new Watcher(node, 'text') this[value] = this[value] // get set更新一下數(shù)據(jù)所以在訂閱者就添加了該事件,然后執(zhí)行set
set(newValue) {value = newValuedispatcher.notify(newValue)},notfiy執(zhí)行,訂閱發(fā)布者執(zhí)行update更新node節(jié)點信息
class Watcher {constructor(node, type) {this.node = nodethis.type = type}update(value) {if (this.type === 'input') {this.node.value = value // 更新的數(shù)據(jù)通過訂閱者發(fā)布到dom}if (this.type === 'text') {this.node.nodeValue = value}} }頁面初始化完畢
更新數(shù)據(jù)
node.addEventListener('input', e => { // 鍵盤事件觸發(fā)this[value] = e.target.value })this[value]也就是data數(shù)據(jù)發(fā)生變化,觸發(fā)set事件,既然觸發(fā)notfiy事件,notfiy遍歷所有節(jié)點,在遍歷的節(jié)點里面根據(jù)頁面初始化的時候訂閱的觸發(fā)類型.進行頁面的刷新
現(xiàn)在可以完成的看看new MVVM的實現(xiàn)過程了
最簡單版本的MVVM完成
標準版本
標準版本額外實現(xiàn)了component,watch,因為模塊化代碼很碎的關(guān)系,看起來還是有難度的
從理念上來說,實現(xiàn)的思想基本是一樣的,可以參照上面的圖示,都是開始的時候都是攔截屬性,解析指令
代碼有將近300行,所以就貼一個地址標準版本MVVM
執(zhí)行順序
更新數(shù)據(jù):
component的實現(xiàn)
計算屬性的觸發(fā) 查看這個例子
computed: {getHelloWord: function () {return this.someStr + this.child.someStr;}},其實計算屬性就是defineproperty的一個延伸
是function執(zhí)行computed[getHelloword],也就是return 的 函數(shù)
this.someStr + this.child.someStr;初始化完成,到這里還沒有綁定數(shù)據(jù),僅僅是初始化完成了
這個執(zhí)行順序有點迷,第二第三方反來了
this.parseGetter(expOrFn);就執(zhí)行完畢了
目前來看為什么component會實時屬性數(shù)據(jù)?
因為component的依賴屬性一旦發(fā)生變化都會更新 getHelloword 的 watcher ,隨之執(zhí)行回調(diào)更新dom
watch的實現(xiàn)
watch的實現(xiàn)相對來說要簡單很多
轉(zhuǎn)載于:https://www.cnblogs.com/wuvkcyan/p/9602562.html
總結(jié)
以上是生活随笔為你收集整理的对类Vue的MVVM前端库的实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 洛谷 P1136 迎接仪式 解题报告
- 下一篇: elementUI vue table