本文采用了比較特殊的input和v-model指令 實(shí)際上vue的指令解析模板很復(fù)雜,本文重點(diǎn)是理解數(shù)據(jù)更新的思想
幾種實(shí)現(xiàn)雙向綁定的做法
目前幾種主流的mvc(vm)框架都實(shí)現(xiàn)了單向數(shù)據(jù)綁定腹纳,而我所理解的雙向數(shù)據(jù)綁定無非就是在單向綁定的基礎(chǔ)上給可輸入元素(input、textare等)添加了change(input)事件锨并,來動(dòng)態(tài)修改model和 view聪蘸,并沒有多高深数焊。所以無需太過介懷是實(shí)現(xiàn)的單向或雙向綁定。
實(shí)現(xiàn)數(shù)據(jù)綁定的做法有大致如下幾種:
發(fā)布者-訂閱者模式(backbone.js)
臟值檢查(angular.js)
數(shù)據(jù)劫持(vue.js)
發(fā)布者-訂閱者模式:
一般通過sub, pub的方式實(shí)現(xiàn)數(shù)據(jù)和視圖的綁定監(jiān)聽膨处,更新數(shù)據(jù)方式通常做法是 vm.set('property', value)见秤,這里有篇文章講的比較詳細(xì),有興趣可點(diǎn)這里
這種方式現(xiàn)在畢竟太low了真椿,我們更希望通過 vm.property = value 這種方式更新數(shù)據(jù)鹃答,同時(shí)自動(dòng)更新視圖,于是有了下面兩種方式
臟值檢查:
angular.js 是通過臟值檢測的方式比對數(shù)據(jù)是否有變更突硝,來決定是否更新視圖挣跋,最簡單的方式就是通過 setInterval() 定時(shí)輪詢檢測數(shù)據(jù)變動(dòng),當(dāng)然Google不會(huì)這么low狞换,angular只有在指定的事件觸發(fā)時(shí)進(jìn)入臟值檢測避咆,大致如下:
DOM事件,譬如用戶輸入文本修噪,點(diǎn)擊按鈕等查库。( ng-click )
XHR響應(yīng)事件 ( $http )
瀏覽器Location變更事件 ( $location )
Timer事件( $timeout , $interval )
執(zhí)行 $digest() 或 $apply()
數(shù)據(jù)劫持:
vue.js 則是采用數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式,通過Object.defineProperty()來劫持各個(gè)屬性的setter黄琼,getter樊销,在數(shù)據(jù)變動(dòng)時(shí)發(fā)布消息給訂閱者,觸發(fā)相應(yīng)的監(jiān)聽回調(diào)脏款。
思路整理
- 實(shí)現(xiàn)一個(gè)數(shù)據(jù)監(jiān)聽器Observer围苫,能夠?qū)?shù)據(jù)對象的所有屬性進(jìn)行監(jiān)聽,如有變動(dòng)可拿到最新值并通知訂閱者
- 實(shí)現(xiàn)一個(gè)指令解析器Compile撤师,對每個(gè)元素節(jié)點(diǎn)的指令進(jìn)行掃描和解析剂府,根據(jù)指令模板替換數(shù)據(jù),以及綁定相應(yīng)的更新函數(shù)
- 實(shí)現(xiàn)一個(gè)Watcher剃盾,作為連接Observer和Compile的橋梁腺占,能夠訂閱并收到每個(gè)屬性變動(dòng)的通知淤袜,執(zhí)行指令綁定的相應(yīng)回調(diào)函數(shù),從而更新視圖
- 入口函數(shù)衰伯,整合以上三者
流程圖
數(shù)據(jù)監(jiān)聽器
function observe(obj, vm) {
// 對傳入的對象 遍歷 并分別添加 object.defineProperty
Object.keys(obj).forEach((key) => {
defineReactive(vm, key, obj[key])
})
}
function defineReactive(vm, key, val) {
var dep = new Dep();
Object.defineProperty(vm, key, {
get: function () {
// 通過這一步 添加訂閱者
if (Dep.target) dep.addSub(Dep.target)
return val;
},
set: function (newval) {
if (newval === val) return
val = newval;
// 通知訂閱者
dep.notify()
}
})
}
// 需要實(shí)現(xiàn)一個(gè)消息訂閱器
function Dep() {
// 消息訂閱的讓容器是一個(gè)數(shù)組 數(shù)組的每一項(xiàng) 都是指代一個(gè)view和mode的中間者
this.subs = []
}
Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub)
},
notify: function () {
this.subs.forEach((sub) => {
// 在這里 需要配合watcher進(jìn)行更新
sub.update()
})
}
}
實(shí)現(xiàn)Compile
// 在這里增加dom編譯模板
function nodeToFragment(node, vm) {
var flag = document.createDocumentFragment();
var child;
while (child = node.firstChild) {
compile(child, vm);
// 將子節(jié)點(diǎn)劫持到文本節(jié)點(diǎn)中
flag.appendChild(child)
}
return flag
}
function compile(node, vm) {
var reg = /\{\{(.*)\}\}/;
// 跟據(jù)節(jié)點(diǎn)類型去判斷
if (node.nodeType === 1) {
var attr = node.attributes;
for (var i = 0; i < attr.length; i++) {
if (attr[i].nodeName === 'v-model') {
// 此時(shí) name為text
var name = attr[i].nodeValue;
// 增加數(shù)據(jù)的變化監(jiān)聽
node.addEventListener('input', (e) => {
vm[name] = e.target.value;
})
;
// 在這里 因?yàn)?我們的數(shù)據(jù)監(jiān)聽器 已經(jīng)封裝了vm[name] 觸發(fā)了 getter方法 完成了數(shù)據(jù)的初始化
node.value = vm[name];
node.removeAttribute('v-model')
}
}
new Watcher(vm, node, name, 'input')
}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
var name = RegExp.$1;
name = name.trim();
new Watcher(vm, node, name, 'text')
}
}
}
增加watcher 觀察函數(shù)
//訂閱者 搭建數(shù)據(jù)監(jiān)聽變化和變異模板的橋梁
function Watcher(vm, node, name, nodeType) {
Dep.target = this;
this.vm = vm;
this.node = node;
this.name = name;
this.nodeType = nodeType
this.update()
Dep.target = null;
}
Watcher.prototype = {
update: function () {
this.get()
if (this.nodeType === 'text') {
this.node.nodeValue = this.value
}
if (this.nodeType === 'input') {
this.node.value = this.value
}
},
get: function () {
this.value = this.vm[this.name];
}
}
入口函數(shù)
function Vue(options) {
// 將options里面的data屬性 放入數(shù)據(jù)監(jiān)聽器
this.data = options.data;
var data = this.data;
observe(data, this); // this指代vm
// 對指定id的dom 進(jìn)行頁面的渲染
this.$el = options.el;
var id = this.$el;
var Dom = nodeToFragment(document.getElementById(id), this);
// 編譯完成之后 將dom 添加到節(jié)點(diǎn)中
document.getElementById(id).appendChild(Dom)
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world',
name: '你好自脯,全世界'
}
});
vm.data.text = 'majunchang'
document.getElementsByClassName('btn')[0].onclick = function () {
vm.text = 'majunchang'
vm.name = '又疑瑤臺(tái)鏡贬蛙,飛在青云端'
}