記一次資料、邏輯、檢視分離的原生JS專案實踐

餘大彬發表於2018-08-14

一切的開始源於這篇文章:一句話理解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層實現的。

相關文章