記一次數據、邏輯、視圖分離的原生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層實現的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM