1.事出必有因
最近在看老項目的代碼,一個富客戶端的js代碼,幾千行的代碼,全是function(){} var...的垂直布局,真的是要感動的哭了。
一開始都是這樣,想實現什么功能,不管三七二十一,function走起,最終堆起無數個變量和函數來完成一個畫面的js。我也是,但過段時間自己去改代碼bug或者加功能的時候,我的天,這是我寫的嗎,什么時候寫的,怎么理不清思路了,而且,修改一個地方其他地方也得改,改完了還容易出新bug,偶爾都會忘了是自己寫的,心里默念:這個傻X...恩,還好是默念。
慢慢的代碼看多了點,了解了些js的模塊封裝的一些方式,面向對象的相關思想(單一職責、高內聚低耦合....再說就有點裝了>.<),越來越覺得易讀、易改的代碼應該需要更好的組織形式,正好最近碰到了一個網友相關的問題,看了他想優化的代碼,真有看到自己一開始寫的代碼的感覺:各處填補想完美解決問題,可最后還是會有出乎意料的bug,於是用了他的代碼做了次實踐。
2.一睹為快
如下圖,功能比較簡易:
選擇之后添加,展示區便陳列:
展示區點擊‘X’的時候去除當前內容,選擇區相應也取消對應的勾選:
3.初出茅廬
先上原版代碼看看:

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>多選框問題</title> </head> <body> <!--<input type="text" data-bind-content="name" /> <span data-bind-content='name'></span>--> <h4>選擇區</h4> <div> <ul id="ul1"> <li>全選<input type="checkbox" name="checkall" /></li> <li><input type="checkbox" name="checkthis" /><span>1</span></li> <li><input type="checkbox" name="checkthis" /><span>2</span></li> <li><input type="checkbox" name="checkthis" /><span>3</span></li> <li><input type="checkbox" name="checkthis" /><span>4</span></li> <li><input type="checkbox" name="checkthis" /><span>5</span></li> </ul> </div> <button id="add">添加</button> <h4>展示區</h4> <ul id="ul2"></ul> </body> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script type="text/javascript"> //封裝 var checkBox = (function () { var globalV = []; var yourChose = function (tableId, addClickId, showId) { console.log($('#' + tableId + ' input[name=checkall]')); //全選 $('#' + tableId + ' input[name=checkall]').click(function () { //如果選擇全選, 所有的選擇框都選中,去除全選,所有的選擇框去除選中 if ($(this).prop('checked')) { $('#' + tableId + ' input[name=checkthis]').prop('checked', true); //全選的時候,將所有選框的數據取出來傳給全局變量globalV $('#' + tableId + ' input[name=checkthis]').each(function (i, ele) { var choseDate = {}; choseDate.isChecked = true; choseDate.id = $(ele).parent().children('span').html(); globalV.push(choseDate); }); } else { $('#' + tableId + ' input[name=checkthis]').prop('checked', false); } console.log(globalV); }) //對各個選擇框綁定事件 $('#' + tableId).on('change', 'input[name=checkthis]', function () { var arr = [];//存儲每個選擇框的狀態 var choseDate = {};//存儲被選中的選擇框的數據 //<li><input type="checkbox" name="check-this" /><span>3</span></li>獲取span里面的值 var this_value = $(this).parent().children('span').html(); //遍歷每個選擇框取選擇的狀態 $('#' + tableId + ' input[name=checkthis]').each(function (i, ele) { arr.push($(ele).prop('checked')); }); //如果有未選中的狀態,去除全選框的選中狀態,否則保留添加全選框的的選中狀態 if (arr.indexOf(false) == -1) { $('#' + tableId + ' input[name=checkall]').prop('checked', true); } else { $('#' + tableId + ' input[name=checkall]').prop('checked', false); } //對應每個選擇框的change事件,如果這個選擇框選中,則存儲這個選擇框的數據,否則遍歷存儲數據的變量,移除這個取消選中的的選擇框的數據 if ($(this).is(':checked')) { choseDate.isChecked = true; choseDate.id = this_value; globalV.push(choseDate); } else { for (var i = 0; i < globalV.length; i++) { if (this_value == globalV[i].id) { globalV.splice(i, 1); } } } console.log(globalV); }); //點擊添加按鈕的事件 $('#'+addClickId).click(function (e) { e.preventDefault(); $('#'+showId).empty();//清空展示區里面的內容 console.log(globalV); //如果沒有選中任何選擇框,則彈出提示 if (globalV.length == 0) { alert('請先選擇!'); } else { //如果選中了一些選擇框,則全局變量數據不為空,開始遍歷全局變量 for (var j = 0; j < globalV.length; j++) { //按照全局變量globalV,給展示區創建元素;(包含了刪除按鈕) var liElement = '<li>\ <span>'+ globalV[j].id + '</span>\ <p style="display:inline-block;width:20px;height:20px;background-color:red;border-radius:50%;text-align:center">X</p>\ </li>'; $('#'+showId).append(liElement); } //給刪除按鈕添加點擊事件 $('#'+showId).on('click', 'p', function () { //var findAndChangeState=$(this).parent('li').children('span').html(); //找到這個刪除按鈕對應的父級標簽li下面的span標簽的內容;注意:這個是簡化;就放在了標簽里面,實際情況可能是個屬性,獲取的這個值對應一個選擇框 //由這個值來查找對應的選擇框,從而改變選擇框的狀態; //這里是點擊了刪除按鈕,那么與他對應的選擇框的選中狀態也會被去除 var findAndChangeState = $(this).parent('li').children('span').html(); //遍歷選擇框找到與刪除按鈕對應的選擇框,將其狀態改為未選中,同時將全選的選擇框也改為未選中 $('#' + tableId + ' input[name=checkthis]').each(function (i, ele) { if ($(this).parent().children('span').html() == findAndChangeState) { $(this).parent().children('input').prop('checked', false); $('#' + tableId + ' input[name=checkall]').prop('checked', false); } }); //改完之后這個刪除按鈕對應的父級標簽 $(this).parent('li').remove(); }) } }) }; return { globalV:globalV, yourChose:yourChose } })() checkBox.yourChose('ul1', 'add', 'ul2') </script> </html>
代碼拷下來,看着比我剛開始寫的前台代碼要好不少:注釋到位、封裝避免全局環境污染、事件功能明確。
放瀏覽器跑一遍,多點兩下...bug就出來了,細看以下代碼就知道,bug的出現與他定義的globalV有關,而這個值可以看到是兩處在改動,而且是每次事件都會更新。一個數據多個地方多次更改,想知道怎么出問題了,肯定是要花點時間排查的。
bug就不提了,代碼是以一種非常流程化的思路在行文,該干什么了就碼代碼去干什么,我們都在這么干。可當時自己爽了,以后就不爽了,別人也不爽...特別是當代碼量開始增大的時候。
4.漸入佳境

