超贊的動手建立JavaScript架構的詳細教程,超贊javascript

來源:互聯網
上載者:User

超贊的動手建立JavaScript架構的詳細教程,超贊javascript

 覺得Mootools不可思議?想知道Dojo是如何?的?對JQuery的技巧感到好奇?在這篇教程裡,我們將探尋架構背後的秘密,然後試著自己動手建立一個你所喜愛的架構的簡易版本。

我們幾乎每天都在使用各種各樣的JavaScript架構。當你剛入門的時候,方便的DOM(文件物件模型)操作讓你覺得JQuery這樣的東西非常棒。這是因為:首先,對於新手來說DOM太難理解了;當然,對於一個API來說難以理解可不是什麼好事。其次,瀏覽器間的相容性問題非常令人困擾。

  •     我們將元素封裝成對象是因為我們想要能夠為對象添加方法。
  • 在這個教程裡,我們將試著從頭實現這些架構之一。是的,這會很有趣,不過在你太過興奮前我要澄清幾點:
  •     這不會是一個功能很完善的架構。的確,我們要寫很多東西,但它還算不上JQuery。可是我們將要做的事情會讓你體驗到在真正編寫架構的感覺。
  •     我們不打算保證全方位的相容性。我們將要編寫的架構能夠在 Internet Explorer 8+、Firefox 5+、Opera 10+、Chrome和Safari上工作。
  •     我們的架構不會覆蓋到所有可能的功能。比如說,我們的append和preappend方法只有在你傳給它一個我們架構的執行個體時才能工作;我們不會用原生的DOM節點和節點列表。

    另外:儘管在教程中我們不會為我們的架構編寫測試案例,但是我已經在第一次開發它的時候做好了。你可以從 Github上擷取架構和測試案例的代碼。


第一步: 建立架構模板

我們將從一些封裝代碼開始,它將容納我們的整個架構。這是典型的立即函數(IIFE).
 

window.dome = (function () { function Dome (els) { } var dome = {  get: function (selector) {  } }; return dome;}());

你可以看到,我們的架構叫做dome,因為它是一個基本的DOM架構。沒錯,基本(lame有“瘸子”、“不完整”的意思,dom加lame等於dome)的。

我們已經有了一些東西。 首先,我們有了一個函數;它將成為構造架構的對象執行個體的建構函式;那些對象將會包含我們選擇和建立的元素。

然後,我們有了一個dome對象,它就是我們的架構對象;你可以看到它最終作為函數的傳回值返回給了函數調用者(譯註:賦值給了window.dome)。這裡還有一個空的get函數,我們將用它從頁面裡選取元素。那麼,我們來填充代碼吧。

第二步: 擷取元素

dome的get函數只有一個參數,但是它可以是很多東西。如果它一個string(字串),我們將假定它是一個CSS(層疊樣式表)選取器;不過我們也可能得到一個DOM節點或者DOM節點列表。
 

get: function (selector) { var els; if (typeof selector === "string") {  els = document.querySelectorAll(selector); } else if (selector.length) {  els = selector; } else {  els = [selector]; } return new Dome(els);}

我們用document.querySelectorAll來簡單的選擇元素:當然,這將限制我們的瀏覽器安全色性,不過對於這種情況還是可以接受的。如果selector不是string類型,我們將檢查它的length屬性。如果存在,我們就知道我們得到的是一個節點列表;否則,就是一個單獨的元素,我們將它放到一個數組裡。這是因為我們要在下面向Dome傳遞一個數組。你可以看到,我們返回了一個新的Dome對象。讓我們回到Dome函數並且為它填充代碼。

第三步: 建立Dome執行個體

這是Dome函數:
 

function Dome (els) { for(var i = 0; i < els.length; i++ ) {  this[i] = els[i]; } this.length = els.length;}

    我強烈建議你去深入研究一些你喜歡的架構

這非常簡單:我們只是遍曆了els的所有元素,並且把它們儲存在一個以數字為索引的新對象裡。然後我們添加了一個length屬性。

但是這有什麼意義呢?為什麼不直接返回元素?因為:我們將元素封裝成對象是因為我們想要能夠為對象添加方法;這些方法能夠讓我們遍曆這些元素。實際上這正是JQuery的解決方案的濃縮版。

我們的Dome對象已經返回了,現在讓我們來為它的原型(prototype)添加一些方法。我會直接把那些方法寫在Dome函數下面。

