不好意思!耽誤你的十分鐘,讓MVVM原理還給你,十分鐘mvvm

時間在嘀嗒嘀嗒的走著

既然來了就繼續看看吧

  • 這篇文章其實沒有什么鳥用,只不過對于現在的前端面試而言,已經是一個被問煩了的考點了
  • 既然是考點,那么我就想簡簡單單的來給大家劃一下重點

眾所周知當下是MVVM盛行的時代,從早期的Angular到現在的React和Vue,再從最初的三分天下到現在的兩虎相爭。愛掏網 - it200.com

無疑不給我們的開發帶來了一種前所未有的新體驗,告別了操作DOM的思維,換上了數據驅動頁面的思想,果然時代的進步,改變了我們許多許多。愛掏網 - it200.com

啰嗦話多了起來,這樣不好。愛掏網 - it200.com我們來進入今天的主題

劃重點

MVVM 雙向數據綁定 在Angular1.x版本的時候通過的是臟值檢測來處理

而現在無論是React還是Vue還是最新的Angular,其實實現方式都更相近了

那就是通過數據劫持+發布訂閱模式

真正實現其實靠的也是ES5中提供的Object.defineProperty,當然這是不兼容的所以Vue等只支持了IE8+

為什么是它

Object.defineProperty()說實在的我們大家在開發中確實用的不多,多數是修改內部特性,不過就是定義對象上的屬性和值么?干嘛搞的這么費勁(純屬個人想法)

But在實現框架or庫的時候卻發揮了大用場了,這個就不多說了,只不過輕舟一片而已,還沒到寫庫的實力

知其然要知其所以然,來看看如何使用

let obj = {};
let song = '發如雪'; 
obj.singer = '周杰倫';  

Object.defineProperty(obj, 'music', {
    // 1. value: '七里香',
    configurable: true,     // 2. 可以配置對象,刪除屬性
    // writable: true,         // 3. 可以修改對象
    enumerable: true,        // 4. 可以枚舉
    //  get,set設置時不能設置writable和value,它們代替了二者且是互斥的
    get() {     // 5. 獲取obj.music的時候就會調用get方法
        return song;
    },
    set(val) {      // 6. 將修改的值重新賦給song
        song = val;   
    }
});

// 下面打印的部分分別是對應代碼寫入順序執行
console.log(obj);   // {singer: '周杰倫', music: '七里香'}  // 1

delete obj.music;   // 如果想對obj里的屬性進行刪除,configurable要設為true  2
console.log(obj);   // 此時為  {singer: '周杰倫'}

obj.music = '聽媽媽的話';   // 如果想對obj的屬性進行修改,writable要設為true  3
console.log(obj);   // {singer: '周杰倫', music: "聽媽媽的話"}

for (let key in obj) {    
    // 默認情況下通過defineProperty定義的屬性是不能被枚舉(遍歷)的
    // 需要設置enumerable為true才可以
    // 不然你是拿不到music這個屬性的,你只能拿到singer
    console.log(key);   // singer, music    4
}

console.log(obj.music);   // '發如雪'  5
obj.music = '夜曲';       // 調用set設置新的值
console.log(obj.music);   // '夜曲'    6
復制代碼

以上是關于Object.defineProperty的用法

下面我們來寫個實例看看,這里我們以Vue為參照去實現怎么寫MVVM

// index.html

    "app">
        

{{song}}

《{{album.name}}》是{{singer}}2005年11月發行的專輯

主打歌為{{album.theme}}

作詞人為{{singer}}等人。愛掏網 - it200.com

為你彈奏肖邦的{{album.theme}} 復制代碼

上面是html里的寫法,相信用過Vue的同學并不陌生

那么現在就開始實現一個自己的MVVM吧

打造MVVM

// 創建一個Mvvm構造函數
// 這里用es6方法將options賦一個初始值,防止沒傳,等同于options || {}
function Mvvm(options = {}) {   
    // vm.$options Vue上是將所有屬性掛載到上面
    // 所以我們也同樣實現,將所有屬性掛載到了$options
    this.$options = options;
    // this._data 這里也和Vue一樣
    let data = this._data = this.$options.data;
    
    // 數據劫持
    observe(data);
}
復制代碼

