MVVM原理還你

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

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

啰嗦話多了起來,這樣不好。我們來進入今天的主題

劃重點

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

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

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

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

為什么是它

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

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

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

let obj = {};
obj.singer = '周杰倫';  
Object.defintProperty(obj, 'music', {configurable: true,     // 可以配置對象,刪除屬性// writable: true,         // 可以修改對象enumerable: true        // 可以枚舉// value: '七里香',// ☆ get,set設置時不能設置writable和value,它們代替了二者且是互斥的get() {     // 獲取obj.music的時候就會調用get方法return '發如雪';},set(val) {      // obj.music = '聽媽媽的話'console.log(val);   // '聽媽媽的話'}
});console.log(obj);   // {singer: '周杰倫', music: '七里香'}delete obj.music;   // 如果想對obj里的屬性進行刪除,configurable要設為true
console.log(obj);   // 此時為  {singer: '周杰倫'}obj.music = '聽媽媽的話';   // 如果想對obj的屬性進行修改,writable要設為true
console.log(obj);   // {singer: '周杰倫', music: "聽媽媽的話"}for (let key in o) {    // 默認情況下通過defineProperty定義的屬性是不能被枚舉(遍歷)的// 需要設置enumerable為true才可以// 不然你是拿不到music這個屬性的,你只能拿到singerconsole.log(key);   // singer, music
}

以上是關于Object.defineProperty的用法

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

// index.html
<body><div id="app"><h1>{{song}}</h1><p>《{{album.name}}》是{{singer}}2005年11月發行的專輯</p><p>主打歌為{{album.theme}}</p><p>作詞人為{{singer}}等人。</p>為你彈奏肖邦的{{album.theme}}</div><!--實現的mvvm--><script src="mvvm.js"></script><script>// 寫法和Vue一樣let mvvm = new Mvvm({el: '#app',data: {     // Object.defineProperty(obj, 'song', '發如雪');song: '發如雪',album: {name: '十一月的蕭邦',theme: '夜曲'},singer: '周杰倫'}});</script>
</body>

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

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

打造MVVM

