摘要: 時隔半年,重新寫一遍有關數據雙向綁定的文章,模擬當今流行的vue框架實現一個屬于自己的mvvm庫。
前言
本文所有代碼都已經push到本人github個人倉庫overwrite
->my-mvvm
我們知道的,常見的數據綁定的實現方法
1、數據劫持(vue):通過Object.defineProperty()去劫持數據每個屬性對應的getter和setter2、臟值檢測(angular):通過特定事件比如input,change,xhr請求等進行臟值檢測。3、發布-訂閱模式(backbone):通過發布消息,訂閱消息進行數據和視圖的綁定監聽。具體代碼實現可以參考我github個人倉庫overwrite**->my-observer
一言不合先上代碼和效果圖吧
code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>example</title>
<script src="./mvvm.js" charset="utf-8"></script>
</head>
<body>
<div id="mvvm">
<h2>{{b}}</h2>
<input type="text" x-model="a">
<input type="text" name="" value="" x-model="a">
<p x-html="a">{{ a }}</p>
<button type="button" name="button" x-on:click="testToggle">change b</button>
</div>
</body>
<script>
var vm = new MVVM({
el: '#mvvm',
data: {
a: 'test model',
b: 'hello MVVM',
flag: true
},
methods: {
testToggle: function () {
this.flag = !this.flag;
this.b = this.flag ? 'hello MVVM' : 'test success'
}
}
});
</script>
</html>
效果圖
一、總體大綱
要實現一個我們自己的mvvm庫,我們首先需要做的事情不是寫代碼,而是整理一下思路,捋清楚之后再動手絕對會讓你事半功倍。先上流程圖,我們對著流程圖來捋思路
如上圖所示,我們可以看到,整體實現分為四步
1、實現一個Observer,對數據進行劫持,通知數據的變化
2、實現一個Compile,對指令進行解析,初始化視圖,并且訂閱數據的變更,綁定好更新函數
3、實現一個Watcher,將其作為以上兩者的一個中介點,在接收數據變更的同時,讓Dep添加當前Watcher,并及時通知視圖進行update
4、實現MVVM,整合以上三者,作為一個入口函數
二、動手時間
思路捋清楚了,接下來要做的事就是開始動手。
能動手的我決不動口
1、實現Observer
這里我們需要做的事情就是實現數據劫持,并將數據變更給傳遞下去。那么這里將會用到的方法就是Object.defineProperty()來做這么一件事。先不管三七二十一,咱先用用Object.defineProperty()試試手感。
function observe (data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(key => {
observeProperty(data, key, data[key])
})
}
function observeProperty (obj, key, val) {
observe(val);
Object.defineProperty(obj, key, {
enumerable: true, // 可枚舉
configurable: true, // 可重新定義
get: function () {
return val;
},
set: function (newVal) {
if (val === newVal || (newVal !== newVal && val !== val)) {
return;
}
console.log('數據更新啦 ', val, '=>', newVal);
val = newVal;
}
});
}
調用
var data = {
a: 'hello'
}
observe(data);
效果如下
看完是不是發現JavaScript提供給我們的Object.defineProperty()方法功能巨強大巨好用呢。
其實到這,我們已經算是完成了數據劫持,完整的Observer則需要將數據的變更傳遞給Dep實例,然后接下來的事情就丟給Dep去通知下面完成接下來的事情了,完整代碼如下所示
/**
* @class 發布類 Observer that are attached to each observed
* @param {[type]} value [vm參數]
*/
function observe(value, asRootData) {
if (!value || typeof value !== 'object') {
return;
}
return new Observer(value);
}
function Observer(value) {
this.value = value;
this.walk(value);
}
Observer.prototype = {
walk: function (obj) {
let self = this;
Object.keys(obj).forEach(key => {
self.observeProperty(obj, key, obj[key]);
});
},
observeProperty: function (obj, key, val) {
let dep = new Dep();
let childOb = observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
if (Dep.target) {
dep.depend();
}
if (childOb) {
childOb.dep.depend();
}
return val;
},
set: function(newVal) {
if (val === newVal || (newVal !== newVal && val !== val)) {
return;
}
val = newVal;
// 監聽子屬性
childOb = observe(newVal);
// 通知數據變更
dep.notify();
}
})
}
}
/**
* @class 依賴類 Dep
*/
let uid = 0;
function Dep() {
// dep id
this.id = uid++;
// array 存儲Watcher
this.subs = [];
}
Dep.target = null;
Dep.prototype = {
/**
* [添加訂閱者]
* @param {[Watcher]} sub [訂閱者]
*/
addSub: function (sub) {
this.subs.push(sub);
},
/**
* [移除訂閱者]
* @param {[Watcher]} sub [訂閱者]
*/
removeSub: function (sub) {
let index = this.subs.indexOf(sub);
if (index !== -1) {
this.subs.splice(index ,1);
}
},
// 通知數據變更
notify: function () {
this.subs.forEach(sub => {
// 執行sub的update更新函數
sub.update();
});
},
// add Watcher
depend: function () {
Dep.target.addDep(this);
}
}
// 結合Watcher
/**
* Watcher.prototype = {
* get: function () {
* Dep.target = this;
* let value = this.getter.call(this.vm, this.vm);
* Dep.target = null;
* return value;
* },
* addDep: function (dep) {
* dep.addSub(this);
* }
* }
*/
至此,我們已經實現了數據的劫持以及notify數據變化的功能了。
2、實現Compile
按理說我們應該緊接著實現Watcher,畢竟從上面代碼看來,Observer和Watcher關聯好多啊,但是,我們在捋思路的時候也應該知道了,Watcher和Compile也是有一腿的哦。所以咱先把Compile也給實現了,這樣才能更好的讓他們3P。
我不是老司機,我只是一個純潔的開電動車的孩子??
廢話不多說,干實事。
Compile需要做的事情也很簡單
a、解析指令,將指令模板中的變量替換成數據,對視圖進行初始化操作
b、訂閱數據的變化,綁定好更新函數
c、接收到數據變化,通知視圖進行view update
咱先試著寫一個簡單的指令解析方法,實現解析指令初始化視圖。
js部分
function Compile (el, value) {
this.$val = value;
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.compileElement(this.$el);
}
}
Compile.prototype = {
compileElement: function (el) {
let self = this;
let childNodes = el.childNodes;
[].slice.call(childNodes).forEach(node => {
let text = node.textContent;
let reg = /\{\{((?:.|\n)+?)\}\}/;
// 如果是element節點
if (self.isElementNode(node)) {
self.compile(node);
}
// 如果是text節點
else if (self.isTextNode(node) && reg.test(text)) {
// 匹配第一個選項
self.compileText(node, RegExp.$1.trim());
}
// 解析子節點包含的指令
if (node.childNodes && node.childNodes.length) {
self.compileElement(node);
}
})
},
// 指令解析
compile: function (node) {
let nodeAttrs = node.attributes;
let self = this;
[].slice.call(nodeAttrs).forEach(attr => {
var attrName = attr.name;
if (self.isDirective(attrName)) {
var exp = attr.value;
node.innerHTML = typeof this.$val[exp] === 'undefined' ? '' : this.$val[exp];
node.removeAttribute(attrName);
}
});
},
// {{ test }} 匹配變量 test
compileText: function (node, exp) {
node.textContent = typeof this.$val[exp] === 'undefined' ? '' : this.$val[exp];
},
// element節點
isElementNode: function (node) {
return node.nodeType === 1;
},
// text純文本
isTextNode: function (node) {
return node.nodeType === 3
},
// x-XXX指令判定
isDirective: function (attr) {
return attr.indexOf('x-') === 0;
}
}
html部分
<body>
<div id="test">
<h2 x-html="a"></h2>
<p>{{ a }}</p>
</div>
</body>
<script>
var data = {
a: 'hello'
}
new Compile('#test', data)
</script>
結果如圖所示
按照步驟走的我已經實現了指令解析!
這里我們只是實現了指令的解析以及視圖的初始化,并沒有實現數據變化的訂閱以及視圖的更新。完整的Compile則實現了這些功能,詳細代碼如下
/**
* @class 指令解析類 Compile
* @param {[type]} el [element節點]
* @param {[type]} vm [mvvm實例]
*/
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.compileElement(this.$fragment);
// 將文檔碎片放回真實dom
this.$el.appendChild(this.$fragment)
}
}
Compile.prototype = {
compileElement: function (el) {
let self = this;
let childNodes = el.childNodes;
[].slice.call(childNodes).forEach(node => {
let text = node.textContent;
let reg = /\{\{((?:.|\n)+?)\}\}/;
// 如果是element節點
if (self.isElementNode(node)) {
self.compile(node);
}
// 如果是text節點
else if (self.isTextNode(node) && reg.test(text)) {
// 匹配第一個選項
self.compileText(node, RegExp.$1);
}
// 解析子節點包含的指令
if (node.childNodes && node.childNodes.length) {
self.compileElement(node);
}
});
},
// 文檔碎片,遍歷過程中會有多次的dom操作,為提高性能我們會將el節點轉化為fragment文檔碎片進行解析操作
// 解析操作完成,將其添加回真實dom節點中
nodeFragment: function (el) {
let fragment = document.createDocumentFragment();
let child;
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
},
// 指令解析
compile: function (node) {
let nodeAttrs = node.attributes;
let self = this;
[].slice.call(nodeAttrs).forEach(attr => {
var attrName = attr.name;
if (self.isDirective(attrName)) {
var exp = attr.value;
var 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);
}
});
},
// {{ test }} 匹配變量 test
compileText: function (node, exp) {
compileUtil.text(node, this.$vm, exp);
},
// element節點
isElementNode: function (node) {
return node.nodeType === 1;
},
// text純文本
isTextNode: function (node) {
return node.nodeType === 3
},
// x-XXX指令判定
isDirective: function (attr) {
return attr.indexOf('x-') === 0;
},
// 事件指令判定
isEventDirective: function (dir) {
return dir.indexOf('on') === 0;
}
}
// 定義$elm,緩存當前執行input事件的input dom對象
let $elm;
let timer = null;
// 指令處理集合
const compileUtil = {
html: function (node, vm, exp) {
this.bind(node, vm, exp, 'html');
},
text: function (node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
class: function (node, vm, exp) {
this.bind(node, vm, exp, 'class');
},
model: function(node, vm, exp) {
this.bind(node, vm, exp, 'model');
let self = this;
let val = this._getVmVal(vm, exp);
// 監聽input事件
node.addEventListener('input', function (e) {
let newVal = e.target.value;
$elm = e.target;
if (val === newVal) {
return;
}
// 設置定時器 完成ui js的異步渲染
clearTimeout(timer);
timer = setTimeout(function () {
self._setVmVal(vm, exp, newVal);
val = newVal;
})
});
},
bind: function (node, vm, exp, dir) {
let updaterFn = updater[dir + 'Updater'];
updaterFn && updaterFn(node, this._getVmVal(vm, exp));
new Watcher(vm, exp, function(value, oldValue) {
updaterFn && updaterFn(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);
}
},
/**
* [獲取掛載在vm實例上的value]
* @param {[type]} vm [mvvm實例]
* @param {[type]} exp [expression]
*/
_getVmVal: function (vm, exp) {
let val = vm;
exp = exp.split('.');
exp.forEach(key => {
key = key.trim();
val = val[key];
});
return val;
},
/**
* [設置掛載在vm實例上的value值]
* @param {[type]} vm [mvvm實例]
* @param {[type]} exp [expression]
* @param {[type]} value [新值]
*/
_setVmVal: function (vm, exp, value) {
let val = vm;
exps = exp.split('.');
exps.forEach((key, index) => {
key = key.trim();
if (index < exps.length - 1) {
val = val[key];
}
else {
val[key] = value;
}
});
}
}
// 指令渲染集合
const updater = {
htmlUpdater: function (node, value) {
node.innerHTML = typeof value === 'undefined' ? '' : value;
},
textUpdater: function (node, value) {
node.textContent = typeof value === 'undefined' ? '' : value;
},
classUpdater: function () {},
modelUpdater: function (node, value, oldValue) {
// 不對當前操作input進行渲染操作
if ($elm === node) {
return false;
}
$elm = undefined;
node.value = typeof value === 'undefined' ? '' : value;
}
}
好了,到這里兩個和Watcher相關的“菇涼”已經出場了
3、實現Watcher
作為一個和Observer和Compile都有關系的“藍銀”,他做的事情有以下幾點
a、通過Dep接收數據變動的通知,實例化的時候將自己添加到dep中
b、屬性變更時,接收dep的notify,調用自身update方法,觸發Compile中綁定的更新函數,進而更新視圖
這里的代碼比較簡短,所以我決定直接上代碼
/**
* @class 觀察類
* @param {[type]} vm [vm對象]
* @param {[type]} expOrFn [屬性表達式]
* @param {Function} cb [回調函數(一半用來做view動態更新)]
*/
function Watcher(vm, expOrFn, cb) {
this.vm = vm;
expOrFn = expOrFn.trim();
this.expOrFn = expOrFn;
this.cb = cb;
this.depIds = {};
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
else {
this.getter = this.parseGetter(expOrFn);
}
this.value = this.get();
}
Watcher.prototype = {
update: function () {
this.run();
},
run: function () {
let newVal = this.get();
let oldVal = this.value;
if (newVal === oldVal) {
return;
}
this.value = newVal;
// 將newVal, oldVal掛載到MVVM實例上
this.cb.call(this.vm, newVal, oldVal);
},
get: function () {
Dep.target = this; // 將當前訂閱者指向自己
let value = this.getter.call(this.vm, this.vm); // 觸發getter,將自身添加到dep中
Dep.target = null; // 添加完成 重置
return value;
},
// 添加Watcher to Dep.subs[]
addDep: function (dep) {
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep;
}
},
parseGetter: function (exp) {
if (/[^\w.$]/.test(exp)) return;
let exps = exp.split('.');
// 簡易的循環依賴處理
return function(obj) {
for (let i = 0, len = exps.length; i < len; i++) {
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}
}
沒錯就是Watcher這么一個簡短的“藍銀”和Observer和Compile兩位“菇涼”牽扯不清
4、實現MVVM
可以說MVVM是Observer,Compile以及Watcher的“boss”了,他才不會去管他們員工之間的關系,只要他們三能給干活,并且干好活就行。他需要安排給Observer,Compile以及Watche做的事情如下
a、Observer實現對MVVM自身model數據劫持,監聽數據的屬性變更,并在變動時進行notify
b、Compile實現指令解析,初始化視圖,并訂閱數據變化,綁定好更新函數
c、Watcher一方面接收Observer通過dep傳遞過來的數據變化,一方面通知Compile進行view update
具體實現如下
/**
* @class 雙向綁定類 MVVM
* @param {[type]} options [description]
*/
function MVVM (options) {
this.$options = options || {};
let data = this._data = this.$options.data;
let self = this;
Object.keys(data).forEach(key => {
self._proxyData(key);
});
observe(data, this);
new Compile(options.el || document.body, this);
}
MVVM.prototype = {
/**
* [屬性代理]
* @param {[type]} key [數據key]
* @param {[type]} setter [屬性set]
* @param {[type]} getter [屬性get]
*/
_proxyData: function (key, setter, getter) {
let self = this;
setter = setter ||
Object.defineProperty(self, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return self._data[key];
},
set: function proxySetter(newVal) {
self._data[key] = newVal;
}
})
}
}
至此,一個屬于我們自己的mvvm庫也算是完成了。由于本文的代碼較多,又不太好分小部分抽離出來講解,所以我將代碼的解析都直接寫到了代碼中。文中一些不夠嚴謹的思考和錯誤,還請各位小伙伴們拍磚指出,大家一起糾正一起學習。
三、源碼鏈接
最后完整代碼來源(再發一次)
github-xuqiang521/overwrite
碼云-https://git.oschina.net/qiangdada_129/overwrite/my-mvvm
如果喜歡歡迎各位小伙伴們star,overwrite將不斷更新哦。