1.原理
vue數(shù)據(jù)雙向綁 定是通過數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式,通過 **Object.defineProperty() ** 劫持各個屬性的 setter , getter , 在數(shù)據(jù)變動時發(fā)布消息給訂閱者沛简,觸發(fā)相應(yīng)的監(jiān)聽回調(diào)齐鲤。
2. 實(shí)現(xiàn)思路
要實(shí)現(xiàn)mvvm 的雙向綁定,必須實(shí)現(xiàn)以下幾點(diǎn):
- 1 實(shí)現(xiàn)一個數(shù)據(jù)監(jiān)聽器 Observer椒楣,能夠?qū)?shù)據(jù)對象的所有屬性進(jìn)行監(jiān)聽给郊,如果變動,可拿到最新值并通知訂閱者捧灰。
- 2 實(shí)現(xiàn)一個指令解析器 Compile丑罪,對每個元素節(jié)點(diǎn)的指令進(jìn)行掃描和解析,根據(jù)指令模板替換數(shù)據(jù)凤壁,以及綁定相應(yīng)的更新函數(shù)吩屹。
- 3 實(shí)現(xiàn)一個 Watcher,作為連接 Observer 和 Compile 的橋梁拧抖,能夠訂閱并收到每個屬性變動的通知煤搜,執(zhí)行指令綁定的相應(yīng)回調(diào)函數(shù),從而更新視圖唧席。
- 4 實(shí)現(xiàn)一個可以容納訂閱者的消息訂閱器 Dep 擦盾,訂閱器 Dep 主要負(fù)責(zé)收集訂閱器,然后在屬性變化的時候執(zhí)行對象的 訂閱者的更新函數(shù)淌哟。
- 5 實(shí)現(xiàn)MVVM 入口函數(shù)迹卢,整合以上三者。
上述流程如圖所示:
3. 實(shí)現(xiàn)Observer 數(shù)據(jù)監(jiān)聽器
Observer 是一個數(shù)據(jù)監(jiān)聽器徒仓,其核心方法就是 Object.defineProperty()腐碱。如果要對所有屬性都進(jìn)行監(jiān)聽的話,那么可以通過遞歸方法遍歷所有屬性值掉弛,并對其進(jìn)行 Object.defineProperty()處理症见,如下代碼:
- Observer.js
// 1.實(shí)現(xiàn)Observer觀察者
function Observer(data) {
this.data = data;
this.walk(data);
}
Observer.prototype = {
constructor: Observer,
walk: function (data) {
let self = this;
Object.keys(data).forEach(key => {
self.defineReacctive(self.data, key, data[key])
})
},
defineReacctive: function (data, key, val) {
observe(val);//監(jiān)聽子屬性
Object.defineProperty(data, key, {
enumerable: true,//可枚舉
configurable: false,//不能再define
get: function () {
return val;
},
set: function (newVal) {
if (val === newVal) return;
console.log('監(jiān)聽到的值發(fā)生變化了', val, '-->', newVal);
val = newVal;
}
})
}
}
function observe(data, vm) {
if (!data || typeof data !== 'object') {
return;
}
return new Observer(data)
}
4. 實(shí)現(xiàn)Dep 消息訂閱器
設(shè)計(jì)過程中,需要創(chuàng)建一個可以容納訂閱者的消息訂閱器 Dep殃饿,訂閱器 Dep主要負(fù)責(zé) 收集訂閱器Watcher谋作,然后在屬性變化的時候執(zhí)行對象訂閱者的更新函數(shù)。
- Dep.js
// 實(shí)現(xiàn)消息訂閱器
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function (sub) {
this.subs.push(sub)
},
notify: function () {
this.subs.forEach(function (sub) {
sub.update();
})
}
}
Dep.target = null;
- 結(jié)合Dep 和Observer
將訂閱器添加的訂閱者 設(shè)計(jì)在Observer的 getter里面乎芳,這是為了讓 Watcher 初始化進(jìn)行觸發(fā)遵蚜,在 Observer的setter函數(shù)里面帖池,如果數(shù)據(jù)變化,就會通知所有訂閱者吭净,訂閱者們就會執(zhí)行對象的更新函數(shù)睡汹。一個比較完整的Observer 已經(jīng)實(shí)現(xiàn)了。
Observer.prototype = {
constructor: Observer,
walk: function (data) {
let self = this;
Object.keys(data).forEach(key => {
self.defineReacctive(self.data, key, data[key])
})
},
defineReacctive: function (data, key, val) {
let dep = new Dep();
observe(val);//監(jiān)聽子屬性
Object.defineProperty(data, key, {
enumerable: true,//可枚舉
configurable: false,//不能再define
get: function () {
//將訂閱者Wather賦予給 Dep.target攒钳,每個訂閱者都是不一樣的
Dep.target && dep.addSub(Dep.target);//在這里添加一個訂閱器
return val;
},
set: function (newVal) {
if (val === newVal) return;
console.log('監(jiān)聽到的值發(fā)生變化了', val, '-->', newVal);
val = newVal;
dep.notify();//通知所有訂閱者
}
})
}
}
5. 實(shí)現(xiàn)Compile 編譯指令
Compile主要做的事情是解析模板指令帮孔,將模板中的變量替換成數(shù)據(jù),然后初始化渲染頁面不撑,并將每個指定對應(yīng)的節(jié)點(diǎn)綁定更新函數(shù)文兢,添加監(jiān)聽數(shù)據(jù)的Watcher訂閱者,一旦數(shù)據(jù)有變動焕檬,收到通知姆坚,更新視圖,如圖所示:
- Compile.js 代碼如下:
// 編譯指令
function Compile(el, vm) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.nodeFragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
constructor: Compile,
nodeFragment: function (el) {
let fragment = document.createDocumentFragment();
let child;
//將原生節(jié)點(diǎn)拷貝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
init: function () {
this.compileElement(this.$fragment);
},
compileElement: function (el) {
let childNodes = el.childNodes;
let self = this;
[].slice.call(childNodes).forEach(function (node) {
let text = node.textContent;
let reg = /\{\{(.*)\}\}/; //匹配 {{}}
if (self.isElementNode(node)) {//元素節(jié)點(diǎn)
self.compile(node)
} else if (self.isTextNode(node) && reg.test(text)) { //文本節(jié)點(diǎn)且 {{}}
self.compileText(node, RegExp.$1.trim());
}
if (node.childNodes && node.childNodes.length) {//擁有孩子節(jié)點(diǎn)实愚,繼續(xù)遞歸
self.compileElement(node)
}
})
},
compile: function (node) {
let nodeAttrs = node.attributes;
let self = this;
[].slice.call(nodeAttrs).forEach(function (attr) {
let attrName = attr.name;
if (self.isDirective(attrName)) {
let exp = attr.value;
let dir = attrName.substring(2);
if (self.isEventDirective(dir)) {//事件指令
compileUtil.eventHandler(node, self.$vm, exp, dir);
} else {//普通指令
compileUtil[dir] && compileUtil[dir](node, self.$vm, exp)
}
node.removeAttribute(attrName);
}
})
},
compileText: function (node, exp) {
compileUtil.text(node, this.$vm, exp);
},
isDirective: function (attr) {//普通指令
return attr.indexOf('v-') == 0;
},
isEventDirective: function (dir) {//事件指令
return dir.indexOf('on') === 0;
},
isElementNode: function (node) {//元素節(jié)點(diǎn)
return node.nodeType == 1;
},
isTextNode: function (node) {//文本節(jié)點(diǎn)
return node.nodeType == 3;
},
}
//指定處理集合
let compileUtil = {
text: function (node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
html: function (node, vm, exp) {
this.bind(node, vm, exp, 'html');
},
model: function (node, vm, exp) {
this.bind(node, vm, exp, 'model');
let self = this;
let val = this._getVMVal(vm, exp);
node.addEventListener('input', function (e) {
let newVal = e.target.value;
if (val === newVal) return;
console.log(newVal);
self._setVMVal(vm, exp, newVal);
val = newVal
})
},
class: function (node, vm, exp) {
this.bind(node, vm, exp, 'class');
},
bind: function (node, vm, exp, dir) {
let updateFn = updater[dir + 'Updater'];
updateFn && updateFn(node, this._getVMVal(vm, exp));
new Watcher(vm, exp, function (value, oldValue) {
updateFn && updateFn(node, value, oldValue)
})
},
//事件處理
eventHandler: function (node, vm, exp, dir) {
let eventType = dir.split(':')[1];
let fn = vm.$options.methods && vm.$options.methods[exp];
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
},
_getVMVal:function(vm,exp) {
let val = vm;
exp = exp.split('.');
exp.forEach(function(k) {
val = val._data[k]
})
return val;
},
_setVMVal: function(vm,exp,value) {
let val = vm;
exp = exp.split('.');
exp.forEach(function(k,i) {
//非最后一個key兼呵,更新val的值
if(i<exp.length -1) {
val = val._data[k];
} else {
val._data[k] = value
}
})
},
}
let updater = {
textUpdater: function(node,value) {
node.textContent = typeof value == 'undefined'? '' : value
},
htmlUpdater: function(node,value) {
node.innerHtml = typeof value == 'undefined'? '' : value
},
classUpdater: function(node,value,oldValue) {
let className = node.className;
className = className.replace(oldValue,'').replace(/\s$/,'');
let space = className && String(value)? ' ': '';
node.className = className + space +value;
},
modelUpdater: function(node,value) {
node.value = typeof value == 'undefined'? '' : value
},
}
6. 實(shí)現(xiàn)Watcher 訂閱者
Watcher 訂閱者作為 Observer 和 Compile 之間通信的橋梁,主要做的事情是:
1.在自身實(shí)例化時往屬性訂閱器Dep 里面添加自己:在Dep.target上緩存訂閱器腊敲,通過觸發(fā) getter方法击喂,把自己添加到getter方法里面,添加成功后去掉Dep.target碰辅。
2.自身必須有一個update方法懂昂。
3.待屬性變動dep.notice()通知時,能調(diào)用自身的update方法没宾,并觸發(fā)Compil中綁定的回調(diào)凌彬。
function MVVM(options) {
this.$options = options || {};
let data = this._data = this.$options.data;
let self = this;
observe(data,self);
this.$compile = new Compile(options.el || document.body, self)
}
從上面代碼可看出監(jiān)聽的數(shù)據(jù)對象是options.data,每次需要更新視圖循衰,則必須通過let vm = new MVVM({data:{name: 'zs'}}); vm._data.name = 'li';铲敛,這樣的方式來改變數(shù)據(jù)。
顯然不符合我們一開始的期望会钝,我們所期望的調(diào)用方式應(yīng)該是這樣的:
let vm = new MVVM({data:{name: 'zs'}}); vm.name = 'li';
所以這里我們需要給MVVM實(shí)例添加一個屬性代理的方法伐蒋,使訪問vm的屬性代理可訪問 vm._data的屬性,改造后的代碼如下:
function MVVM(options) {
this.$options = options || {};
console.log(this);
let data = this._data = this.$options.data;
let self = this;
//數(shù)據(jù)代理
//實(shí)現(xiàn) vm.xxx -> vm._data.xxx
Object.keys(data).forEach(function (key) {
self._proxyData(key)
});
this._initComputed();
observe(data);
this.$compile = new Compile(options.el || document.body, this)
}
MVVM.prototype = {
constructor: MVVM,
$watch: function (key, options, cb) {
new Watcher(this, key, cb);
},
_proxyData: function (key, setter, getter) {
let self = this;
setter = setter ||
Object.defineProperty(self, key, {
enumerable: true,
configurable: false,
get: function proxyGetter() {
return self._data[key]
},
set: function proxySetter(newVal) {
self._data[key] = newVal;
}
})
},
_initComputed: function () {//添加計(jì)算屬性
let self = this;
let computed = this.$options.computed;
if (typeof computed === 'object') {
Object.keys(computed).forEach(function (key) {
Object.defineProperty(self, key, {
get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
set: function() {}
})
})
}
},
}
這里主要利用了 Object.defineProperty() 這個方法來劫持 vm實(shí)例對象的屬性的讀寫權(quán)顽素,使讀寫vm實(shí)例的屬性 轉(zhuǎn)成了 vm._data的屬性值咽弦。
7. html
<body>
<div id="app">
<h1 id="name">{{name}}</h1>
<input type="text" v-model="msg">
<p>{{msg}}</p>
<button v-on:click="clickHandle">change</button>
</div>
</body>
<script src="./Dep.js"></script>
<script src="./Observer.js"></script>
<script src="./Watcher.js"></script>
<script src="./Compile.js"></script>
<script src="./MVVM.js"></script>
<script>
let vm = new MVVM({
el: '#app',
data: {
msg: 'hello'
},
methods: {
clickHandle: function() {
this.msg = 'hi';
console.log(this);
}
},
})
</script>
效果圖:
最后,源碼都放在gitee上面了胁出,請點(diǎn)擊源碼