国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

對類Vue的MVVM前端庫的實現代碼

2019-11-19 13:00:11
字體:
來源:轉載
供稿:網友

MVVM

(ModelView ViewModel)是一種基于MVC的設計,開發人員在HTML上寫一些Bindings,利用一些指令綁定,就能在Model和ViewModel保持不變的情況下,很方便的將UI設計與業務邏輯分離,從而大大的減少繁瑣的DOM操作。

關于實現MVVM,網上實在是太多了,本文為個人總結,結合源碼以及一些別人的實現

關于雙向綁定

•vue 數據劫持 + 訂閱 - 發布
•ng 臟值檢查
•backbone.js 訂閱-發布(這個沒有使用過,并不是主流的用法)

雙向綁定,從最基本的實現來說,就是在defineProperty綁定的基礎上在綁定input事件,達到v-model的功能

代碼思路圖

兩個版本:

•簡單版本: 非常簡單,但是因為是es6,并且代碼極度簡化,所以不談功能,思路還是很清晰的
•標準版本: 參照了Vue的部分源碼,代碼的功能高度向上抽取,閱讀稍微有點困難,實現了基本的功能,包括計算屬性,watch,核心功能都實現沒問題,但是不支持數組

簡單版本

簡單版本的地址: 簡單版本

​ 這個MVVM也許代碼邏輯上面實現的并不完美,并不是正統的MVVM, 但是代碼很精簡,相對于源碼,要好理解很多,并且實現了v-model以及v-on methods的功能,代碼非常少,就100多行

class MVVM { constructor(options) { const {  el,  data,  methods } = options this.methods = methods this.target = null this.observer(this, data) this.instruction(document.getElementById(el)) // 獲取掛載點 } // 數據監聽器 攔截所有data數據 傳給defineProperty用于數據劫持 observer(root, data) { for (const key in data) {  this.definition(root, key, data[key]) } } // 將攔截的數據綁定到this上面 definition(root, key, value) { // if (typeof value === 'object') { // 假如value是對象則接著遞歸 // return this.observer(value, value) // } let dispatcher = new Dispatcher() // 調度員 Object.defineProperty(root, key, {  set(newValue) {  value = newValue  dispatcher.notify(newValue)  },  get() {  dispatcher.add(this.target)  return value  } }) } //指令解析器 instruction(dom) { const nodes = dom.childNodes; // 返回節點的子節點集合 // console.log(nodes); //查看節點屬性 for (const node of nodes) { // 與for in相反 for of 獲取迭代的value值  if (node.nodeType === 1) { // 元素節點返回1  const attrs = node.attributes //獲取屬性  for (const attr of attrs) {   if (attr.name === 'v-model') {   let value = attr.value //獲取v-model的值   node.addEventListener('input', e => { // 鍵盤事件觸發    this[value] = e.target.value   })   this.target = new Watcher(node, 'input') // 儲存到訂閱者   this[value] // get一下,將 this.target 給調度員   }   if (attr.name == "@click") {   let value = attr.value // 獲取點擊事件名   node.addEventListener('click',    this.methods[value].bind(this)   )   }  }  }  if (node.nodeType === 3) { // 文本節點返回3  let 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更新一下數據  }  } } }}//調度員 > 調度訂閱發布class Dispatcher { constructor() { this.watchers = [] } add(watcher) { this.watchers.push(watcher) // 將指令解析器解析的數據節點的訂閱者存儲進來,便于訂閱 } notify(newValue) { this.watchers.map(watcher => watcher.update(newValue)) // 有數據發生,也就是觸發set事件,notify事件就會將新的data交給訂閱者,訂閱者負責更新 }}//訂閱發布者 MVVM核心class Watcher { constructor(node, type) { this.node = node this.type = type } update(value) { if (this.type === 'input') {  this.node.value = value // 更新的數據通過訂閱者發布到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

首先,通過解構獲取所有的new MVVM傳進來的對象

class MVVM { constructor(options) { const {  el,  data,  methods } = options this.methods = methods // 提取methods,便于后面將this給methods this.target = null // 后面有用 this.observer(this, data) this.instruction(document.getElementById(el)) // 獲取掛載點 }

屬性劫持

開始執行this.observer observer是一個數據監聽器,將data的數據全部攔截下來

observer(root, data) { for (const key in data) {  this.definition(root, key, data[key]) } }

在this.definition里面把data數據都劫持到this上面

