從 0.5 開始造輪子 仿 vue 的 mvvm(一)

----歡迎查看我的博客----

從 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ù),實例,整合以上三者

這篇文章中找了個圖:

image

入口函數(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,本文章的完整代碼見:

github完整代碼

在線例子,需要墻

下一章 將替換我們的劫持對象 Object.defineProperty 為 Proxy

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容