第四步:添加幾個工具 + 生產力

要添加的第一批功能是些簡單的工具函數。由於Dome對象可能包含至少一個DOM元素,那麼我們需要在幾乎每一個方法裡面都遍曆所有元素;這樣,這些工具才會給力。

我們從一個map函數開始:   
 

Dome.prototype.map = function (callback) { var results = [], i = 0; for ( ; i < this.length; i++) {  results.push(callback.call(this, this[i], i)); } return results;};

當然,這個map函數有一個入參,一個回呼函數。我們遍曆Dome對象所有元素,收集回呼函數的傳回值到結果集中。注意我們是怎樣調用回呼函數的:
 

callback.call(this, this[i], i));

通過這種方式,函數將在Dome執行個體的上下文中被調用,並且函數接收到兩個參數:當前元素和元素序號。

我們也想要一個foreach函數。事實上這很簡單:
 

Dome.prototype.forEach(callback) { this.map(callback); return this;};

由於map函數和foreach函數之間的不同僅僅是map需要返回些東西,我們可以僅僅將回調傳給this.map然後忽略返回的數組;代替返回的是,我們將返回this,來使我們的庫呈鏈式。foreach會被頻繁的調用,所以,注意當一個函數的回調被返回,事實上,返回的是Dome執行個體。例如,下面的方法事實上就返回了Dome執行個體:
 

Dome.prototype.someMethod1 = function (callback) { this.forEach(callback); return this;};Dome.prototype.someMethod2 = function (callback) { return this.forEach(callback);};

還有一個:mapOne。很容易就知道這個函數是做什麼的,但是真正的問題是,為什麼需要它?這就需要一些我們稱之為"庫哲學"的東西了。
一個簡短的"哲學"闡釋

  •     首先,對於一個初學者來說,DOM很讓人糾結;它的API不完善。

如果構建一個庫僅僅是寫代碼,那就不是什麼難事。但是當我開發這個庫時,我發現那些不完善的部分決定了一定數量的方法的實現方式。

很快,我們要去構建一個返回被選擇元素的文本的text方法。如果Dome對象包含多個DOM節點(比如dome.get("li")),返回什嗎?如果你就像jQuery那樣($("li").text())很簡單的編寫,你將得到一個字串,這個字串是所有元素的文本的直接拼接。有用嗎?我認為沒用,但是我不認為沒有更好的辦法。

對於這個項目,我將以數組方式返回多個元素的文本,除非數組裡只有一個元素,那麼我僅僅返回一個文本字串,而不是一個包含了一個元素的數組。我想你會經常去擷取單個元素的文本,所以我們最佳化了那種情況。但是,如果你想去擷取多個元素的文本,我們的返回你也會用著很爽。
回到代碼

那麼,mapOne方法僅僅是簡單的運行map函數,然後返回數組,或者一個數組裡的元素。如果你仍然不確定這是如何有用,堅持一下,你就會看到!
 

Dome.prototype.mapOne = function (callback) { var m = this.map(callback); return m.length > 1 ? m : m[0];};

 
第5步: 處理Text和HTML

接著,讓我們來添加文本方法。就像jQuery,我們可以傳遞一個string值,設定節點元素的text值,或者通過無參方法得到返回的text值。
 

Dome.prototype.text = function (text) { if (typeof text !== "undefined") {  return this.forEach(function (el) {   el.innerText = text;  }); } else {  return this.mapOne(function (el) {   return el.innerText;  }); }};

如你所料,當我們設定(setting)或者得到(getting)value值時,需要檢查text的值。要注意的是如果justif(文本)方法不起作用,是因為text為空白字串是一個錯誤的值。

如果我們設定(setting)時,可是使用一個forEach 遍曆元素,設定它們的innerText屬性。如果我們得到(getting)時,返回元素的innerText屬性。在使用mapOne方法是要注意:如果我們正在處理多個元素,將返回一個數組;其他的則還是一個字串。

如果html方法使用innerHTML 屬性而不是innerText,它將會更優雅的處理涉及text文本的事情。
 

Dome.prototype.html = function (html) { if (typeof html !== "undefined") {  this.forEach(function (el) {   el.innerHTML = html;  });  return this; } else {  return this.mapOne(function (el) {   return el.innerHTML;  }); }};

就像我說過的:幾乎相同的。

第六步: 修改類