// 創建一個Mvvm構造函數
// 這里用es6方法將options賦一個初始值,防止沒傳,等同于options || {}
function Mvvm(options = {}) {   // vm.$options Vue上是將所有屬性掛載到上面// 所以我們也同樣實現,將所有屬性掛載到了$optionsthis.$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 = fragment.firstChild) {fragment.appendChild(child);    // 此時將el中的內容放入內存中}// 對el里面的內容進行替換function replace(frag) {Array.from(frag.childNodes).forEach(node => {let txt = node.textContent;let reg = /\{\{(.*)\}\}/;   // 正則匹配{{}}if (node.nodeType === 3 && reg.test(txt)) { // 即是文本節點又有大括號的情況{{}}console.log(RegExp.$1); // 匹配到的第一個分組 如: a.b, clet arr = RegExp.$1.split('.');let val = vm;arr.forEach(key => {val = val[key];     // 如this.a.b});// 用trim方法去除一下首尾空格node.textContent = txt.replace(reg, val).trim();}// 如果還有子節點,繼續遞歸replaceif (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結構<input v-model="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  typelet exp = attr.value;   // c        textif (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結構<p>求和的值是{{sum}}</p>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,noopObject.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. 通過通過通過,收了,感謝大官人的留步了

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/247557.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/247557.shtml
英文地址,請注明出處:http://en.pswp.cn/news/247557.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

poj1316

1&#xff0e;鏈接地址 https://vjudge.net/problem/POJ-1316 2&#xff0e;問題描述 In 1949 the Indian mathematician D.R. Kaprekar discovered a class of numbers called self-numbers. For any positive integer n, define d(n) to be n plus the sum of the digits of …

CSS頁面布局解決方案大全

前端布局非常重要的一環就是頁面框架的搭建&#xff0c;也是最基礎的一環。在頁面框架的搭建之中&#xff0c;又有居中布局、多列布局以及全局布局&#xff0c;今天我們就來總結總結前端干貨中的CSS布局。 居中布局 水平居中 1&#xff09;使用inline-blocktext-align&#xff…

AES加密算法的學習筆記

AES簡介高級加密標準(AES,Advanced Encryption Standard)為最常見的對稱加密算法(微信小程序加密傳輸就是用這個加密算法的)。 對稱加密算法也就是加密和解密用相同的密鑰&#xff0c;具體的加密流程如下圖&#xff1a; 下面簡單介紹下各個部分的作用與意義&#xff1a; 明文P沒…

為什么要用setTimeout模擬setInterval ?

setInterval有兩個缺點&#xff1a; 使用setInterval時&#xff0c;某些間隔會被跳過&#xff1b;可能多個定時器會連續執行&#xff1b;在前一個定時器執行完前&#xff0c;不會向隊列插入新的定時器&#xff08;解決缺點一&#xff09;保證定時器間隔&#xff08;解決缺點二&…

前端 crypto-js aes 加解密

背景 前段時間公司做項目&#xff0c;該項目涉及到的敏感數據比較多&#xff0c;經過的一波討論之后&#xff0c;決定前后端進行接口加密處理&#xff0c;采用的是 AES BASE64 算法加密~ 網上關于 AES 對稱加密的算法介紹看上一篇&#xff01; 具體實現 其實搞懂了是怎么一回事…

對排序算法的研究

算法是什么&#xff1f;、 算法&#xff08;Algorithm&#xff09; 代表著用系統的方法描述解決問題的策略機制&#xff0c;可以通過一定規范的 輸入&#xff0c;在有限時間內獲得所需要的 輸出。 一個算法的好壞是通過 時間復雜度 與 空間復雜度 來衡量的。 簡單來說&#xff…

js實用算法

判斷文本是否為回文 定義&#xff1a;如果將一個文本翻轉過來&#xff0c;能和原文本完全相等&#xff0c;那么就可以稱之為“回文”。 方法一&#xff08;字符串、數組內置方法&#xff09;123456789101112131415/** 判斷文字是否為回文* param {string|number} val 需要判斷的…

stylus

stylus格式 指將css中{} &#xff1b;去掉即可

隨筆記錄(2019.7.10)

1、ISO/OSI 網絡七層參考模型 物理層 數據鏈路層 網絡層 傳輸層 會話層 表示層 應用層 2、 TCP/IP 網絡四層模型和五層模型 四層模型&#xff1a; 網絡接口層 網絡層 傳輸層 應用層 五層模型&#xff1a; 物理層 數據鏈路層 網絡層 傳輸層 應用層 3、 協議簇 &#xff08;1&a…

轉發:Ajax動態畫EChart圖表

本人由于項目需要&#xff0c;在狀態變化的時候需要動態繪制對應數據的EChart圖表&#xff0c;并且不刷新整個網頁。 所以就用Ajax動態畫EChart圖表&#xff0c;下面是開發過程中遇到的一些坑的總結。 流程&#xff1a;頁面首次加載時展示一幅原始的圖形&#xff0c;若后臺數據…

如果硬盤不顯示可以這么處理

http://www.zhuangjiba.com/soft/9574.html轉載于:https://www.cnblogs.com/braveheart007/p/11167311.html

Highcharts的餅圖大小的控制

在Highcharts中&#xff0c;餅圖的大小是Highcharts自動計算并進行繪制。餅圖的大小受數據標簽大小、數據標簽到切片的距離影響。當數據標簽內容較多&#xff0c;并且距離切片較遠時&#xff0c;餅圖就會被壓縮的很小。解決這個問題&#xff0c;有以下幾種方法&#xff1a;&…

轉:谷歌離線地圖基礎

一.需要文件 gapi3文件夾&#xff1a;存放接口等tilemap文件夾&#xff1a;存放圖片gapi.js文件maptool.js文件 二.html配置 <script type"text/javascript" src"gapi.js"></script> <script type"text/javascript" src"map…

HTTP Header 詳解

搜集資料 HTTP&#xff08;HyperTextTransferProtocol&#xff09;即超文本傳輸協議&#xff0c;目前網頁傳輸的的通用協議。HTTP協議采用了請求/響應模型&#xff0c;瀏覽器或其他客戶端發出請求&#xff0c;服務器給與響應。就整個網絡資源傳輸而言&#xff0c;包括message-h…

Windows CE 6.0中斷處理過程(轉載)

這里我們主要討論的是CE的中斷建立和中斷相應的大概流程以及所涉及的代碼位置。這里所講述的&#xff0c;是針對ARM平臺的。在CE的中斷處理里面&#xff0c;有一部分工作是CE Kernel完成的&#xff0c;有一部分工作是要由OEM完成的。 Kernel代碼工作 ExVector.s&#xff1a;中斷…

document.createDocumentFragment 以及創建節點速度比較

document.createDocumentFragment document.createDocumentFragment()方法創建一個新空白的DocumentFragment對象。 DocumentFragments是DOM節點。它們不是主DOM樹的一部分。通常的用例是創建文檔片段&#xff0c;將元素附加到文檔片段&#xff0c;然后將文檔片段附加到DOM樹。…

Javascript重溫OOP之原型與原型鏈

prototype原型對象 每個函數都有一個默認的prototype屬性&#xff0c;其實際上還是一個對象&#xff0c;如果被用在繼承中&#xff0c;姑且叫做原型對象。 在構造函數中的prototype中定義的屬性和方法&#xff0c;會被創建的對象所繼承下來。舉個栗子&#xff1a; function F()…

webpack超詳細配置

在這里就不詳細介紹webpack來源以及作用了, 本篇博文面向新手主要說明如何配置webpack, 以及webpack的使用方法, 直到創建出一個合理的屬于自己webpack項目; 流程 webpack安裝 Step 1: 首先安裝Node.js, 可以去Node.js官網下載.Step2: 在Git或者cmd中輸入下面這段代碼, 通過全局…

小白十分鐘-推薦導航欄

大腿繞道&#xff0c;給小白學習用&#xff0c;上代碼 <div class"list"><div class"infor"><ul class"left"><li><a href"">限時特價</a></li><li><a href"">熱門推…

Underscore.js常用方法介紹

Underscore.js是一個很精干的庫&#xff0c;壓縮后只有4KB。它提供了幾十種函數式編程的方法&#xff0c;彌補了標準庫的不足&#xff0c;大大方便了JavaScript的編程。MVC框架Backbone.js就將這個庫作為自己的工具庫。除了可以在瀏覽器環境使用&#xff0c;Underscore.js還可以…