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

posted @ 2018-08-14 08:00  余大彬  阅读(1355)  评论(1编辑  收藏  举报