老掉牙的文章了,不過為了加深上一篇對觀察者模式的理解,所以來自己實現一個簡單的vue雙向綁定。
目標
給一個input做個雙向綁定的功能
<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 => 數據 : 給input加個事件,變化的時候改變數據即可。
數據 => input :通過defineProperty設置get和set屬性來劫持數據,觸發視圖的更新。
開始
1、給對象所有的鍵值都用defineProperty設置get,set。
function observe(data){
if(typeof data !== "object"){ //如果不是對象
return;
}
Object.keys(data).forEach(key => { //遍歷對象鍵值
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;
}
})
}
這樣,一旦修改數據都能在set函數中監聽到。
2、實現MVVM構造函數。
在調用MVVM構造函數的時候,需要把data里面所有的鍵值都綁定上get和set。然后編譯模板。
class MVVM{
constructor(options){
this._options = options;
let data = this._data = options.data();
observe(data); //給數據的所有鍵值加上get set
let dom = document.getElementById(options.el);
new Compile(dom ,this); //編譯模板了
}
}
3、實現模板編譯
模版編譯就是遍歷節點,尋找具有v-model屬性的元素節點,以及{{}}這種格式的文本節點(簡化了,vue有很多指令都需要進行判斷)。
class Compile{
constructor(el,vm){
this._el = el;
this._compileElement(el);
}
_compileElement(el){ //遍歷節點
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){ //文本節點
let reg = /\{\{(.*)\}\}/;
let text = node.textContent;
if(reg.test(text)){
//如果這個元素是{{}}這種格式
}
}else if(node.nodeType == 1){ //元素節點
let nodeAttr = node.attributes;
Array.from(nodeAttr).forEach(attr => {
if(attr == "v-model"){
//如果這個元素有v-model屬性,那么得做點事情了
}
})
}
}
}
現在停下來思考,如果查到了某個元素的屬性有v-model,我們該做什么。
一個數據變化,所有它關聯的dom元素都需要更新。咦,這不就是觀察者模式做的事嗎!觀察者會被添加到目標中,目標一通知,所有的觀察者都會更新。所以,查到元素有v-model后(或者{{}}),就需要創建一個觀察者,添加到目標(數據)中。
4、實現觀察者
觀察者需要實現一個update方法。
class Watcher{
constructor(vm,exp,cb){ //初始化的時候把對象和鍵值傳進來
this._cb = cb;
this._vm = vm;
this._exp = exp; //保存鍵值
this._value = vm[exp]; //隱藏開關,這句代碼會發生什么?
}
update(){
let value = this._vm[_exp];
if(value != this._value){
this._value = value;
this._cb.call(this.vm,value);
}
}
}
vm[exp] 就會觸發get,這點很重要。
5、實現目標
觀察者是被添加到目標上的,所以得寫個目標的構造函數
class Dep{ //目標
constructor(){
this.subs = [];
}
add(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(sub => {
sub.update();
})
}
}
6、準備就緒,將一切串聯起來
(1)每次給key值添加get,set的時候都要創建一個Dep(目標)。
(2)每次模板編譯的時候,遇到v-model或者{{}}就創建一個觀察者添加到Dep。同時將data里的值賦給node。input還需要綁定一個input事件,輸入時改變對象里的值。
(3)準備一個全局變量,利用key值的get屬性添加watcher。
完成版:
watcher:
var uId = 0;
class Watcher{
constructor(vm,exp,cb){ //初始化的時候把對象和鍵值傳進來
this._cb = cb;
this._vm = vm;
this._exp = exp; //保存鍵值
this._uid = uId;
uId++; //每個觀察者配個ID,防止重復添加
Target = this;
this._value = vm[exp]; //看到沒,這里觸發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){ //文本節點
let reg = /\{\{(.*)\}\}/;
let text = node.textContent;
if(reg.test(text)){
//如果這個元素是{{}}這種格式
let key = RegExp.$1;
node.textContent = this._vm[key];
new Watcher(this._vm,key,val=>{
node.textContent = val;
})
}
}else if(node.nodeType == 1){ //元素節點
let nodeAttr = node.attributes;
Array.from(nodeAttr).forEach(attr => {
if(attr.nodeName == "v-model"){
node.value = this._vm[attr.nodeValue]; //初始化賦值
//如果這個元素有v-model屬性,那么得做點事情了
node.addEventListener('input',()=>{
this._vm[attr.nodeValue] = node.value;
})
new Watcher(this._vm,attr.nodeValue,val =>{
node.value = val;
})
}
})
}
}
一個很簡單雙向綁定,很多指令我都沒去解析,看看理解下觀察者模式就好了。附上Github