數據劫持

為什么要做數據劫持?

  • 觀察對象,給對象增加Object.defineProperty
  • vue特點是不能新增不存在的屬性 不存在的屬性沒有get和set
  • 深度響應 因為每次賦予一個新對象時會給這個新對象增加defineProperty(數據劫持)

多說無益,一起看代碼

// 創建一個Observe構造函數
// 寫數據劫持的主要邏輯
function Observe(data) {
    // 所謂數據劫持就是給對象增加get,set
    // 先遍歷一遍對象再說
    for (let key in data) {     // 把data屬性通過defineProperty的方式定義屬性
        let val = data[key];
        observe(val);   // 遞歸繼續向下找,實現深度的數據劫持
        Object.defineProperty(data, key, {
            configurable: true,
            get() {
                return val;
            },
            set(newVal) {   // 更改值的時候
                if (val === newVal) {   // 設置的值和以前值一樣就不理它
                    return;
                }
                val = newVal;   // 如果以后再獲取值(get)的時候,將剛才設置的值再返回去
                observe(newVal);    // 當設置為新值后,也需要把新值再去定義成屬性
            }
        });
    }
}

// 外面再寫一個函數
// 不用每次調用都寫個new
// 也方便遞歸調用
function observe(data) {
    // 如果不是對象的話就直接return掉
    // 防止遞歸溢出
    if (!data || typeof data !== 'object') return;
    return new Observe(data);
}
復制代碼

以上代碼就實現了數據劫持,不過可能也有些疑惑的地方比如:遞歸

再來細說一下為什么遞歸吧,看這個栗子

    let mvvm = new Mvvm({
        el: '#app',
        data: {
            a: {
                b: 1
            },
            c: 2
        }
    });
復制代碼

我們在控制臺里看下

被標記的地方就是通過遞歸observe(val)進行數據劫持添加上了get和set,遞歸繼續向a里面的對象去定義屬性,親測通過可放心食用

接下來說一下observe(newVal)這里為什么也要遞歸

還是在可愛的控制臺上,敲下這么一段代碼 mvvm._data.a = {b:'ok'}

然后繼續看圖說話

通過observe(newVal)加上了 現在大致明白了為什么要對設置的新值也進行遞歸observe了吧,哈哈,so easy

數據劫持已完成,我們再做個數據代理

數據代理

數據代理就是讓我們每次拿data里的數據時,不用每次都寫一長串,如mvvm._data.a.b這種,我們其實可以直接寫成mvvm.a.b這種顯而易見的方式

下面繼續看下去,+號表示實現部分

function Mvvm(options = {}) {  
    // 數據劫持
    observe(data);
    // this 代理了this._data
+   for (let key in data) {
        Object.defineProperty(this, key, {
            configurable: true,
            get() {
                return this._data[key];     // 如this.a = {b: 1}
            },
            set(newVal) {
                this._data[key] = newVal;
            }
        });
+   }
}

// 此時就可以簡化寫法了
console.log(mvvm.a.b);   // 1
mvvm.a.b = 'ok';    
console.log(mvvm.a.b);  // 'ok'
復制代碼

寫到這里數據劫持和數據代理都實現了,那么接下來就需要編譯一下了,把{{}}里面的內容解析出來

數據編譯

function Mvvm(options = {}) {
    // observe(data);
        
    // 編譯    
+   new Compile(options.el, this);    
}

