日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

MVVM原理还你

發布時間:2023/12/13 编程问答 45 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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一共包含了以下東西

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

    以上是生活随笔為你收集整理的MVVM原理还你的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。