下一步,我們想對class進行操作,所以添加能addClass()和removeClass()。addClass()的參數是一個class名稱或者名稱的數組。為了實現動態參數,我們需要對參數的類型進行判斷。如果參數是一個數組,那麼遍曆這個數組,將元素添加上這些class名稱,如果參數是一個字串,則直接加上這個class名稱。函數需要確保不將原來的class名稱弄亂。
 

Dome.prototype.addClass = function (classes) { var className = ""; if (typeof classes !== "string") {  for (var i = 0; i < classes.length; i++) {   className += " " + classes[i];  } } else {  className = " " + classes; } return this.forEach(function (el) {  el.className += className; });};

很直觀吧?嘿嘿

現在,寫下removeClass(),同樣簡單。不過每次只允許刪除一個class名稱。
 

Dome.prototype.removeClass = function (clazz) { return this.forEach(function (el) {  var cs = el.className.split(" "), i;  while ( (i = cs.indexOf(clazz)) > -1) {   cs = cs.slice(0, i).concat(cs.slice(++i));  }  el.className = cs.join(" "); });};

對於每一個元素,我們都將el.className 分割成一個字串數組。那麼我們使用一個while迴圈串連,直到cs.indexOf(clazz)傳回值大於-1。我們將得到的結果join成el.className。

第七步: 修複一個IE引起的BUG

我們處理的最糟瀏覽器是IE8.在這個小小的庫中,只有一個IE引起的BUG需要去修複; 並且謝天謝地,修複它非常簡單.IE8不支援Array的方法indexOf;我們需要在removeClass方法中使用到它, 下面讓我們來完成它:
 

if (typeof Array.prototype.indexOf !== "function") { Array.prototype.indexOf = function (item) {  for(var i = 0; i < this.length; i++) {   if (this[i] === item) {    return i;   }  }  return -1; };}

它看上去非常簡單,並且它不是完整實現(不支援使用第二個參數),但是它能實現我們的目標.

第8步: 調整屬性

現在,我們想要一個attr函數。這將很容易,因為它幾乎和text方法或者html方法是一樣的。像這些方法,我們都能夠設定和得到屬性:我們將設定一個屬性的名稱和值,同時只通過參數名來得到值。
 

Dome.prototype.attr = function (attr, val) { if (typeof val !== "undefined") {  return this.forEach(function(el) {   el.setAttribute(attr, val);  }); } else {  return this.mapOne(function (el) {   return el.getAttribute(attr);  }); }};

如果形參有一個值,我們將遍曆元素並通過元素的setAttribute方法設定屬性值。另外,我們將使用mapOne返回通過getAttribute方法得到參數。

第9步: 建立元素

像任何一個優秀的架構一樣,我們也應該能夠建立元素。當然,在Demo執行個體中沒有一個好的方法,所以讓我們來把方法加入到demo工程中。
 

