從 0.5 開始造輪子
??這系列文章主要以學習為主,講述了如何從 0.5 開始 造一個輪子,為什么是0.5因為我查了很多資料,參考了很多。至于為什么第一個是 vue 可能是參考資料比較多,在一個目前在公司的技術(shù)棧是 vue ,于是先擱置了以前的技術(shù)棧, react 。后面空閑了準備撿起react ,開始 造輪子,雖然之前造過,但是 感覺有點 low,后面再說吧。。。
核心 -- 可愛的數(shù)據(jù)數(shù)據(jù)劫持
??數(shù)據(jù)劫持怎么理解,其實很簡單。相信寫過 java 的應(yīng)該很容易理解。其實就是javabeen, 對對象的屬性添加 set,get,操作。在js里面可以通過 Object.defineProperty 來劫持對象屬性的setter和getter操作,當然 es6 里和 vue 里目前已經(jīng)替換成了 Proxy ,之后我們也會替換掉 。數(shù)據(jù)劫持“種下”一個鉤子,當數(shù)據(jù)發(fā)生變化觸發(fā)set函數(shù)做一些操作,get時候又會觸發(fā)一個鉤子。
具體看個例子吧:
let obj = {
name: 'mvvm'
};
let testname = 'vue';
Object.defineProperty(obj, 'name', {
// 1. value: '七里香',
configurable: true, // 2. 可以配置對象,刪除屬性
// writable: true, // 3. 可以修改對象
enumerable: true, // 4. 可以枚舉
// ☆ get,set設(shè)置時不能設(shè)置writable和value,它們代替了二者且是互斥的
get() { // 5. 獲取obj.name的時候就會調(diào)用get方法
return testname;
},
set(val) { // 6. 將修改的值重新賦給name
testname = val;
}
});
console.log(obj);
/*
{
name: 'vue',
set:function(val){},
get:function(){}
}
*/
開始造輪子
要實現(xiàn)mvvm的雙向綁定,就必須要實現(xiàn)以下幾點:
1、實現(xiàn)一個數(shù)據(jù)監(jiān)聽器Observer,能夠?qū)?shù)據(jù)對象的所有屬性進行監(jiān)聽,如有變動可拿到最新值并通知訂閱者也就是我們說的數(shù)據(jù)劫持
2、實現(xiàn)一個指令解析器Compile,對每個元素節(jié)點的指令進行掃描和解析,根據(jù)指令模板替換數(shù)據(jù),以及綁定相應(yīng)的更新函數(shù),說白了就是字符串解析器
3、實現(xiàn)一個Watcher,作為連接Observer和Compile的橋梁,能夠訂閱并收到每個屬性變動的通知,執(zhí)行指令綁定的相應(yīng)回調(diào)函數(shù),從而更新視圖
4、mvvm入口函數(shù),實例,整合以上三者
從這篇文章中找了個圖:

