记一次数据、逻辑、视图分离的原生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层实现的。