// 創建Compile構造函數
function Compile(el, vm) {
    // 將el掛載到實例上方便調用
    vm.$el = document.querySelector(el);
    // 在el范圍里將內容都拿到,當然不能一個一個的拿
    // 可以選擇移到內存中去然后放入文檔碎片中,節省開銷
    let fragment = document.createDocumentFragment();
    
    while (child = vm.$el.firstChild) {
        fragment.appendChild(child);    // 此時將el中的內容放入內存中
    }
    // 對el里面的內容進行替換
    function replace(frag) {
        Array.from(frag.childNodes).forEach(node => {
            let txt = node.textContent;
            let reg = /\{\{(.*?)\}\}/g;   // 正則匹配{{}}
            
            if (node.nodeType === 3 && reg.test(txt)) { // 即是文本節點又有大括號的情況{{}}
                console.log(RegExp.$1); // 匹配到的第一個分組 如: a.b, c
                let arr = RegExp.$1.split('.');
                let val = vm;
                arr.forEach(key => {
                    val = val[key];     // 如this.a.b
                });
                // 用trim方法去除一下首尾空格
                node.textContent = txt.replace(reg, val).trim();
            }
            // 如果還有子節點,繼續遞歸replace
            if (node.childNodes && node.childNodes.length) {
                replace(node);
            }
        });
    }
    
    replace(fragment);  // 替換內容
    
    vm.$el.appendChild(fragment);   // 再將文檔碎片放入el中
}
復制代碼

看到這里在面試中已經可以初露鋒芒了,那就一鼓作氣,做事做全套,來個一條龍

現在數據已經可以編譯了,但是我們手動修改后的數據并沒有在頁面上發生改變

下面我們就來看看怎么處理,其實這里就用到了特別常見的設計模式,發布訂閱模式

發布訂閱

發布訂閱主要靠的就是數組關系,訂閱就是放入函數,發布就是讓數組里的函數執行

// 發布訂閱模式  訂閱和發布 如[fn1, fn2, fn3]
function Dep() {
    // 一個數組(存放函數的事件池)
    this.subs = [];
}
Dep.prototype = {
    addSub(sub) {   
        this.subs.push(sub);    
    },
    notify() {
        // 綁定的方法,都有一個update方法
        this.subs.forEach(sub => sub.update());
    }
};
// 監聽函數
// 通過Watcher這個類創建的實例,都擁有update方法
function Watcher(fn) {
    this.fn = fn;   // 將fn放到實例上
}
Watcher.prototype.update = function() {
    this.fn();  
};

let watcher = new Watcher(() => console.log(111));  // 
let dep = new Dep();
dep.addSub(watcher);    // 將watcher放到數組中,watcher自帶update方法, => [watcher]
dep.addSub(watcher);
dep.notify();   //  111, 111
復制代碼

數據更新視圖

  • 現在我們要訂閱一個事件,當數據改變需要重新刷新視圖,這就需要在replace替換的邏輯里來處理
  • 通過new Watcher把數據訂閱一下,數據一變就執行改變內容的操作
function replace(frag) {
    // 省略...
    // 替換的邏輯
    node.textContent = txt.replace(reg, val).trim();
    // 監聽變化
    // 給Watcher再添加兩個參數,用來取新的值(newVal)給回調函數傳參
+   new Watcher(vm, RegExp.$1, newVal => {
        node.textContent = txt.replace(reg, newVal).trim();    
+   });
}

// 重寫Watcher構造函數
function Watcher(vm, exp, fn) {
    this.fn = fn;
+   this.vm = vm;
+   this.exp = exp;
    // 添加一個事件
    // 這里我們先定義一個屬性
+   Dep.target = this;
+   let arr = exp.split('.');
+   let val = vm;
+   arr.forEach(key => {    // 取值
+      val = val[key];     // 獲取到this.a.b,默認就會調用get方法
+   });
+   Dep.target = null;
}
復制代碼

當獲取值的時候就會自動調用get方法,于是我們去找一下數據劫持那里的get方法

function Observe(data) {
+   let dep = new Dep();
    // 省略...
    Object.defineProperty(data, key, {
        get() {
+           Dep.target && dep.addSub(Dep.target);   // 將watcher添加到訂閱事件中 [watcher]
            return val;
        },
        set(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            observe(newVal);
+           dep.notify();   // 讓所有watcher的update方法執行即可
        }
    })
}
復制代碼

當set修改值的時候執行了dep.notify方法,這個方法是執行watcher的update方法,那么我們再對update進行修改一下