var dome = { // get method here create: function (tagName, attrs) { }};

正如你所看到的:我們需要兩個形參:元素名,和一個參數對象。大多數的屬性通過我們的arrt方法被使用,但是tagName和attrs卻有特殊待遇。我們為className屬性使用addClass方法,為text屬性使用text方法。當然,我們首先要建立元素,和Demo對象。下面就是所有的作用:
 

create: function (tagName, attrs) { var el = new Dome([document.createElement(tagName)]);  if (attrs) {   if (attrs.className) {    el.addClass(attrs.className);    delete attrs.className;   }  if (attrs.text) {   el.text(attrs.text);   delete attrs.text;  }  for (var key in attrs) {   if (attrs.hasOwnProperty(key)) {    el.attr(key, attrs[key]);   }  } } return el;}

如上,我們建立了元素,將他發送到新的Dmoe對象中。接著,我們處理所有屬性。注意:當使用完className和text屬性後,我們不得不刪除他們。這將保證當我們遍曆其他的鍵時,它們還能被使用。當然,我們最終通過返回這個新的Demo對象。

我們建立了新的元素,我們想要將這些元素插入到DOM,對吧?

第10步:尾部添加(Appending)與頭部添加(Prepending)元素

下一步,我們來實現尾部添加與頭部添加方法。考慮到多種情境,實現這些方法可能有些棘手。下面是我們的想要達到的效果:
 

dome1.append(dome2);dome1.prepend(dome2);

    IE8對我們來說就是一奇葩。

尾部添加或頭部添加,包括以下幾種情境:

  •     單個新元素添加至單個或多個已存在元素中
  •     多個新元素添加至單個或多個已存在元素中
  •     單個已存在元素添加至單個或多個已存在元素中
  •     多個已存在元素添加至單個或多個已存在元素中

注意:這裡的”新元素“表示還未加入DOM中節點元素,”已存在元素“指已存在於DOM中的節點元素。
現在讓我們一步步來實現之:
 

Dome.prototype.append = function (els) { this.forEach(function (parEl, i) {  els.forEach(function (childEl) {  }); });};

假設參數els是一個DOM對象。一個功能完備的DOM庫應該能處理節點(node)或節點序列(nodelist),但現在我們不作要求。首先遍曆需要被添加進的元素 (父元素),再在這個迴圈中遍曆將被添加的元素 (子項目)。
    如果將一個子項目添加至多個父元素,需要複製子項目(避免最後一次操作會移除上一次添加操作)。可是,沒必要在初次添加的時候就複製,只需要在其它迴圈中複製就可以了。因此處理如下:
 

if (i > 0) { childEl = childEl.cloneNode(true);}

變數i來自外層forEach迴圈:它表示父級元素的序號。第一個父元素添加的是子項目本身,而其他父元素添加的都是目標子項目的複製。因為作為參數傳入的子項目是未被複製的,所以,當將單個子項目添加至單個父元素時,所有的節點都是可響應的。
最後,真正的添加元素操作:

parEl.appendChild(childEl);

因此,組合起來,我們得到以下實現:
 

Dome.prototype.append = function (els) { return this.forEach(function (parEl, i) {  els.forEach(function (childEl) {   if (i > 0) {    childEl = childEl.cloneNode(true);   }   parEl.appendChild(childEl);  }); });};

prepend方法

我們按照相同的邏輯實現prepend方法,其實也相當簡單。
 

Dome.prototype.prepend = function (els) { return this.forEach(function (parEl, i) {  for (var j = els.length -1; j > -1; j--) {   childEl = (i > 0) ? els[j].cloneNode(true) : els[j];   parEl.insertBefore(childEl, parEl.firstChild);  } });};

不同點在於添加多個元素時,添加後的順序會被反轉。所以不能採用forEach迴圈,而是用倒序的for迴圈代替。同樣的,在添加至非第一個父元素時需複製目標子項目。

第十一步: 刪除節點

對於我們最後一個節點的操作方法,從dom中刪除這些節點,很簡單,只需要:

 Dome.prototype.remove = function () { return this.forEach(function (el) {  return el.parentNode.removeChild(el); });};

只需要通過節點的迭代和在他們的父節點調用刪除子節點方法。比較好的是這個dom對象依然正常工作(感謝文件物件模型吧)。我們可以在它上面使用我們想使用的方法,包括插入,預插回DOM,很漂亮,不是嗎?

第12步:事件處理

最後,卻是最重要的一環,我們要寫幾個事件處理函數。

如你所知,IE8依然使用舊的IE事件,因此我們需要為此作檢測。同時,我們也要做好使用DOM 0 級事件的準備。

查看下面的方法,我們稍後會討論:
 

Dome.prototype.on = (function () { if (document.addEventListener) {  return function (evt, fn) {   return this.forEach(function (el) {    el.addEventListener(evt, fn, false);   });  }; } else if (document.attachEvent) {  return function (evt, fn) {   return this.forEach(function (el) {    el.attachEvent("on" + evt, fn);   });  }; } else {  return function (evt, fn) {   return this.forEach(function (el) {    el["on" + evt] = fn;   });  }; }}());

在這裡,我們用到了立即執行函數(IIFE),在函數內我們做了特性檢測。如果document.addEventListener方法存在,我們就使用它;另外我們也檢測document.attachEvent,如果沒有就使用DOM 0級方法。請注意我們如何從立即執行函數中返回最終函數:其最後會被分配到Dome.prototype.on。在做特性檢測時,與每次運行函數時檢測相比,這樣的方式分配適合的方法更加方便。

事件解除綁定方法off與on方法類似:.
 

Dome.prototype.off = (function () { if (document.removeEventListener) {  return function (evt, fn) {   return this.forEach(function (el) {    el.removeEventListener(evt, fn, false);   });  }; } else if (document.detachEvent) {  return function (evt, fn) {   return this.forEach(function (el) {    el.detachEvent("on" + evt, fn);   });  }; } else {  return function (evt, fn) {   return this.forEach(function (el) {    el["on" + evt] = null;   });  }; }}());

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.