 definition(root, key, value) { if (typeof value === 'object') { // 假如value是對象則接著遞歸  return this.observer(value, value) } let dispatcher = new Dispatcher() // 調度員 Object.defineProperty(root, key, {  set(newValue) {  value = newValue  dispatcher.notify(newValue)  },  get() {  dispatcher.add(this.target)  return value  } }) }

此時data的數據變化我們已經可以監聽到了,但是我們監聽到后還要與頁面進行實時相應,所以這里我們使用調度員,在頁面初始化的時候get(),這樣this.target,也就是后面的指令解析器解析出來的v-model這樣的指令儲存到調度員里面,主要請看后面的解析器的代碼

指令解析器

指令解析器通過執行 this.instruction(document.getElementById(el)) 獲取掛載點

instruction(dom) { const nodes = dom.childNodes; // 返回節點的子節點集合 // console.log(nodes); //查看節點屬性 for (const node of nodes) { // 與for in相反 for of 獲取迭代的value值  if (node.nodeType === 1) { // 元素節點返回1  const attrs = node.attributes //獲取屬性  for (const attr of attrs) {   if (attr.name === 'v-model') {   let value = attr.value //獲取v-model的值   node.addEventListener('input', e => { // 鍵盤事件觸發    this[value] = e.target.value   })   this.target = new Watcher(node, 'input') // 儲存到訂閱者   this[value] // get一下,將 this.target 給調度員   }   if (attr.name == "@click") {   let value = attr.value // 獲取點擊事件名   node.addEventListener('click',    this.methods[value].bind(this)   )   }  }  }  if (node.nodeType === 3) { // 文本節點返回3  let 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更新一下數據  }  } } }

這里代碼首先解析出來我們自定義的屬性然后,我們將@click的事件直接指向methods,methds就已經實現了

現在代碼模型是這樣

調度員Dispatcher與訂閱者Watcher

我們需要將Dispatcher和Watcher聯系起來

于是我們之前創建的變量this.target開始發揮他的作用了

正執行解析器里面使用this.target將node節點,以及觸發關鍵詞存儲到當前的watcher 訂閱,然后我們獲取一下數據

this.target = new Watcher(node, 'input') // 儲存到訂閱者this[value] // get一下,將 this.target 給調度員

在執行this[value]的時候,觸發了get事件

get() { dispatcher.add(this.target) return value}

這get事件里面,我們將watcher訂閱者告知到調度員,調度員將訂閱事件存儲起來

//調度員 > 調度訂閱發布class Dispatcher { constructor() { this.watchers = [] } add(watcher) { this.watchers.push(watcher) // 將指令解析器解析的數據節點的訂閱者存儲進來,便于訂閱 } notify(newValue) { this.watchers.map(watcher => watcher.update(newValue)) // 有數據發生,也就是觸發set事件,notify事件就會將新的data交給訂閱者,訂閱者負責更新 }}

與input不太一樣的是文本節點不僅需要獲取,還需要set一下,因為要讓訂閱者更新node節點

this.target = new Watcher(node, 'text')this[value] = this[value] // get set更新一下數據

所以在訂閱者就添加了該事件,然后執行set

set(newValue) {  value = newValue  dispatcher.notify(newValue)  },

notfiy執行,訂閱發布者執行update更新node節點信息

class Watcher { constructor(node, type) { this.node = node this.type = type } update(value) { if (this.type === 'input') {  this.node.value = value // 更新的數據通過訂閱者發布到dom } if (this.type === 'text') {  this.node.nodeValue = value } }}

頁面初始化完畢

更新數據

node.addEventListener('input', e => { // 鍵盤事件觸發 this[value] = e.target.value})

this[value]也就是data數據發生變化,觸發set事件,既然觸發notfiy事件,notfiy遍歷所有節點,在遍歷的節點里面根據頁面初始化的時候訂閱的觸發類型.進行頁面的刷新

現在可以完成的看看new MVVM的實現過程了

最簡單版本的MVVM完成

標準版本

標準版本額外實現了component,watch,因為模塊化代碼很碎的關系,看起來還是有難度的

從理念上來說,實現的思想基本是一樣的,可以參照上面的圖示,都是開始的時候都是攔截屬性,解析指令

代碼有將近300行,所以就貼一個地址標準版本MVVM

執行順序

1.new MVVM
2.獲取$options = 所以參數
3.獲取data,便于后面劫持
4.因為是es5,后面forEach內部指向window,這不是我們想要的,所以存儲當前this 為me
5._proxyData劫持所有data數據
6.初始化計算屬性
7.通過Object.key()獲取計算屬性的屬性名
8.初始化計算屬性將計算屬性掛載到vm上
9.開始observer監聽數據
10.判斷data是否存在
11.存在就new Observer(創建監聽器)
12.數據全部進行進行defineProperty存取監聽處理,讓后面的數據變動都觸發這個的get/set
13.開始獲取掛載點
14.使用querySelector對象解析el
15.創建一個虛擬節點,并存儲當前的dom
16.解析虛擬dom
17.使用childNodes解析對象
18.因為是es5,所以使用[].slice.call將對象轉數組
19.獲取到后進行 {{ }}匹配 指令的匹配 以及遞歸子節點
20.指令的匹配: 匹配到指令因為不知道多少個指令名稱,所以這里還是使用[].slice.call循環遍歷
21.解析到有 v-的指令使用substring(2)截取后面的屬性名稱
22.再判斷是不是指令v-on 這里就是匹配on關鍵字,匹配到了就是事件指令,匹配不到就是普通指令
23.普通指令解析{{ data }} _getVMValget會觸發MVVM的_proxyData事件 在_proxyData事件里面觸發data的get事件
24.這時候到了observer的defineReactive的get里面獲取到了數據,因為沒有Dispatcher.target,所以不進行會觸發調度員
25.至此_getVMVal獲取到了數據
26.modelUpdater進行Dom上面的數據更新
27.數據開始進行訂閱,在訂閱里面留一個回調函數用于更新dom
28.在watcher(訂閱者)獲取this,訂閱的屬性,回調
29.在this.getter這個屬性上面返回一個匿名函數,用于獲取data的值
30.觸發get事件,將當前watcher的this存儲到Dispatcher.garget上面
31.給this.getters,callvm的的this,執行匿名函數,獲取劫持下來的data,又觸發了MVVM的_proxyData的get事件,繼而有觸發了observer的defineReactive的get事件,不過這一次Dispatcher.target有值,執行了depend事件
32.在depend里面執行了自己的addDep事件,并且將Observer自己的this傳進去
33.addDep里面執行了Dispatcher的addSub事件,
34.在addUsb事件里面將訂閱存儲到Dispatcher里面的this.watchers里面的
35.訂閱完成,后面將這些自定義的指令進行移除
36.重復操作,解析所有指令,v-on:click = "data"直接執行methods[data].bind(vm)

更新數據:

1.觸發input事件
2.觸發_setVMVal事件
3.觸發MVVM的set事件
4.觸發observer的set事件
5.觸發dep.notify()
6.觸發watcher的run方法
7.觸發new Watcher的回調 this.cb
8.觸發compile里面的updaterFn 事件
9.更新視圖

component的實現

計算屬性的觸發 查看這個例子

computed: {  getHelloWord: function () {   return this.someStr + this.child.someStr;  }  },

其實計算屬性就是defineproperty的一個延伸

1.首先compile里面解析獲取到{{ getHelloword }}'
2.執行updater[textUpdater]
3.執行_getVMVal獲取計算屬性的返回值
4.獲取vm[component]就會執行下面的get事件

Object.defineProperty(me, key, {   get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,   set: function () {}  })

是function執行computed[getHelloword],也就是return 的 函數

this.someStr + this.child.someStr;

1.依次獲取data,觸發mvvm的get 以及observer的get,

初始化完成,到這里還沒有綁定數據,僅僅是初始化完成了

1.開始訂閱該事件 new Watcher()
2.component不是函數所以不是function 執行this.parseGetter(expOrFn);
3.返回一個覆蓋expOrrn的匿名函數
4.開始初始化 執行get()
5.存儲當前this,開始獲取vm[getHelloword]
6.觸發component[getHelloword]
7.開始執行MVVM的get this.someStr
8.到MVVM的get 到 observer的get 因為 Dispatcher.target存著 getHelloWord 的 this.depend ()所以執行
9.Dispatcher的depend(),執行watcher的addDep(),執行 Dispatcher的addSub() 將當前的watcher存儲到監聽器
10.開始get第二個數據 this.child.someStr,同理也將getHelloWord的this存入了當前的Dispatcher
11.開始get第三個數據 this.child,同理也將getHelloWord的this存入了當前的Dispatcher

這個執行順序有點迷,第二第三方反來了

this.parseGetter(expOrFn);就執行完畢了

目前來看為什么component會實時屬性數據?

因為component的依賴屬性一旦發生變化都會更新 getHelloword 的 watcher ,隨之執行回調更新dom

watch的實現

watch的實現相對來說要簡單很多

1.我們只要將watch監聽的數據告訴訂閱者就可以了
2.這樣,wacth更新了
3.觸發set,set觸發notify
4.notify更新watcher
5.watcher執行run
6.run方法去執行watch的回調
7.即完成了watch的監聽

watch: function (key, cb) { new Watcher(this, key, cb)},


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 平远县| 凯里市| 保山市| 宁化县| 阳朔县| 家居| 黑河市| 师宗县| 化州市| 萝北县| 武宁县| 古浪县| 宁都县| 江都市| 濮阳市| 长岛县| 乐陵市| 宣城市| 昆山市| 车险| 天津市| 遂溪县| 陇南市| 台湾省| 郑州市| 江孜县| 孙吴县| 松溪县| 麦盖提县| 海林市| 蓝山县| 清涧县| 绥中县| 克山县| 长岛县| 抚松县| 南漳县| 易门县| 延寿县| 周口市| 唐河县|