安卓mvvm如何實現雙向綁定
1. MVVM框架如何解析雙向綁定
這篇文章主要介紹了MVVM 框架解析之雙向綁定,現在分享給大家,也給大家做個參考。
MVVM 框架
近年來前端一個明顯的開發趨勢就是架構從傳統的 MVC 模頌螞式向 MVVM 模式遷移。在傳統的 MVC 下,當前前端和後端發生數據交互後會刷新整個頁面,從而導致比較差的用戶體驗。因此我們通過 Ajax 的方式和網關 REST API 作通訊,非同步的刷新頁面的某個區塊,來優化和提升體驗。
MVVM 框架基本概念
在 MVVM 框架中,View(視圖) 和 Model(數據) 是不可以直接通訊的,在它們之間存在著 ViewModel 這個中間介充當著觀察者的角色。當用戶操作 View(視圖),ViewModel 感知到變化,然後通知 Model 發生相應改變;反之當 Model(數據) 發生改變,ViewModel 也能感知到變化,使 View 作出相應更新。這個一來一回的過程就是我們所熟知的雙向綁定。
MVVM 框架的應用場景
MVVM 框架的好處顯而易見:當前端對數據進行操作的時候,可以通過 Ajax 請求對數據持久化,只需改變 dom 里需要改變的那部分數據內容,而不必刷新整個頁面。特別是在移動端,刷新頁面的代價太昂貴。雖然有些資源會被緩存,但是頁面的 dom、css、js 都會被瀏覽器重新解析一遍,因此移動端頁面通常會被做成 SPA 單頁應用。由此在這基礎上誕生了很多 MVVM 框架,比如 React.js、Vue.js、Angular.js 等等。
MVVM 框架的簡單實現
模擬 Vue 的雙向綁定流,實現了一個簡單的MVVM 框架,從上圖中可以看出虛線方形中就是之前提到的 ViewModel 中間介層,它充當著觀察者的角色。另外可以發現雙向綁定流中的 View 到 Model 其實是通過 input 的事件監聽函數實現的,如果換成 React(單向綁定流) 的話,它在這一步交給狀態管理工具(比如 Rex)來實現。另外雙向綁定流中的 Model 到 View 其實各個 MVVM 框架實現的都是大同小異的,都用到的核心方法是 Object.defineProperty(),通過這個方法可以進行態隱數據劫持,當數據發生變化時可以捕捉到相應變化,從而進行後續的處理。
Mvvm(入口文件) 的實現
一般會這樣調用 Mvvm 框架
const vm = new Mvvm({
el: '#app',
data: {
title: 'mvvm title',
name: 'mvvm name'
},
})但是這樣子的話,如果要得到 title 屬性就要形如 vm.data.title 這樣取得,為了讓 vm.title 就能獲得 title 屬性,從而在 Mvvm 的 prototype 上加上一個代理方法,代碼如下:
function Mvvm (options) {
this.data = options.data
const self = this
Object.keys(this.data).forEach(key =>
self.proxyKeys(key)
)
}
Mvvm.prototype = {
proxyKeys: function(key) {
const self = this
Object.defineProperty(this, key, {
get: function () { // 這里的 get 和 set 實現了 vm.data.title 和 vm.title 的值同步
return self.data[key]
},
set: function (newValue) {
self.data[key] = newValue
}
})
}
}實現了代理方法後,就步入主流程的實現
function Mvvm (options) {
this.data = options.data
// ...
observe(this.data)
new Compile(options.el, this)
}observer(觀察者) 的實現
observer 的職責是監聽 Model(JS 對象) 的變化,最核野閉埋心的部分就是用到了 Object.defineProperty() 的 get 和 set 方法,當要獲取 Model(JS 對象) 的值時,會自動調用 get 方法;當改動了 Model(JS 對象) 的值時,會自動調用 set 方法;從而實現了對數據的劫持,代碼如下所示。
let data = {
number: 0
}
observe(data)
data.number = 1 // 值發生變化
function observe(data) {
if (!data || typeof(data) !== 'object') {
return
}
const self = this
Object.keys(data).forEach(key =>
self.defineReactive(data, key, data[key])
)
}
function defineReactive(data, key, value) {
observe(value) // 遍歷嵌套對象
Object.defineProperty(data, key, {
get: function() {
return value
},
set: function(newValue) {
if (value !== newValue) {
console.log('值發生變化', 'newValue:' + newValue + ' ' + 'oldValue:' + value)
value = newValue
}
}
})
}運行代碼,可以看到控制台輸出 值發生變化 newValue:1 oldValue:0,至此就完成了 observer 的邏輯。
Dep(訂閱者數組) 和 watcher(訂閱者) 的關系
觀測到變化後,我們總要通知給特定的人群,讓他們做出相應的處理吧。為了更方便地理解,我們可以把訂閱當成是訂閱了一個微信公眾號,當微信公眾號的內容有更新時,那麼它會把內容推送(update) 到訂閱了它的人。
那麼訂閱了同個微信公眾號的人有成千上萬個,那麼首先想到的就是要 new Array() 去存放這些人(html 節點)吧。於是就有了如下代碼:
// observer.js
function Dep() {
this.subs = [] // 存放訂閱者
}
Dep.prototype = {
addSub: function(sub) { // 添加訂閱者
this.subs.push(sub)
},
notify: function() { // 通知訂閱者更新
this.subs.forEach(function(sub) {
sub.update()
})
}
}
function observe(data) {...}
function defineReactive(data, key, value) {
var dep = new Dep()
observe(value) // 遍歷嵌套對象
Object.defineProperty(data, key, {
get: function() {
if (Dep.target) { // 往訂閱器添加訂閱者
dep.addSub(Dep.target)
}
return value
},
set: function(newValue) {
if (value !== newValue) {
console.log('值發生變化', 'newValue:' + newValue + ' ' + 'oldValue:' + value)
value = newValue
dep.notify()
}
}
})
}初看代碼也比較順暢了,但可能會卡在 Dep.target 和 sub.update,由此自然而然地將目光移向 watcher,
// watcher.js
function Watcher(vm, exp, cb) {
this.vm = vm
this.exp = exp
this.cb = cb
this.value = this.get()
}
Watcher.prototype = {
update: function() {
this.run()
},
run: function() {
// ...
if (value !== oldVal) {
this.cb.call(this.vm, value) // 觸發 compile 中的回調
}
},
get: function() {
Dep.target = this // 緩存自己
const value = this.vm.data[this.exp] // 強制執行監聽器里的 get 函數
Dep.target = null // 釋放自己
return value
}
}從代碼中可以看到當構造 Watcher 實例時,會調用 get() 方法,接著重點關注 const value = this.vm.data[this.exp] 這句,前面說了當要獲取 Model(JS 對象) 的值時,會自動調用 Object.defineProperty 的 get 方法,也就是當執行完這句的時候,Dep.target 的值傳進了 observer.js 中的 Object.defineProperty 的 get 方法中。同時也一目瞭然地在 Watcher.prototype 中發現了 update 方法,其作用即觸發 compile 中綁定的回調來更新界面。至此解釋了 Observer 中 Dep.target 和 sub.update 的由來。
來歸納下 Watcher 的作用,其充當了 observer 和 compile 的橋梁。
1 在自身實例化的過程中,往訂閱器(dep) 中添加自己
2 當 model 發生變動,dep.notify() 通知時,其能調用自身的 update 函數,並觸發 compile 綁定的回調函數實現視圖更新
最後再來看下生成 Watcher 實例的 compile.js 文件。
compile(編譯) 的實現
首先遍歷解析的過程有多次操作 dom 節點,為提高性能和效率,會先將跟節點 el 轉換成 fragment(文檔碎片) 進行解析編譯,解析完成,再將 fragment 添加回原來的真實 dom 節點中。代碼如下:
function Compile(el, vm) {
this.vm = vm
this.el = document.querySelector(el)
this.fragment = null
this.init()
}
Compile.prototype = {
init: function() {
if (this.el) {
this.fragment = this.nodeToFragment(this.el) // 將節點轉為 fragment 文檔碎片
this.compileElement(this.fragment) // 對 fragment 進行編譯解析
this.el.appendChild(this.fragment)
}
},
nodeToFragment: function(el) {
const fragment = document.createDocumentFragment()
let child = el.firstChild // △ 第一個 firstChild 是 text
while(child) {
fragment.appendChild(child)
child = el.firstChild
}
return fragment
},
compileElement: function(el) {...},
}這個簡單的 mvvm 框架在對 fragment 編譯解析的過程中對 {{}} 文本元素、v-on:click 事件指令、v-model 指令三種類型進行了相應的處理。
Compile.prototype = {
init: function() {
if (this.el) {
this.fragment = this.nodeToFragment(this.el) // 將節點轉為 fragment 文檔碎片
this.compileElement(this.fragment) // 對 fragment 進行編譯解析
this.el.appendChild(this.fragment)
}
},
nodeToFragment: function(el) {...},
compileElement: function(el) {...},
compileText: function (node, exp) { // 對文本類型進行處理,將 {{abc}} 替換掉
const self = this
const initText = this.vm[exp]
this.updateText(node, initText) // 初始化
new Watcher(this.vm, exp, function(value) { // 實例化訂閱者
self.updateText(node, value)
})
},
compileEvent: function (node, vm, exp, dir) { // 對事件指令進行處理
const eventType = dir.split(':')[1]
const cb = vm.methods && vm.methods[exp]
if (eventType && cb) {
node.addEventListener(eventType, cb.bind(vm), false)
}
},
compileModel: function (node, vm, exp) { // 對 v-model 進行處理
let val = vm[exp]
const self = this
this.modelUpdater(node, val)
node.addEventListener('input', function (e) {
const newValue = e.target.value
self.vm[exp] = newValue // 實現 view 到 model 的綁定
})
},
}在上述代碼的 compileTest 函數中看到了期盼已久的 Watcher 實例化,對 Watcher 作用模糊的朋友可以往上回顧下 Watcher 的作用。另外在 compileModel 函數中看到了本文最開始提到的雙向綁定流中的 View 到 Model 是藉助 input 監聽事件變化實現的。
項目地址
本文記錄了些閱讀 mvvm 框架源碼關於雙向綁定的心得,並動手實踐了一個簡版的 mvvm 框架,不足之處在所難免,歡迎指正。
上面是我整理給大家的,希望今後會對大家有幫助。
相關文章:
通過微信小程序如何實現驗證碼獲取倒計時效果
ES6 迭代器和 for.of循環(詳細教程)
在vue中使用better-scroll滾動插件
在VUE + UEditor中如何實現單圖片跨域上傳功能
2. 實現雙向數據綁定
MVVM框架主要包含3個部分: model 、 view 和 viewmodel 。
簡單的來說,就是框架的控制器層(這里的控制器層是一個泛指,可以理解為控制view行為和聯系model層的中間件)和UI展示層(view層)建立一個雙向的數據通道。當這兩層中的任何一方發生變化時,另一層將會自動作出相應的變化。
一般來說要實現這種雙向數據綁定,在前端我目前了解的有三種形式:
目前angular,regular的實現都是基於臟檢查。當發生某些特定的事情的時候,框架會調用相關的digest方法。內部邏輯就是遍歷所有的 watcher ,對監控的屬性做對比。如果值發生了變化,則執行相應的 handler 。
當系統進入臟檢查階段,遍歷所有的 $watch 綁定的 watcher ,然後對比 watcher.get() 與 watcher.last ,如果不同則運行對應的 watcher.fn(newvalue, oldvalue) 。然後再進入下一個watcher的檢查。
何時進行臟檢查?
由於regularjs是基於臟檢查,所以當不是由regularjs本身控制的悶鏈友操作(如事件、指令)引起的數據操作,可能需要你手動的去同步data與view的數據. $update方法即幫助將你的data同步到view層.
]( https://regularjs.github.io/reference?syntax-zh#bind-once )元素來控制你的觀察者數量。
使用ES7中的 Object.observe 方法對對象(或者其屬性)進行監螞槐控觀察,一旦其發生喚基變化時,將會執行相應的handler。這是目前監控屬性數據變更最完美的一種方法,語言(瀏覽器)原生支持,沒有什麼比這個更好了。唯一的遺憾就是目前支持廣度還不行,有待全面推廣。
vue.js和avalon.js實現數據雙向綁定的原理就是屬性訪問器。
它使用了ES5中的定義標准屬性的Object.defineProperty 方法。
Object.defineProperty 使用示例:
首先,vuejs在實例化的過程中,會對遍歷傳給實例化對象選項中的data 選項,遍歷其所有屬性並使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。
同時每一個實例對象都有一個watcher實例對象,他會在模板編譯的過程中,用getter去訪問data的屬性,watcher此時就會把用到的data屬性記為依賴,這樣就建立了視圖與數據之間的聯系。當之後我們渲染視圖的數據依賴發生改變(即數據的setter被調用)的時候,watcher會對比前後兩個的數值是否發生變化,然後確定是否通知視圖進行重新渲染。這樣就實現了所謂的雙向數據綁定。