記一次數據、邏輯、視圖分離的原生JS項目實踐
一切的開始源於這篇文章:一句話理解Vue核心內容。
在文章中,作者給出了這樣一個思考:
假設現在有一個這樣的需求,有一張圖片,在被點擊時,可以記錄下被點擊的次數。 這看起來很簡單吧, 按照上面提到到開發方式,應該很快就可以搞定。 那麽接下來,需求稍微發生了點變動, 要求有兩張圖片,分別被點擊時,可以記錄下各自的點擊次數。這次似乎也很簡單,只需把原先的代碼復制粘貼一份就可以了。 那麽當這個需求變成五張圖片時,你會怎麽做? 還是簡單復制粘貼吧,這樣完全可以完成這個需求,但是你會覺得很別扭,因為你的代碼此時變得很臃腫,存在很多重復的過程,但是似乎還在你的忍受範圍內。 這時候需求又發生了微小的變動,還是五張照片分別記錄被點擊次數,不過這樣單獨羅列五張圖片似乎太占空間,現在只需要存在一個圖片的位置,通過選擇按鈕來切換被點擊的圖片。 這時候你可能會奔潰掉,因為要完成這個看似微小的改動,你原先寫的大部分代碼可能都需要被刪掉,甚至是完全清空掉,從零開始寫起。
也許你應該像我一樣,從一張圖片到五張圖片完成上面的需求。相信我,這個過程很有趣。因為每增加一次需求,你或多或少都會需要重構你的代碼。特別是如果你直接從一張跳到五張的話,那麽你就需要完全重構你的代碼。
二話不說,先看整個項目的效果。這裏我直接放了五張圖片實現的效果。
說實話,這其實是一個非常簡單的demo,只要對JS的知識稍微熟悉一點,並且在寫代碼時註意一下閉包的問題,就可以輕松的實現效果。在沒學vue之前,我們一定是這樣寫代碼的。
<ul> <li>one</li> <li>two</li> <li>three</li> <li>fore</li> <li>five</li> </ul> <div class="container"> <img class="pic" src=‘http://www.jqhtml.com/wp-content/themes/sc/images/logo.png‘> <img class="pic" src=‘http://www.jqhtml.com/wp-content/themes/sc/images/logo.png‘> <img class="pic" src=‘http://www.jqhtml.com/wp-content/themes/sc/images/logo.png‘> <img class="pic" src=‘http://www.jqhtml.com/wp-content/themes/sc/images/logo.png‘> <img class="pic" src=‘http://www.jqhtml.com/wp-content/themes/sc/images/logo.png‘> <p class=‘num‘></p> <p class=‘num‘></p> <p class=‘num‘></p> <p class=‘num‘></p> <p class=‘num‘></p> </div> <script> var img = document.getElementsByTagName(‘img‘); var num = document.getElementsByTagName(‘p‘); var li = document.getElementsByTagName(‘li‘); for (let i = 0; i < 5; i++) { li[i].onclick = (function(index) {//形成閉包 return (function(e) { for (let j = 0; j < 5; j++) { //console.log(num); num[j].removeAttribute(‘class‘); img[j].removeAttribute(‘class‘); } num[index].setAttribute(‘class‘,‘show‘); img[index].setAttribute(‘class‘,‘show‘); }) })(i) img[i].onclick = counter(num[i]); } //計數器函數 function counter(ele) { var num = 0,//點擊的次數 node = ele; return function(e) {//形成閉包讓每個元素都有自己私有num變量 node.innerHTML = ++num; } } </script>
這種直接操作DOM來改變視圖的開發方式似乎並不能hold住復雜的邏輯和代碼量,況且在這個例子中邏輯並非很復雜。這也證明了由JS來直接操作DOM以改變視圖的開發方式並不適合如今的前端開發。這也是前端開發為什麽需要類似vue這樣的框架。
如果你學過vue,你會發現完成這個需求,只需要改一下data對象裏的圖片數就輕松的實現了需求。(用vue實現上面的需求更加簡單,只需要幾行代碼就可以實現,並且可擴展性也好,感興趣的同學可以用vue實現一下上面的需求)
我們可以明顯的感覺到vue這種數據和視圖分離的代碼組織方式更加的容易實現擴展,並且代碼可讀性更強。而我們上面的原生JS 的實現方式將數據和視圖都混在一起了,當項目需求越來越復雜的時候會讓代碼越臃腫,且越不易於擴展。
其實數據和視圖分離並不是框架的專利,要知道框架也是由原生的JS實現的。因此原生JS也可以寫出數據和視圖分離的代碼,讓項目變得更加易於擴展。
下面我們就按照數據、視圖、邏輯分離的思路來重構一下我們這個項目的代碼。
首先,我們把數據給抽離,可以看到視圖的樣子大概是這樣的一個形式。
<body> <ul id="cat-list"> //列表 </ul> <section id="cat">//貓圖片的顯示區域 <h2 id="cat-name"></h2> <div id="cat-count"></div> <img src="" alt="" id="cat-img"> </section> </body>
我們將數據存儲在一個名為model的對象中。
var model = { currentCat: null, cats: [ //貓的圖片數據 { clickCount : 0, name : ‘Tabby‘, imgSrc : ‘img/434164568_fea0ad4013_z.jpg‘, }, //省略余下的圖片數據 ] }
在初始化頁面的時候,我們要加載數據,渲染頁面。
var catView = { //圖片區域的視圖 init: function() { //儲存DOM元素,方便後續操作 this.cat = document.getElementById(‘cat‘); this.catName = document.getElementById(‘cat-name‘); this.catCount = document.getElementById(‘cat-count‘); this.catImg = document.getElementById(‘cat-img‘); this.cat.addEventListener(‘click‘,function() {//給每張圖片添加點擊事件 controler.addCount(); },false); this.render(); }, render: function() { let currentCat = controler.getCurrentCat(); this.catName.textContent = currentCat.name; this.catCount.textContent = currentCat.clickCount; this.catImg.src = ‘../‘ + currentCat.imgSrc; } } var listView = { //列表區域的視圖 init: function() { this.catList = document.getElementById(‘cat-list‘); this.render(); }, render: function() { let cats = controler.getCats(); let fragment = document.createDocumentFragment(‘ul‘); cats.forEach((item,index) => { let li = document.createElement(‘li‘); li.textContent = item.name; li.setAttribute(‘class‘,‘item‘); li.addEventListener(‘click‘,function() {//給li添加點擊事件 controler.setCurrentCat(item); catView.render(); }) fragment.appendChild(li); }) this.catList.appendChild(fragment); fragment = null; } }
從上面的視圖對象可以知道,視圖並不直接從model中獲取數據,而是通過一個中間對象controler來間接訪問model,也就是說controler對象實現了所有的視圖和數據間的邏輯操作。
var controler = { init: function() { model.currentCat = model.cats[0]; catView.init(); listView.init(); }, //獲取全部的貓 getCats: function() { return model.cats; }, //獲取當前顯示的貓 getCurrentCat: function() { return model.currentCat; }, //設置當前被點擊的貓 setCurrentCat: function(cat) { return model.currentCat = cat; }, addCount: function() { model.currentCat.clickCount++; catView.render(); } }
到這裏,我們用數據、視圖、邏輯分離的代碼組織方式重構了一個小型的項目,從該項目中可以清楚的看到:數據model只負責存儲數據,而視圖view只負責頁面的渲染,而controler負責view和model之間的交互邏輯的實現。
等一下,既然說交互邏輯是放在controler中實現的,而視圖只負責渲染頁面,那為什麽click點擊事件會放在視圖層呢?
這裏要明確一下的就是(僅個人理解):視圖並不是俠義上的靜態頁面,視圖指的是靜態頁面和動態入口(用戶交互,如點擊事件),所以事件的綁定放在view層是完全可以理解的,view層實現了一個動態的入口,而用戶點擊後的所有邏輯操作都是在controler層實現的。
記一次數據、邏輯、視圖分離的原生JS項目實踐