入口函數(shù),輪子的開始
看看實現(xiàn)過程:
this.$options = options; // 配置掛載
this.$el = document.querySelector(options.el); // 獲取dom
this._data = options.data;//數(shù)據(jù)掛載
this._watcherTpl = {};//watcher池 發(fā)布訂閱
this._observer(this._data); //數(shù)據(jù)劫持
// this._compile(dom)
this._compile(this.$el);//渲染
Observer用來數(shù)據(jù)劫持
??給數(shù)據(jù)添加 getter, setter, 并且在setter時候做一些事情,當然這里沒有做深度劫持。下個章節(jié)加上。這里注意一下value,這里我們是使用 let 定義的,如果這里換成 var,就會導致對象的value被最后一個值覆蓋。具體情況 百度一下 let 和 var 在循環(huán)中的區(qū)別就明白了。后續(xù)將替換為 Proxy
查看Observer部分實現(xiàn):
// 重寫data 的 get set 更改數(shù)據(jù)的時候,觸發(fā)watch 更新視圖
myVue.prototype._observer = function (obj) {
var _this = this;
for (key in obj){ // 遍歷數(shù)據(jù)
//訂閱池
// _this._watcherTpl.a = [];
// _this._watcherTpl.b = [];
_this._watcherTpl[key] = {
_directives: []
};
let value = obj[key]; // 獲取屬`性值
let watcherTpl = _this._watcherTpl[key]; // 數(shù)據(jù)的訂閱池
Object.defineProperty(_this._data, key, { // 數(shù)據(jù)劫持
configurable: true, // 可以刪除
enumerable: true, // 可以遍歷
get() {
console.log(`${key}獲取值:${value}`);
return value; // 獲取值的時候 直接返回
},
set(newVal) { // 改變值的時候 觸發(fā)set
console.log(`${key}更新:${newVal}`);
if (value !== newVal) {
value = newVal;
//_this._watcherTpl.xxx.forEach(item)
//[{update:function(){}}]
watcherTpl._directives.forEach((item) => { // 遍歷訂閱池
item.update();
// 遍歷所有訂閱的地方(v-model+v-bind+{{}}) 觸發(fā)this._compile()中發(fā)布的訂閱Watcher 更新視圖
});
}
}
})
};
};
指令解析器Compile
由于這是個最簡單的版本,所以我們暫時只考慮 v-model 和 v-bind 在 input 和 textarea 下的情況。其他情況我們后期迭代處理。
實現(xiàn)情況:
// 模板編譯
myVue.prototype._compile = function (el) {
var _this = this, nodes = el.children; // 獲取app的dom
for (var i = 0, len = nodes.length; i < len; i++) { // 遍歷dom節(jié)點
var node = nodes[i];
if (node.children.length) {
_this._compile(node); // 遞歸深度遍歷 dom樹
}
// 如果有v-model屬性,并且元素是INPUT或者TEXTAREA,我們監(jiān)聽它的input事件
if (node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')) {
node.addEventListener('input', (function (key) {
//attVal = data的值
var attVal = node.getAttribute('v-model'); // 獲取綁定的data
//找到對應(yīng)的發(fā)布訂閱池
_this._watcherTpl[attVal]._directives.push(new Watcher( // 將dom替換成屬性的數(shù)據(jù)并發(fā)布訂閱 在set的時候更新數(shù)據(jù)
node,
_this,
attVal,
'value'
));
return function () {
//觸發(fā)set nodes[i].value;
_this._data[attVal] = nodes[key].value; // input值改變的時候 將新值賦給數(shù)據(jù) 觸發(fā)set=>set觸發(fā)watch 更新視圖
}
})(i));
}
if (node.hasAttribute('v-bind')) { // v-bind指令
var attrVal = node.getAttribute('v-bind'); // 綁定的data
_this._watcherTpl[attrVal]._directives.push(new Watcher( // 將dom替換成屬性的數(shù)據(jù)并發(fā)布訂閱 在set的時候更新數(shù)據(jù)
node,
_this,
attrVal,
'innerHTML'
))
}
var reg = /\{\{\s*([^}]+\S)\s*\}\}/g,
txt = node.textContent; // 正則匹配{{}}
if (reg.test(txt)) {
node.textContent = txt.replace(reg, (matched, attVal) => {
// matched匹配的文本節(jié)點包括{{}}, attVal 是{{}}中間的屬性名
var getName = _this._watcherTpl[attVal]; // 所有綁定watch的數(shù)據(jù)
if (!getName._directives) { // 沒有事件池 創(chuàng)建事件池
getName._directives = [];
}
getName._directives.push(new Watcher( // 將dom替換成屬性的數(shù)據(jù)并發(fā)布訂閱 在set的時候更新數(shù)據(jù)
node,
_this,
attVal,
'innerHTML'
));
return _this._data[attVal];
// return attVal.split('.').reduce((val, key) => {
// return _this._data[key]; // 獲取數(shù)據(jù)的值 觸發(fā)get 返回當前值
// }, _this.$el);
});
}
}
};
實現(xiàn)Watcher
也就是做為 Compile 和 Observer 的連接器,將dom和數(shù)據(jù)劫持聯(lián)系起來。作為一個中間件。說白了就是根據(jù)一些條件更改真實 dom 的 attr。
// new Watcher() 為this._compile()發(fā)布訂閱+ 在this._observer()中set(賦值)的時候更新視圖
function Watcher(el, vm, val, attr) {
this.el = el; // 指令對應(yīng)的DOM元素
this.vm = vm; // myVue實例
this.val = val; // data
this.attr = attr; // 真實dom的屬性
this.update(); // 填入數(shù)組
}
Watcher.prototype.update = function () {
//dom.value = this.mvvm._data[data]
//調(diào)用get
this.el[this.attr] = this.vm._data[this.val]; // 獲取data的最新值 賦值給dom 更新視圖
};
這幾段代碼雖然很短可是可以多揣摩一下。總體下來其實就這些東西。
結(jié)語
?? 其實核心思想大概就是這么3個模塊,能實現(xiàn)一個小的mvvm,本文章的完整代碼見:
下一章 將替換我們的劫持對象 Object.defineProperty 為 Proxy