Watcher.prototype.update = function() {
    // notify的時候值已經更改了
    // 再通過vm, exp來獲取新的值
+   let arr = this.exp.split('.');
+   let val = this.vm;
+   arr.forEach(key => {    
+       val = val[key];   // 通過get獲取到新的值
+   });
    this.fn(val);   // 將每次拿到的新值去替換{{}}的內容即可
};
復制代碼

現在我們數據的更改可以修改視圖了,這很good,還剩最后一點,我們再來看看面試常考的雙向數據綁定吧

雙向數據綁定

    // html結構
    "c" type="text">
    
    // 數據部分
    data: {
        a: {
            b: 1
        },
        c: 2
    }
    
    function replace(frag) {
        // 省略...
+       if (node.nodeType === 1) {  // 元素節點
            let nodeAttr = node.attributes; // 獲取dom上的所有屬性,是個類數組
            Array.from(nodeAttr).forEach(attr => {
                let name = attr.name;   // v-model  type
                let exp = attr.value;   // c        text
                if (name.includes('v-')){
                    node.value = vm[exp];   // this.c 為 2
                }
                // 監聽變化
                new Watcher(vm, exp, function(newVal) {
                    node.value = newVal;   // 當watcher觸發時會自動將內容放進輸入框中
                });
                
                node.addEventListener('input', e => {
                    let newVal = e.target.value;
                    // 相當于給this.c賦了一個新值
                    // 而值的改變會調用setset中又會調用notify,notify中調用watcher的update方法實現了更新
                    vm[exp] = newVal;   
                });
            });
+       }
        if (node.childNodes && node.childNodes.length) {
            replace(node);
        }
    }
復制代碼

大功告成,面試問Vue的東西不過就是這個罷了,什么雙向數據綁定怎么實現的,問的一點心意都沒有,差評!!!

大官人請留步,本來應該收手了,可臨時起意(手癢),再寫點功能吧,再加個computed(計算屬性)和mounted(鉤子函數)吧

computed(計算屬性) && mounted(鉤子函數)

    // html結構
    

求和的值是{{sum}}