<!-- event{ select:fun1, add:fun2, remove:fun3, } mvc: model controller view --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>多選框問題</title> </head> <body> <!--<input type="text" data-bind-content="name" /> <span data-bind-content='name'></span>--> <h4>選擇區</h4> <div> <ul id="ul1"> <li>全選<input type="checkbox" name="checkall" /></li> <li><input type="checkbox" name="checkthis" /><span>1</span></li> <li><input type="checkbox" name="checkthis" /><span>2</span></li> <li><input type="checkbox" name="checkthis" /><span>3</span></li> <li><input type="checkbox" name="checkthis" /><span>4</span></li> <li><input type="checkbox" name="checkthis" /><span>5</span></li> </ul> </div> <button id="add">添加</button> <h4>展示區</h4> <ul id="ul2"></ul> </body> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script type="text/javascript"> //封裝 var checkBox = (function () { var globalV = []; var yourChose = function (tableId, addClickId, showId) { //負責更新數據 var updateData = function () { globalV = []; $('#' + tableId + ' input[name=checkthis]').each(function () { if ($(this).is(':checked')) { var choseDate = {}; var this_value = $(this).parent().children('span').html(); choseDate.isChecked = true; choseDate.id = this_value; globalV.push(choseDate); } }); } //負責更新畫面 //checkBox狀態 function fun1() { if ($(this).attr("name") == "checkthis") { var arr = [];//存儲每個選擇框的狀態 var choseDate = {};//存儲被選中的選擇框的數據 //<li><input type="checkbox" name="check-this" /><span>3</span></li>獲取span里面的值 var this_value = $(this).parent().children('span').html(); //遍歷每個選擇框取選擇的狀態 $('#' + tableId + ' input[name=checkthis]').each(function (i, ele) { arr.push($(ele).prop('checked')); }); //如果有未選中的狀態,去除全選框的選中狀態,否則保留添加全選框的的選中狀態 if (arr.indexOf(false) == -1) { $('#' + tableId + ' input[name=checkall]').prop('checked', true); } else { $('#' + tableId + ' input[name=checkall]').prop('checked', false); } } else { //如果選擇全選, 所有的選擇框都選中,去除全選,所有的選擇框去除選中 if ($(this).prop('checked')) { $('#' + tableId + ' input[name=checkthis]').prop('checked', true); } else { $('#' + tableId + ' input[name=checkthis]').prop('checked', false); } } } //展示區狀態(新增) function fun2() { $('#' + showId).empty();//清空展示區里面的內容 updateData(); //如果沒有選中任何選擇框,則彈出提示 if (globalV.length == 0) { alert('請先選擇!'); } else { //如果選中了一些選擇框,則全局變量數據不為空,開始遍歷全局變量 for (var j = 0; j < globalV.length; j++) { //按照全局變量globalV,給展示區創建元素;(包含了刪除按鈕) var liElement = '<li>\ <span>'+ globalV[j].id + '</span>\ <p style="display:inline-block;width:20px;height:20px;background-color:red;border-radius:50%;text-align:center">X</p>\ </li>'; $('#' + showId).append(liElement); } //給刪除按鈕添加點擊事件 bindEvent('#' + showId + ' p', "click", event.removeLi); } } //展示區狀態(刪除) function fun3() { //var findAndChangeState=$(this).parent('li').children('span').html(); //找到這個刪除按鈕對應的父級標簽li下面的span標簽的內容;注意:這個是簡化;就放在了標簽里面,實際情況可能是個屬性,獲取的這個值對應一個選擇框 //由這個值來查找對應的選擇框,從而改變選擇框的狀態; //這里是點擊了刪除按鈕,那么與他對應的選擇框的選中狀態也會被去除 var findAndChangeState = $(this).parent('li').children('span').html(); //遍歷選擇框找到與刪除按鈕對應的選擇框,將其狀態改為未選中,同時將全選的選擇框也改為未選中 $('#' + tableId + ' input[name=checkthis]').each(function (i, ele) { if ($(this).parent().children('span').html() == findAndChangeState) { $(this).parent().children('input').prop('checked', false); $('#' + tableId + ' input[name=checkall]').prop('checked', false); } }); //改完之后這個刪除按鈕對應的父級標簽 $(this).parent('li').remove(); } //負責注冊事件 var event = { select: fun1, add: fun2, removeLi: fun3 }; var bindEvent = function (selector, type, fun) { $(selector).bind(type, fun); }; //對各個選擇框綁定事件 bindEvent('#' + tableId + ' input[type=checkbox]', "click", event.select); //點擊添加按鈕的事件 bindEvent('#' + addClickId, "click", event.add); }; return { globalV: globalV, yourChose: yourChose } })() checkBox.yourChose('ul1', 'add', 'ul2'); </script> </html>
更改后的版本里的代碼其實都是原來的代碼,但組織后的效果是:事件統一綁定(bindEvent),畫面統一更新(fun1、fun2、fun3),數據統一設定(updateData)。
區分的很清楚,哪兒出錯找哪兒,幾乎不會交叉。而且比較容易拓展,像事件可以繼續bindEvent綁定,畫面更新的函數可以相應與fun1、fun2、fun3並列添加,數據的額外處理可以添加到updateData里。
這僅僅是代碼組織上的優化,其實代碼本身也有很多可以改進的地方,像全選的判定、選擇區聯動刪除等都有更好的思路和代碼實現。
5.夢中初醒
漸漸發現,其實這里面已經有mvc的影子了,各司其職,分工明確,事件綁定那部分就算是一個弱controller,綁定事件,分發事件響應函數;更新畫面狀態部分相當於view了,更新畫面;updateData更新數據部分更新的就是modle;
6.醍醐灌頂
這個組織基本夠用了,但它並不是真正的MVC,也不是最優組織,需要你,一語道破天機,希望有人能醍醐灌頂....
順便看看萬金油的MVC模型:
就是看不懂,是不,哈哈。