知乎 live 原地址:編寫優雅的前端業務代碼css
當咱們在寫業務代碼的時候,咱們到底在寫什麼?html
實際上是對交互的一些處理。全部的交互都是基於用戶或者瀏覽器的一些行爲來觸發的,好比渲染頁面,在頁面onload方法觸發以後,咱們的js代碼纔會執行,好比說懶加載,根據用戶滾動或者可視區域的變化來觸發,好比按鈕的點擊以後頁面的局部刷新,好比input框輸入、表單提交、上傳文件等等,以上所說的這些行爲組成起來的就是咱們前端要寫的業務邏輯。前端
咱們所寫的業務都是基於事件來對產品功能和場景進行描述。vue
頁面的執行順序是怎樣的?react
頁面在初始化的時候,先加載css,而後加載html的節點,最後將js放在頁尾按順序執行,而後css加載,模板輸出以後css生效,js再加載,js再對頁面的模板進行交互處理,好比編譯模板,最後再把交互功能好比事件進行綁定。不管寫什麼業務,都會是這個生命週期。設計模式
一個頁面的輸出是有它的生命週期的,同時,一個js程序,好比vue或者react也是有本身的生命週期的,因此每個類,每個業務邏輯都是有本身的生命週期的。瀏覽器
下面是react的組件的生命週期:最開始的時候先拿到默認的參數屬性,而後初始化狀態,在render以前有一個事件的廣播,在render以後有一個事件的廣播,這時候這個組件就render到頁面上了。組件在運行的時候有兩個方式,一個是狀態的改變,它會觸發update,再去觸發state的改變,而後再對應地從新render,在render以前會先觸發事件廣播,render以後也會觸發一個事件廣播。另外一個方式是卸載,卸載以前會觸發一個廣播。安全
vue的生命週期和react的實際上是很像的,它只不過比react多了一步對template和el進行一個判斷的掃描,對應的也是render渲染,在渲染以後會有一個屬性的update,有一個銷燬的過程。前端框架
總結一下,vue和react的生命週期其實就是完成它這個前端框架的業務邏輯。閉包
下面這段代碼是將一個面向過程的js程序改成面向對象方式以後的js程序。
(function (global, $, _, doc) { "use strict"; // 定義一個構造器,是這個文件的入口 var app = function (options) { options = options || {}; this.a = options.a; // ... this.eventMap = { 'click .title': 'titleClick', 'dbclick .input': 'inputDbclick', }; // 初始化全部節點屬性 this.initEles(); this.init(); }; // 定義構造函數靜態屬性,掛載全部選擇器的屬性 app.Eles = { pap: $('.paper'), centryY: $('.centry-y') }; // 工具方法 var utils = { has: function(arr, name) { return arr.indexOf(name) > -1; }, // ... }; app.prototype = { constructor: app, initEles: function () { var eles = app.Eles; for (var name in eles) { if (eles.hasOwnProperty(name)) { this[name] = $(eles[name]); } } }, init: function () { this.bindEvent(this.eventMap); }, initDrag: function () { // ... }, uninitDrag: function () { // ... }, bindEvent: function (maps) { this.initDrag(); this.initOrdinaryEvents(maps); }, unbindEvent: function (maps) { this.uninitDrag(); this.unInitOrdinaryEvents(maps); }, initOrdinaryEvents: function (maps) { this._scanEventsMap(maps, true); }, unInitOrdinaryEvents: function (maps) { this._scanEventsMap(maps, false); }, _scanEventsMap: function (maps, isOn) { var delegateEventSplitter = /^(\S+)\s*(.*)$/, bind = isOn ? this._delegate : this._undelegate; for (var keys in maps) { if (maps.hasOwnProperty(keys)) { var matchs = keys.match(delegateEventSplitter); bind(matchs[1], matchs[2], this[maps[keys]].bind(this)); } } }, _delegate: function (name, selector, func) { // 事件委派,將事件綁定在$(documet)上 doc.on(name, selector, func); }, _undelegate: function (name, selector, func) { doc.off(name, selector, func); }, destroy: function() { this.unbindEvent(); } }; // 將構造函數掛在window上,那麼在外部也能訪問閉包內的屬性,至關於對外暴露了一個接口 global.app = app; $(function () { // 實例化構造函數,至關於這個構造函數就開始執行了 new app(); }); })(this, this.jQuery, this._, this.jQuery(document));
上述代碼作的事情:
eventMap
,定義bindEvent
、initOrdinaryEvents
、_scanEventsMap
、_delegate
方法。遍歷eventMap
,經過事件委派,將事件綁定在$(documet)
上。以前的使用的是onclick
或者on
方法來進行事件的綁定,維護的時候要到各個地方去找,如今只須要關注eventMap
就很方便,可以清楚知道事件、選擇器和事件處理函數名。$('.paper')
,而是用this.pap
(initEles方法實現的)便可,好處是在壓縮的時候,字符串是不能被壓縮的,而this.pap
會被壓縮,壓縮率會更高。if (e.target.id === 'titleDrag' || e.target.id == 'subtitleDrag') {} // 改成 if (['titleDrag', 'subtitleDrag'].indexOf(e.target.id) > -1) {}
$('.center-box').removeClass('hidden').css({ width: ui.helper.width(), left: parseInt(centerY.css('left')) - Math.floor(ui.helper.width() / 2) }); // 改成 var width = ui.helper.width(); var left = parseInt(centerY.css('left'), 10); this.centerBox.removeClass('hidden').css({ width: width, left: left - Math.floor(width / 2) });
posObj[event.target.id] = { id: event.target.id, outerHTML: this.delStyle(event.target.outerHTML), style: '#' + event.target.id + '{position: absolute;left:' + (ui.offset.left - 260) / mmToPx + 'mm;top:' + (ui.offset.top - 40) / mmToPx + 'mm;}' }; // 改成 posObj[id] = { id: id, outerHTML: this.delStyle(target.outerHTML), style: '#' + id + '{' + this._getPositionLT(top, left, mmToPx) + '}' };
$(target).next().removeClass('hidden'); $(target).next().find('.line-left').css({ top: 40, left: ui.offset.left, height: pap.css('height'), width: parseInt(pap.css('width')) - ui.offset.left + 260 + 'px' }); $(target).next().find('.line-top').css({ left: 260, top: ui.offset.top, width: pap.css('width'), height: parseInt(pap.css('height')) - ui.offset.top + 40 + 'px' }); // 改成 this._drawNextLine(nextEle, left, top);
this.centerBox.addClass('hidden'); // 改成 utils.hide(centerBox);
var ul = this.widgetUl; var ht; switch (e.target.id) { case 'thin': ht = '<li><div class="hr thin"></div></li>'; ul.append(ht); break; case 'middle': ht = '<li><div class="hr middle"></div></li>'; ul.append(ht); break; case 'thick': ht = '<li><div class="hr thick"></div></li>'; ul.append(ht); break; } // 改成 this.widgetUl.append('<li><div class="hr ' + e.target.id + '"></div></li>');
var txt = this.txt; var cus = this.cus; var mmToPx = this.mmToPx; var pap = this.pap; var target = $(e.target); txt.text(target.text()); switch (e.target.id) { case 'a4': pap.css({ width: 210 * mmToPx + 'px', height: 297 * mmToPx + 'px' }); cus.addClass('hidden'); break; case 'b5': pap.css({ width: 176 * mmToPx + 'px', height: 250 * mmToPx + 'px' }); cus.addClass('hidden'); break; case '16k': pap.css({ width: 184 * mmToPx + 'px', height: 260 * mmToPx + 'px' }); cus.addClass('hidden'); break; case 'cus': cus.removeClass('hidden'); break; } // 改成 var txt = this.txt; var cus = this.cus; var id = e.target.id; var target = $(e.target); var setpapCss = { 'a4': [210, 297], 'b5': [176, 250], '16k': [184, 260] }; txt.text(target.text()); if (setpapCss[id]) { var wh = setpapCss[id]; this.setPapWH(wh[0], wh[1]); utils.hide(cus); } if (id === 'cur') { utils.show(cus); }
常見的 js 業務場景分析以及解決思路:
把全部的選擇器的屬性掛載構造函數靜態屬性上,統一進行管理,並經過initEles
方法將其掛載在this上。
使用eventMap
進行統一管理。
app在實例化的時候會往頁面里加這個組件,調用this.destory
時會解綁全部事件(還能夠補充把html刪掉)。
參考artTemplate.js。
解耦你的業務 js 代碼,如何在業務中使用設計模式?
模塊化就是把文件拆分紅小文件。
常見的一些業務優化方法總結:
參照上述優化代碼中的switch...case
例子。
建議用沒有樣式意義的自定義屬性data-*
來做爲選擇器,而不是用具備樣式意義的class和id,由於若是有一天css類名變了,那麼js也得改變。
jQuery的css方法,或者對jQuery的css方法的進一步封裝。
增長你的項目可維護性和代碼可讀性:
先寫僞代碼,全部的方法不考慮它的實現,把它拆成粒度比較細的顆粒,用方法名來描述業務邏輯,把方法名都寫好了以後,互相調用,最後再添加最細粒度的方法的實現。用這種方法來寫的話,不寫註釋也能夠,由於方法名就把業務邏輯解釋了。