data: { a: 1, b: 9 }, computed: { sum() { return this.a + this.b; }, noop() {} }, mounted() { setTimeout(() => { console.log('所有事情都搞定了'); }, 1000); } function Mvvm(options = {}) { // 初始化computed,將this指向實例 + initComputed.call(this); // 編譯 new Compile(options.el, this); // 所有事情處理好后執行mounted鉤子函數 + options.mounted.call(this); // 這就實現了mounted鉤子函數 } function initComputed() { let vm = this; let computed = this.$options.computed; // 從options上拿到computed屬性 {sum: ?, noop: ?} // 得到的都是對象的key可以通過Object.keys轉化為數組 Object.keys(computed).forEach(key => { // key就是sum,noop Object.defineProperty(vm, key, { // 這里判斷是computed里的key是對象還是函數 // 如果是函數直接就會調get方法 // 如果是對象的話,手動調一下get方法即可 // 如: sum() {return this.a + this.b;},他們獲取a和b的值就會調用get方法 // 所以不需要new Watcher去監聽變化了 get: typeof computed[key] === 'function' ? computed[key] : computed[key].get, set() {} }); }); } 復制代碼

寫了這些內容也不算少了,最后做一個形式上的總結吧

總結

通過自己實現的mvvm一共包含了以下東西

  1. 通過Object.defineProperty的get和set進行數據劫持
  2. 通過遍歷data數據進行數據代理到this上
  3. 通過{{}}對數據進行編譯
  4. 通過發布訂閱模式實現數據與視圖同步
  5. 通過通過通過,收了,感謝大官人的留步了

補充

針對以上代碼在實現編譯的時候還是會有一些小bug,再次經過研究和高人指點,完善了編譯,下面請看修改后的代碼

修復:兩個相鄰的{{}}正則匹配,后一個不能正確編譯成對應的文本,如{{album.name}} {{singer}}

function Compile(el, vm) {
    // 省略...
    function replace(frag) {
        // 省略...
        if (node.nodeType === 3 && reg.test(txt)) {
            function replaceTxt() {
                node.textContent = txt.replace(reg, (matched, placeholder) => {   
                    console.log(placeholder);   // 匹配到的分組 如:song, album.name, singer...
                    new Watcher(vm, placeholder, replaceTxt);   // 監聽變化,進行匹配替換內容
                    
                    return placeholder.split('.').reduce((val, key) => {
                        return val[key]; 
                    }, vm);
                });
            };
            // 替換
            replaceTxt();
        }
    }
}
復制代碼

上面代碼主要實現依賴的是reduce方法,reduce 為數組中的每一個元素依次執行回調函數

如果還有不太清楚的,那我們單獨抽出來reduce這部分再看一下

    // 將匹配到的每一個值都進行split分割
    // 如:'song'.split('.') => ['song'] => ['song'].reduce((val, key) => val[key]) 
    // 其實就是將vm傳給val做初始值,reduce執行一次回調返回一個值
    // vm['song'] => '周杰倫'
    
    // 上面不夠深入,我們再來看一個
    // 再如:'album.name'.split('.') => ['album', 'name'] => ['album', 'name'].reduce((val, key) => val[key])
    // 這里vm還是做為初始值傳給val,進行第一次調用,返回的是vm['album']
    // 然后將返回的vm['album']這個對象傳給下一次調用的val
    // 最后就變成了vm['album']['name'] => '十一月的蕭邦'
    
    return placeholder.split('.').reduce((val, key) => {
        return val[key]; 
    }, vm);
復制代碼

reduce的用處多多,比如計算數組求和是比較普通的方法了,還有一種比較好用的妙處是可以進行二維數組的展平(flatten),各位不妨來看最后一眼

let arr = [
  [1, 2],
  [3, 4],
  [5, 6]
];

let flatten = arr.reduce((previous, current) => {
  return previous.concat(current);
});

console.log(flatten); // [1, 2, 3, 4, 5, 6]

// ES6中也可以利用...展開運算符來實現的,實現思路一樣,只是寫法更精簡了
flatten = arr.reduce((a, b) => [...a, ...b]);
console.log(flatten); // [1, 2, 3, 4, 5, 6]
復制代碼

再次感謝父老鄉親,兄弟姐妹們的觀看了!這回真的是最后一眼了,已經到底了!


原文發布時間為:2024年6月8日

原文作者:chenhongdong

本文來源:掘金如需轉載請聯系原作者

聲明:所有內容來自互聯網搜索結果,不保證100%準確性,僅供參考。如若本站內容侵犯了原著者的合法權益,可聯系我們進行處理。
發表評論
更多 網友評論0 條評論)
暫無評論

返回頂部

主站蜘蛛池模板: 国产真实伦在线观看| 国产第一福利影院| 中文字幕精品一区| 欧美色图23p| 国产一区二区在线观看麻豆| 98精品国产综合久久| 日本一二线不卡在线观看| 亚洲欧美另类国产| 色哟哟最新在线观看入口| 国产精品国产精品偷麻豆| 两个人看的www在线| 最好看的最新中文字幕2018免费视频| 国产视频一二区| 中文无遮挡h肉视频在线观看| 欧美午夜精品久久久久免费视| 制服美女视频一区| 国产自产视频在线观看香蕉| 天堂网在线www| 丰满人妻一区二区三区免费视频 | 中文在线√天堂| 欧洲精品无码一区二区三区在线播放| 再深点灬舒服了灬太大了在线观看| 91华人在线视频| 国产麻豆交换夫妇| 一区二区手机视频| 日本xxxx在线| 五月天色婷婷丁香| 欺凌小故事动图gif邪恶| 午夜无码A级毛片免费视频| 高清色本在线www| 国产综合成人亚洲区| 一区二区三区在线看| 日本一本一区二区| 亚洲av专区无码观看精品天堂| 波多野吉衣AV无码| 内射人妻无套中出无码| 被弄出白浆喷水了视频| 国产无遮挡吃胸膜奶免费看视频 | 久久99国产精品久久99| 最近2019中文字幕大全第二页| 亚洲欧美日韩久久精品第一区|