Reactive Programming是一種編程形式,在很多場(chǎng)景都會(huì)見到晨雳,最近正在學(xué)習(xí)的RxJS是一個(gè)例子行瑞,當(dāng)然Vue同樣是一種Reactive Programming,就是當(dāng)變量發(fā)生改變的時(shí)候餐禁,相關(guān)的變量和視圖也會(huì)跟著改變血久,而我們開發(fā)者不需要自己去寫代碼來(lái)實(shí)現(xiàn)這個(gè)過程,我們只需要關(guān)心變量改變之后應(yīng)該進(jìn)行什么操作帮非,更加關(guān)注于業(yè)務(wù)流程氧吐。
Vue的雙向數(shù)據(jù)綁定是基于ES5的Object.defineProperty()
的getter
和setter
,每當(dāng)數(shù)據(jù)發(fā)生變化末盔,就會(huì)執(zhí)行getter/setter
筑舅,結(jié)合發(fā)布者/訂閱者的模式,通知訂閱者這些變化陨舱,進(jìn)而執(zhí)行相應(yīng)的回調(diào)函數(shù)翠拣。
今天我們來(lái)分析一下vue雙向數(shù)據(jù)綁定的原理,同時(shí)我們自己用js來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的雙向數(shù)據(jù)綁定游盲,首先看一下原理圖:
MVVM
就是我們要實(shí)現(xiàn)的vue
實(shí)例误墓,簡(jiǎn)單講述一下流程:
首先通過一個(gè)
Observer
(監(jiān)聽器或者劫持器)去劫持data
對(duì)象中的所有屬性蛮粮,方法就是使用Object.defineProperty()
中的getter/setter
,在屬性set
的時(shí)候通知Dependency
(訂閱器/容器)發(fā)布變化谜慌;實(shí)現(xiàn)一個(gè)
Watcher
(訂閱者)然想,這個(gè)Watcher
就是說我收到數(shù)據(jù)變化的通知后,應(yīng)該去執(zhí)行什么操作(重新填充列表畦娄,填充值等等又沾,即更新視圖)弊仪,一個(gè)data.message數(shù)據(jù)可能對(duì)應(yīng)多個(gè)使用場(chǎng)景熙卡,比如v-model="message"
、v-text="message"
励饵、{{message}}
等等驳癌,所以Watcher
不止一個(gè);上面說到
Watcher
不止一個(gè)役听,所以我們可以實(shí)現(xiàn)一個(gè)容器Dependency
颓鲜,里面存放data.message對(duì)應(yīng)的所有Watcher
,這樣當(dāng)Observer
的Setter
改變時(shí)典予,調(diào)用Dependency
的notify
方法甜滨,逐條去通知所有的Watcher
;實(shí)現(xiàn)一個(gè)編譯器
Complier
瘤袖,編譯器的作用是掃描和解析每一個(gè)節(jié)點(diǎn)node
衣摩,先將節(jié)點(diǎn)轉(zhuǎn)換為fragment
(性能優(yōu)化,一次性append所有節(jié)點(diǎn)至目標(biāo)element內(nèi))捂敌,再根據(jù)不同的節(jié)點(diǎn)類型nodeType
艾扮,針對(duì)v-model
、v-text
占婉、{{message}}
做不同的處理泡嘴,完成第一次的數(shù)據(jù)message
填充(即初始化視圖);同時(shí)編譯器還擔(dān)當(dāng)著初始化Watcher
的任務(wù)逆济,將Watcher
添加到Dependency
中去酌予;
有了以上的思路帮匾,接下來(lái)就是編寫代碼時(shí)間昏滴,使用了ES6的class
,首先我們來(lái)實(shí)現(xiàn)Observer
:
class Observer {
constructor(data) {
this.observer(data);
}
observer(data) {
if (!data || typeof data !== 'object') {
return false;
} else {
Object.keys(data).forEach((key) => {
// 劫持data對(duì)象中的每一條數(shù)據(jù)
this.defineReactive(data, key, data[key]);
})
}
}
defineReactive(obj, key, value) {
let dep = new Dependency();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
if (Dependency.target) {
dep.addSub(Dependency.target); // 添加訂閱者watcher,應(yīng)該是整個(gè)實(shí)例Watcher
}
return value;
},
set(newValue) {
// 值未變化return回去
if (newValue === value) { return false; }
value = newValue;
// 數(shù)據(jù)變化诫欠,通知dep里所有的watcher
dep.notify();
}
})
}
}
// 第一次get值的時(shí)候不會(huì)添加Watcher到Dependency,實(shí)例化(調(diào)用)watcher時(shí)再添加
Dependency.target = null;
接下來(lái)實(shí)現(xiàn)Watcher
:
class Watcher {
constructor(vm, expr, callback) {
this.vm = vm;
this.expr = expr; // data中的key值
this.callback = callback; // 值變化的時(shí)候執(zhí)行什么回調(diào)
this.value = this.get(); // 實(shí)例化watcher的時(shí)候?qū)⒆约禾砑拥紻ependency
}
get() {
Dependency.target = this; // 緩存自己,就是這個(gè)Watcher實(shí)例
let value = this.vm.$data[this.expr]; // 觸發(fā)執(zhí)行Observer中的get函數(shù)升薯,將自己添加到Dep
Dependency.target = null; // 釋放自己
return value;
}
update() {
// 值更新后莱褒,Observer的setter就會(huì)觸發(fā),就會(huì)執(zhí)行dep.notify()涎劈,即通過Dep容器通知watcher根據(jù)callback去更新視圖
let newValue = this.vm.$data[this.expr];
let oldValue = this.value;
if (newValue !== oldValue) {
// 新老值不一致广凸,執(zhí)行回調(diào)
this.callback(newValue);
}
}
}
然后我們需要一個(gè)容器Dependency
去儲(chǔ)存data.message對(duì)應(yīng)的所有Watcher
:
class Dependency {
constructor() {
this.subs = []; // 容器數(shù)據(jù)阅茶,放watcher用
}
addSub(watch) {
this.subs.push(watch); // 將watcher添加到subs內(nèi)
}
notify() {
// 通知subs內(nèi)的所有watcher更新回調(diào)
this.subs.forEach((watch) => {
watch.update();
})
}
}
下面是編譯器Complier
,編譯器涉及的東西比較雜谅海,判斷的情況比較多脸哀,所以這里只考慮到了v-model
、v-text
扭吁、{{message}}
這3種情況的實(shí)現(xiàn):
class Complier {
constructor(el, vm) {
this.vm = vm;
this.el = document.querySelector(el);
if (this.el) {
// 使用fragment儲(chǔ)存元素撞蜂,這時(shí)候#app內(nèi)就沒有節(jié)點(diǎn)了,因?yàn)橐呀?jīng)被frag刪除完了
let fragment = this.nodeToFragment(this.el);
this.complie(fragment); // 編譯fragment
this.el.appendChild(fragment); // 將fragment放回#app內(nèi)
}
}
complie(node) {
// 使用Array.from將類數(shù)組node.childNodes轉(zhuǎn)換為真正的數(shù)組
let nodeList = Array.from(node.childNodes);
nodeList.forEach((item) => {
//根據(jù)nodeType判讀節(jié)點(diǎn)類型侥袜,執(zhí)行不同的編譯
switch (item.nodeType) {
case 1:
this.elementComplier(item);break;
case 3:
this.textComplier(item);break;
}
})
}
elementComplier(node) {
// 元素節(jié)點(diǎn)編譯器蝌诡,處理屬性v-model,v-text等
let attrs = Array.from(node.attributes);
attrs.forEach((attr) => {
if (attr.name.indexOf('v-') > -1) {
let type = attr.name.split('-')[1]; // 取到'model',即指令的類型
complierUnits[type] && complierUnits[type](node, this.vm, attr.value);
}
})
}
textComplier(node) {
// 文本節(jié)點(diǎn)編譯器{{message}},跟v-text共用一個(gè)編譯方法
if ((/\{\{(.+)\}\}/).test(node.textContent)) {
complierUnits.text(node, this.vm, RegExp.$1);
}
}
nodeToFragment(node) {
// 將node轉(zhuǎn)換為fragment
let frag = document.createDocumentFragment();
let child;
while (child = node.firstChild) {
// fragment調(diào)用appendChild方法會(huì)刪除node.firstChild節(jié)點(diǎn)
frag.appendChild(child);
}
return frag;
}
}
// 編譯器工具箱
const complierUnits = {
model (node, vm, expr) {
let updateFn = this.updater.modelUpdater;
// 初始化的時(shí)候取一次值填充枫吧,渲染頁(yè)面數(shù)據(jù)
updateFn && updateFn(node, vm.$data[expr]);
// 實(shí)例化watcher(調(diào)用watcher),將watcher添加到Dep中浦旱,同時(shí)定義好回調(diào)函數(shù)(數(shù)據(jù)變化后干什么)
new Watcher(vm, expr, function(newValue){
updateFn && updateFn(node, newValue);
});
// 監(jiān)聽input值的變化,從視圖到data
node.addEventListener('input', (event) => {
vm.$data[expr] = event.target.value;
})
},
text (node, vm, expr) {
let updateFn = this.updater.textUpdater;
updateFn && updateFn(node, vm.$data[expr]);
new Watcher(vm, expr, function(newValue){
updateFn && updateFn(node, newValue);
});
},
updater: {
modelUpdater(node, value) {
node.value = value;
},
textUpdater(node, value) {
node.textContent = value;
}
}
};
還有入口main.js
:
class MVVM {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
// 當(dāng)視圖存在時(shí)
if (this.$el) {
// 將屬性添加進(jìn)Observer九杂,劫持?jǐn)?shù)據(jù)
new Observer(this.$data);
// 編譯頁(yè)面
new Complier(this.$el, this);
}
}
}
最后就是html調(diào)用了:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>vue原理簡(jiǎn)單實(shí)現(xiàn)</title>
<script src="js/dependency.js"></script>
<script src="js/observer.js"></script>
<script src="js/watcher.js"></script>
<script src="js/complier.js"></script>
<script src="js/main.js"></script>
</head>
<body>
<div id="app">
<span v-text="message"></span>
<input type="text" v-model="message" />
{{message}}
</div>
<script>
let vm = new MVVM({
el: '#app',
data: {
message: 'hello Vue!'
}
})
</script>
</body>
</html>
總結(jié):實(shí)例化MVVM
時(shí)颁湖,先使用Object.defineProperty
劫持每一個(gè)data數(shù)據(jù),為每一個(gè)屬性實(shí)例化一個(gè)Dependency例隆;
在編譯頁(yè)面的時(shí)候?yàn)槊恳粋€(gè)需要更新message的地方添加一個(gè)Watcher
甥捺,即v-model="message"
、v-text="message"
和{{message}}
镀层,有一個(gè)算一個(gè)镰禾,將這些Watcher
添加到Dependency
中進(jìn)行統(tǒng)一管理;在編譯的時(shí)候我們還要為input添加一個(gè)事件監(jiān)聽addEventListener
鹿响,這樣input的輸入值變化時(shí)羡微,觸發(fā)setter
,在setter
內(nèi)調(diào)用Dep
的notify()
方法惶我,循環(huán)調(diào)用每一個(gè)Watcher
的update
更新我們的視圖(執(zhí)行回調(diào)函數(shù))妈倔。
以上代碼都放到了我的github倉(cāng)庫(kù)vue-principle,歡迎查閱绸贡。推銷一下我的博客