老掉牙的文章了碧注,不過為了加深上一篇對(duì)觀察者模式的理解蠢笋,所以來自己實(shí)現(xiàn)一個(gè)簡單的vue雙向綁定。
目標(biāo)
給一個(gè)input做個(gè)雙向綁定的功能
<div id="ele">
<input v-model="test"/>
{{test}}
</div>
<script src="./src/observer.js"></script>
<script src="./src/watcher.js"></script>
<script src="./src/compile.js"></script>
<script src="./src/mvvm.js"></script>
<script>
const vm = new MVVM({
el:'ele',
data(){
return {
test:''
}
}
})
</script>
思路
input => 數(shù)據(jù) : 給input加個(gè)事件填硕,變化的時(shí)候改變數(shù)據(jù)即可擂错。
數(shù)據(jù) => input :通過defineProperty設(shè)置get和set屬性來劫持?jǐn)?shù)據(jù),觸發(fā)視圖的更新粱锐。
開始
1疙挺、給對(duì)象所有的鍵值都用defineProperty設(shè)置get,set怜浅。
function observe(data){
if(typeof data !== "object"){ //如果不是對(duì)象
return;
}
Object.keys(data).forEach(key => { //遍歷對(duì)象鍵值
defineReactive(data,key,data[key]);
});
}
function defineReactive(data,key,val){
observe(val);
Object.defineProperty(data,key,{
enumerable: true,
configurable: true,
get(){
return val;
},
set(newval){
val = newval;
}
})
}
這樣铐然,一旦修改數(shù)據(jù)都能在set函數(shù)中監(jiān)聽到。
2、實(shí)現(xiàn)MVVM構(gòu)造函數(shù)锦爵。
在調(diào)用MVVM構(gòu)造函數(shù)的時(shí)候舱殿,需要把data里面所有的鍵值都綁定上get和set。然后編譯模板险掀。
class MVVM{
constructor(options){
this._options = options;
let data = this._data = options.data();
observe(data); //給數(shù)據(jù)的所有鍵值加上get set
let dom = document.getElementById(options.el);
new Compile(dom ,this); //編譯模板了
}
}
3沪袭、實(shí)現(xiàn)模板編譯
模版編譯就是遍歷節(jié)點(diǎn),尋找具有v-model屬性的元素節(jié)點(diǎn)樟氢,以及{{}}這種格式的文本節(jié)點(diǎn)(簡化了冈绊,vue有很多指令都需要進(jìn)行判斷)。
class Compile{
constructor(el,vm){
this._el = el;
this._compileElement(el);
}
_compileElement(el){ //遍歷節(jié)點(diǎn)
let childs = el.childNodes;
Array.from(childs).forEach(node => {
if (node.childNodes && node.childNodes.length) {
this._compileElement(node);
}else{
this._compile(node);
}
})
}
_compile(node){
if(node.nodeType == 3){ //文本節(jié)點(diǎn)
let reg = /\{\{(.*)\}\}/;
let text = node.textContent;
if(reg.test(text)){
//如果這個(gè)元素是{{}}這種格式
}
}else if(node.nodeType == 1){ //元素節(jié)點(diǎn)
let nodeAttr = node.attributes;
Array.from(nodeAttr).forEach(attr => {
if(attr == "v-model"){
//如果這個(gè)元素有v-model屬性埠啃,那么得做點(diǎn)事情了
}
})
}
}
}
現(xiàn)在停下來思考死宣,如果查到了某個(gè)元素的屬性有v-model,我們該做什么碴开。
一個(gè)數(shù)據(jù)變化毅该,所有它關(guān)聯(lián)的dom元素都需要更新。咦潦牛,這不就是觀察者模式做的事嗎眶掌!觀察者會(huì)被添加到目標(biāo)中,目標(biāo)一通知巴碗,所有的觀察者都會(huì)更新朴爬。所以,查到元素有v-model后(或者{{}})橡淆,就需要?jiǎng)?chuàng)建一個(gè)觀察者召噩,添加到目標(biāo)(數(shù)據(jù))中。
4逸爵、實(shí)現(xiàn)觀察者
觀察者需要實(shí)現(xiàn)一個(gè)update方法具滴。
class Watcher{
constructor(vm,exp,cb){ //初始化的時(shí)候把對(duì)象和鍵值傳進(jìn)來
this._cb = cb;
this._vm = vm;
this._exp = exp; //保存鍵值
this._value = vm[exp]; //隱藏開關(guān),這句代碼會(huì)發(fā)生什么痊银?
}
update(){
let value = this._vm[_exp];
if(value != this._value){
this._value = value;
this._cb.call(this.vm,value);
}
}
}
vm[exp] 就會(huì)觸發(fā)get抵蚊,這點(diǎn)很重要。
5溯革、實(shí)現(xiàn)目標(biāo)
觀察者是被添加到目標(biāo)上的,所以得寫個(gè)目標(biāo)的構(gòu)造函數(shù)
class Dep{ //目標(biāo)
constructor(){
this.subs = [];
}
add(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(sub => {
sub.update();
})
}
}
6谷醉、準(zhǔn)備就緒致稀,將一切串聯(lián)起來
(1)每次給key值添加get,set的時(shí)候都要?jiǎng)?chuàng)建一個(gè)Dep(目標(biāo))。
(2)每次模板編譯的時(shí)候俱尼,遇到v-model或者{{}}就創(chuàng)建一個(gè)觀察者添加到Dep抖单。同時(shí)將data里的值賦給node。input還需要綁定一個(gè)input事件,輸入時(shí)改變對(duì)象里的值矛绘。
(3)準(zhǔn)備一個(gè)全局變量耍休,利用key值的get屬性添加watcher。
完成版:
watcher:
var uId = 0;
class Watcher{
constructor(vm,exp,cb){ //初始化的時(shí)候把對(duì)象和鍵值傳進(jìn)來
this._cb = cb;
this._vm = vm;
this._exp = exp; //保存鍵值
this._uid = uId;
uId++; //每個(gè)觀察者配個(gè)ID货矮,防止重復(fù)添加
Target = this;
this._value = vm[exp]; //看到?jīng)]羊精,這里觸發(fā)getter了
Target = null; //用完就刪
}
update(){
let value = this._vm[this._exp];
if(value != this._value){
this._value = value;
this._cb.call(this.vm,value);
}
}
}
obeserve:
function defineReactive(data,key,val){
observe(val);
let dep = new Dep();
Object.defineProperty(data,key,{
enumerable: true,
configurable: true,
get(){
Target && dep.add(Target); //添加觀察者了
return val;
},
set(newval){
val = newval;
dep.notify(); //通知所有觀察者去更新
}
})
}
watcher:
_compile(node){
if(node.nodeType == 3){ //文本節(jié)點(diǎn)
let reg = /\{\{(.*)\}\}/;
let text = node.textContent;
if(reg.test(text)){
//如果這個(gè)元素是{{}}這種格式
let key = RegExp.$1;
node.textContent = this._vm[key];
new Watcher(this._vm,key,val=>{
node.textContent = val;
})
}
}else if(node.nodeType == 1){ //元素節(jié)點(diǎn)
let nodeAttr = node.attributes;
Array.from(nodeAttr).forEach(attr => {
if(attr.nodeName == "v-model"){
node.value = this._vm[attr.nodeValue]; //初始化賦值
//如果這個(gè)元素有v-model屬性,那么得做點(diǎn)事情了
node.addEventListener('input',()=>{
this._vm[attr.nodeValue] = node.value;
})
new Watcher(this._vm,attr.nodeValue,val =>{
node.value = val;
})
}
})
}
}
一個(gè)很簡單雙向綁定囚玫,很多指令我都沒去解析喧锦,看看理解下觀察者模式就好了。附上Github