Vue作為當(dāng)前國內(nèi)使用廣泛的前端MVVM框架汛蝙,其中的雙向數(shù)據(jù)綁定大大減少了前端代碼維護數(shù)值變化的難度缭受,顯得高效而神秘,那么今天就來解開其神秘面紗胎围!動手實現(xiàn)簡單的雙向數(shù)據(jù)綁定,本項目源碼請猛戳這里。
1. 實現(xiàn)簡單的vue雙向數(shù)據(jù)綁定
animate.gif
1.1 基本原理
-
首先看原理圖如下
data-binding.png 其中主要部分及其功能(首字母大寫為課實例化的類,小寫為函數(shù))
-
MVVM
即Vue實例,主要包括data
和template
兩部分(其他暫不考慮) -
data
對象數(shù)據(jù)模型Model,template
對應(yīng)視圖View -
observe
為數(shù)據(jù)劫持模塊,主要實現(xiàn)數(shù)據(jù)的getter
和setter
,并為屬性綁定訂閱者,在屬性值發(fā)生變化是通知訂閱者 -
Watcher
為訂閱者,通過depend
將自己添加至訂閱者管理模塊Dep
實例中,主要實現(xiàn)屬性值變化時調(diào)用回調(diào)函數(shù)更新視圖 -
Dep
為訂閱者管理模塊,是建立observe
與Watcher
的橋梁.通過notify
通知所有訂閱者數(shù)據(jù)發(fā)生變化 -
compile
為模板解析模塊,解析v-
指令以及模板字面量等,并為相應(yīng)屬性添加訂閱者Watcher
和回調(diào)函數(shù)
1.2 基本步驟如下
-
Vue
包括data
和template
兩部分,分別對應(yīng)Model與View - 通過
observe
為data
的每一個屬性和其子屬性添加getter
和setter
- 通過
Dep
實例來管理訂閱者,其中data的每一個屬性擁有一個Dep
實例(data
與Dep
實例為一對多的關(guān)系) - 通過
compile
解析模板template
,分析出那些是data
的屬性并創(chuàng)建Watcher
實例,添加至屬性對應(yīng)Dep
實例中 - 當(dāng)
data
屬性值發(fā)生變化時,即調(diào)用屬性的getter
時會觸發(fā)Dep
實例的notify
方法,接著出發(fā)Watcher
實例的update
方法,刷新視圖(Dep
實例與Watcher
實例同樣是一對多的關(guān)系`) - 當(dāng)視圖數(shù)據(jù)發(fā)生變化時,改變
data
對應(yīng)屬性值,繼續(xù)步驟5,實現(xiàn)視圖刷新
2. 用法
<div id='wu-app'>
<input type="text" v-model='text'>
<br>
<label for="">Input value:{{text}}</label>
<br>
<input type="button" v-on:click='btnClick' value='Click Me'>
</div>
<script>
window.onload = function () {
var app = new W.Wu({
el: '#wu-app',
data: {
text: 'Hello World!'
},
methods: {
btnClick(e) {
this.text = 'You clicked the button!'
}
}
})
}
</script>
3. 代碼分析
3.1 主模塊:入口
export function Wu(options) {
this.$options = options;
this.$data = options.data || {};
this.$methods = options.methods || {};
this.$watched = options.watched;
// 將data和methods以及computed中的屬性方法代理在自己身上
proxy(this, this.$data);
proxy(this, this.$methods);
// 初始化數(shù)據(jù)劫持
observe(this.$data);
// 模板解析
compile(options.el || document.body, this);
}
3.2 數(shù)據(jù)劫持
function observe(data) {
if (!data || typeof data !== "object") return;
Object.keys(data).forEach(function(key) {
let val = data[key],
dep = new Dep();
//觀察子屬性
observe(val);
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function() {
// console.log(`i get ${key}:${val}`);
//添加訂閱者
Dep.target && dep.addSub(Dep.target);
return val;
},
set: function(newVal) {
// console.log(`i set ${key}:${val}-->${newVal}`);
val = newVal;
//通知所有訂閱者數(shù)據(jù)變更
dep.notify();
}
});
});
}
3.3 模板解析
let compileUtil = {
elementNodeType: 1,
textNodeType: 3,
isDirective(attr) {
let reg = /v-|:|@/;
return reg.test(attr);
return (
attr.indexof("v-") == 0 ||
attr.indexof(":") == 0 ||
attr.indexof("@") == 0
);
},
// 將原生節(jié)點拷貝到fragment
node2Fragment(node) {
let frag = document.createDocumentFragment();
[].slice.call(node.childNodes).forEach(child => {
frag.appendChild(child);
});
return frag;
},
// 更新回調(diào)函數(shù)
update(node, dir, newVal, oldVal) {
switch (dir) {
case "model":
node.value = typeof newVal === "undefined" ? "" : newVal;
break;
case "class":
break;
case "html":
break;
case "text":
node.textContent = typeof newVal === "undefined" ? "" : newVal;
break;
}
},
// 獲取屬性值(當(dāng)表達式為不只是key,而是一各需要運算的語句是如何處理?)
getVMVal(vm, exp) {
let src = vm;
exp.split(".").forEach(k => {
src = src[k];
});
return src;
},
// 設(shè)置屬性值
setVMVal(vm, exp, val) {
let src = vm,
keys = exp.split(".");
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
if (i < keys.length - 1) {
src = src[k];
} else {
src[k] = val;
}
}
return src;
},
// 解析節(jié)點
compileNode(node, vm) {
[].slice.call(node.childNodes).forEach(child => {
switch (child.nodeType) {
// 節(jié)點
case compileUtil.elementNodeType:
let attrs = child.attributes;
[].slice.call(attrs).forEach(attr => {
// 判斷是否為內(nèi)部指令
let attrName = attr.name;
if (this.isDirective(attrName)) {
let dir = attrName.split(/v-|:|@/).join(""),
exp = attr.value;
// 事件
if (dir.substring(0, 2) === "on") {
child.addEventListener(
dir.substring(2),
//注意 this 指向
this.getVMVal(vm, exp).bind(vm)
);
} else {
// 其他指令model bind text等
this.update(child, dir, this.getVMVal(vm, exp));
// 訂閱者
new Watcher(vm, exp, (newVal, oldVal) => {
// 更新回調(diào)
this.update(child, dir, newVal, oldVal);
});
// model
if (dir === "model") {
let oldVal = this.getVMVal(vm, exp);
// 注冊對于表單輸入項的input事件
child.addEventListener("input", e => {
var newVal = e.target.value;
if (newVal !== oldVal) {
// 更改數(shù)值
this.setVMVal(vm, exp, newVal);
}
});
}
}
// 移除指令
// child.removeAttributes(attr);
}
});
if (child.childNodes && child.childNodes.length > 0) {
this.compileNode(child, vm);
}
break;
// 文本
case compileUtil.textNodeType:
var text = child.textContent,
reg = /\{\{(.*)\}\}/;
if (reg.test(text)) {
var exp = reg.exec(text)[1];
this.update(child, "text", this.getVMVal(vm, exp));
new Watcher(vm, exp, (newVal, oldVal) => {
this.update(child, "text", newVal, oldVal);
});
}
break;
}
});
}
};
// 模板解析
function compile(template, vm) {
let el =
template.nodeType == compileUtil.elementNodeType
? template
: document.querySelector(template); // 取出id為el的第一個節(jié)點作為容器
if (el) {
// 將原始節(jié)點存為fragment進行操作 減少頁面渲染次數(shù) 提升效率
let fragment = compileUtil.node2Fragment(el);
compileUtil.compileNode(fragment, vm);
// 處理完后 重新添加至容器
el.appendChild(fragment);
}
}
3.4 訂閱者
let _uid = 0;
export function Watcher(vm, exp, cb) {
// 唯一標(biāo)識
this.id = _uid++;
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.depend();
}
Watcher.prototype = {
constructor: Watcher,
depend() {
Dep.target = this;
//通過觸發(fā)getter,添加自己為訂閱者
var value = this.vm[this.exp];
Dep.target = null;
return value;
},
// 更新
update() {
let oldVal = this.value,
newVal = this.vm[this.exp];
if (oldVal !== newVal) {
this.value = newVal;
this.cb(newVal, oldVal);
}
}
};
3.5 訂閱者管理
export function Dep() {
// 鍵值對
this.subs = new Map();
}
Dep.prototype = {
constructor: Dep,
// 添加訂閱者
addSub(watcher) {
// 通過訂閱者id作為唯一標(biāo)識 避免重復(fù)訂閱
this.subs.set(watcher.id, watcher);
},
// 通知訂閱者
notify() {
this.subs.forEach(watcher => {
watcher.update();
});
}
};
Dep.target = null;
參考
如果您感覺有所幫助白魂,或者有問題需要交流汽纤,歡迎留言評論,非常感謝福荸!
前端菜鳥蕴坪,還請多多關(guān)照!