揭祕js框架中的經常使用套路

咱們天天都在使用各類各樣的框架,這些框架伴隨着咱們天天的工做。經過使用這些框架的目的是爲了解放咱們,不多人去真正關心這些框架的背後都作了些什麼。我也使用了很多的框架,經過這些流行框架也讓我學習到了一些知識,就想把這些東西分享出來。javascript

每一個標題都是一個獨立的主題,徹底能夠根據須要挑有興趣的閱讀。html

字符串轉DOM

常用jquery的小夥伴對下面的代碼應該一點都不陌生:java

var text = $('<div>hello, world</div>');

$('body').append(text)
複製代碼

以上代碼執行的結果就是在頁面增長了一個div節點。拋開jQuery, 代碼可能會變得稍稍複雜:jquery

var strToDom = function(str) {
    var temp = document.createElement('div');

    temp.innerHTML = str;
    return temp.childNodes[0];
}

var text = strToDom('<div>hello, world</div>');

document.querySelector('body').appendChild(text);
複製代碼

這段代碼,跟使用jQuery的效果是如出一轍的,哈哈jQuery也不過如此嘛。若是你這麼想你就錯了。下面兩種代碼運行的有什麼區別:ajax

var tableTr = $('<tr><td>Simple text</td></tr>');
$('body').append(tableTr);

var tableTr = strToDom('<tr><td>Simple text</td></tr>');
document.querySelector('body').appendChild(tableTr);
複製代碼

表面上看沒任何的問題,若是用開發者工具看頁面結構的話,會發現:json

strToDom 僅僅建立了一個文本節點,而不是一個真正的tr標籤。緣由是包含HTML元素的字符串經過解析器在瀏覽器中運行,解析器忽略了沒有放置在正確的上下文中的標籤, 所以咱們只能獲得一個文本節點。api

jQuery 是如何解決這個問題的呢? 經過分析源碼,我找到了下面的代碼:數組

var wrapMap = {
  option: [1, '<select multiple="multiple">', '</select>'],
  legend: [1, '<fieldset>', '</fieldset>'],
  area: [1, '<map>', '</map>'],
  param: [1, '<object>', '</object>'],
  thead: [1, '<table>', '</table>'],
  tr: [2, '<table><tbody>', '</tbody></table>'],
  col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
  td: [3, '<table><tbody><tr>', '</tr></tbody></table>'],
  _default: [1, '<div>', '</div>']
};
wrapMap.optgroup = wrapMap.option;
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td; 
複製代碼

每個元素,須要特殊處理數組分配。這個想法是爲了構建正確的DOM元素和依賴的嵌套級別獲取咱們所須要的東西。例如, tr 元素,咱們須要建立兩層嵌套: tabletbody瀏覽器

有了這個Map映射表後,咱們就能夠拿到最終須要的標籤。下面代碼演示瞭如何從<tr><td>hello word</td></tr>中取到tr閉包

var match = /<\s*\w.*?>/g.exec(str);
var tag = match[0].replace(/</g, '').replace(/>/g, '');
複製代碼

剩下的就是根據合適的上下文返回DOM元素, 最終咱們將strToDom進行最終的修改:

var strToDom = function(str) {
  var wrapMap = {
    option: [1, '<select multiple="multiple">', '</select>'],
    legend: [1, '<fieldset>', '</fieldset>'],
    area: [1, '<map>', '</map>'],
    param: [1, '<object>', '</object>'],
    thead: [1, '<table>', '</table>'],
    tr: [2, '<table><tbody>', '</tbody></table>'],
    col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
    td: [3, '<table><tbody><tr>', '</tr></tbody></table>'],
    _default: [1, '<div>', '</div>']
  };
  wrapMap.optgroup = wrapMap.option;
  wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
  wrapMap.th = wrapMap.td;
  var element = document.createElement('div');
  var match = /<\s*\w.*?>/g.exec(str);

  if(match != null) {
    var tag = match[0].replace(/</g, '').replace(/>/g, '');
    var map = wrapMap[tag] || wrapMap._default, element;
    str = map[1] + str + map[2];
    element.innerHTML = str;
    // Descend through wrappers to the right content
    var j = map[0]+1;
    while(j--) {
      element = element.lastChild;
    }
  } else {
    // if only text is passed
    element.innerHTML = str;
    element = element.lastChild;
  }
  return element;
}
複製代碼

經過 match != null 判斷是建立的是標籤仍是文本節點。這一次咱們經過瀏覽器能夠建立一個有效的DOM樹。最後經過使用while循環,直到取到咱們想要的標籤,最後返回這個標籤。

AngularJS 依賴注入

當咱們開始使用AngularJS時,它的雙向數據綁定讓人印象深入。此外另外一個神奇特徵就是依賴注入。下面是一個簡單的例子:

function TodoCtrl($scope, $http) {
  $http.get('users/users.json').success(function(data) {
    $scope.users = data;
  });
}
複製代碼

這是一個典型的AngularJS控制器寫法:經過發起一個HTTP請求,從JSON文件獲取數據,並將數據賦值給 $scope.users 。AngularJS框架會自動將$scope$http注入控制器中。讓咱們看看它是如何實現的。

看一個例子,咱們想將用戶姓名顯示到頁面上,爲了簡單起見,採用的mock假數據模擬http請求:

var dataMockup = ['John', 'Steve', 'David'];
var body = document.querySelector('body');
var ajaxWrapper = {
  get: function(path, cb) {
    console.log(path + ' requested');
    cb(dataMockup);
  }
}

var displayUsers = function(domEl, ajax) {
  ajax.get('/api/users', function(users) {
    var html = '';
    for(var i=0; i < users.length; i++) {
      html += '<p>' + users[i] + '</p>';
    }
    domEl.innerHTML = html;
  });
}

displayUsers(body, ajaxWrapper)
複製代碼

displayUsers(body, ajaxWrapper)執行須要兩個依賴項:body和ajaxWrapper。咱們的目標是直接調用displayUsers()而沒有傳遞參數,也能按咱們指望的運行。

大部分的框架提供了依賴注入機制有一個模塊,一般叫injector。全部的依賴統一在這裏註冊,並提供對外訪問的接口:

var injector = {
  storage: {},
  register: function(name, resource) {
    this.storage[name] = resource;
  },
  resolve: function(target) {

  }
};
複製代碼

其中關鍵的resolve的實現:它接收一個目標對象,經過返回一個閉包,包裝target並調用它。例如:

resolve: function(target) {
  return function() {
    target();
  };
}
複製代碼

這樣咱們就能夠調用咱們須要的依賴的函數了。

下一步就是獲取target的參數列表了,這裏我引用了AngularJS的實現方式:

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
...
function annotate(fn) {
  ...
  fnText = fn.toString().replace(STRIP_COMMENTS, '');
  argDecl = fnText.match(FN_ARGS);
  ...
}
複製代碼

我屏蔽了其它代碼細節,只留下對咱們有用的部分。 annotate 對應的就是咱們本身的 resolve 。它將經過目標函數轉換爲一個字符串,同時還將註釋給去掉了, 最終獲得參數信息:

resolve: function(target) {
  var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
  var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
  fnText = target.toString().replace(STRIP_COMMENTS, '');
  argDecl = fnText.match(FN_ARGS);
  console.log(argDecl);
  return function() {
    target();
  }
}
複製代碼

打開控制檯:

其中argDecl數組的第二個元素包含了全部的參數, 經過參數名稱就能夠獲得injector中存儲的依賴項了。 下面是具體的實現:

resolve: function(target) {
  var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
  var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
  fnText = target.toString().replace(STRIP_COMMENTS, '');
  argDecl = fnText.match(FN_ARGS)[1].split(/, ?/g);
  var args = [];
  for(var i=0; i&lt;argDecl.length; i++) {
    if(this.storage[argDecl[i]]) {
      args.push(this.storage[argDecl[i]]);
    }
  }
  return function() {
    target.apply({}, args);
  }
}
複製代碼

經過 .split(/, ?/g) 將字符串 domEl, ajax 轉換成數組, 經過檢查injector中是否註冊了同名的依賴,若是存在,將依賴項放入一個新的數組做爲參數傳遞給 target 函數。

調用的代碼應該是這樣的:

injector.register('domEl', body);
injector.register('ajax', ajaxWrapper);

displayUsers = injector.resolve(displayUsers);
displayUsers();
複製代碼

這樣的實現的好處是,咱們將domEl和ajax注入到任意想要的函數中。咱們甚至能夠實現應用的配置化。再也不須要將參數傳來傳去,代價僅僅是經過registerresolve

目前爲止咱們的自動注入並非完美的,存在兩個缺點:

一、函數不支持自定義參數。

二、上線代碼壓縮致使參數名字改變,致使沒法獲取正確的依賴項。

這兩個問題AngualrJS已經所有解決了,有興趣能夠看個人另外一篇文章: javascript實現依賴注入的思路,裏面詳細介紹了依賴注入的完整解決方案。

Ember Computed屬性

可能如今大多數人一聽到計算屬性,首先想到的是 Vue 中的 Computed 計算屬性。其實在 Ember 框架也提供了這樣一個特性,用於計算屬性的屬性。有點繞口,看一個官方例子吧:

App.Person = Ember.Object.extend({
  firstName: null,
  lastName: null,
  fullName: function() {
    return this.get('firstName') + ' ' + this.get('lastName');
  }.property('firstName', 'lastName')
});
var ironMan = App.Person.create({
  firstName: "Kobe",
  lastName:  "Bryant"
});
ironMan.get('fullName') // "Kobe Bryant"
複製代碼

Person 對象具備firstName和lastName屬性。computed屬性fullName返回包含person全名的鏈接字符串。使人奇怪的地方在於fullName的函數使用了 .property 方法。 咱們看一下 property 的代碼:

Function.prototype.property = function() {
  var ret = Ember.computed(this);
  // ComputedProperty.prototype.property expands properties; no need for us to
  // do so here.
  return ret.property.apply(ret, arguments);
};
複製代碼

經過添加新屬性調整全局函數對象的原型。在類定義期間運行一些邏輯是一種很好的方法。

Ember 使用 gettersetter 來操做對象的數據。這就簡化了計算屬性的實現,由於咱們以前還有一層要處理實際的變量。可是,若是咱們可以將計算屬性與普通js對象一塊兒使用,那就更有趣了。例如:

var User = {
  firstName: 'Kobe',
  lastName: 'Bryant',
  name: function() {
    // getter + setter
  }
};

console.log(User.name); // Kobe Bryant
User.name = 'LeBron James';
console.log(User.firstName); // LeBron
console.log(User.lastName); // James
複製代碼

name做爲一個常規屬性,本質上就是一個獲取或設置firstName和lastName的函數。

JavaScript有一個內置的特性,能夠幫助咱們實現這個想法:

var User = {
  firstName: 'Kobe',
  lastName: 'Bryant',
};

Object.defineProperty(User, "name", {
  get: function() { 
    return this.firstName + ' ' + this.lastName;
  },
  set: function(value) { 
    var parts = value.toString().split(/ /);
    this.firstName = parts[0];
    this.lastName = parts[1] ? parts[1] : this.lastName;
  }
});
複製代碼

Object.defineProperty 方法能夠接受對象、對象的屬性名、gettersetter 。咱們要作的就是編寫這兩個方法的實現邏輯。運行上面的代碼,咱們就能獲得想要的結果:

console.log(User.name); // Kobe Bryant
User.name = 'LeBron James';
console.log(User.firstName); // LeBron
console.log(User.lastName); // James
複製代碼

Object.defineProperty 雖然是咱們想要的,但顯然咱們不想每次都這麼寫。在理想的狀況下,咱們但願提供一個接口。在本節中,咱們將編寫一個名爲 Computize 的函數,它將處理對象並以某種方式將name函數轉換爲具備相同名稱的屬性。

var Computize = function(obj) {
  return obj;
}
var User = Computize({
  firstName: 'Kobe',
  lastName: 'Bryant',
  name: function() {
    ...
  }
});
複製代碼

咱們想使用name方法做爲setter,同時做爲getter。這相似於Ember的計算屬性。

如今,咱們將本身的邏輯添加到函數對象的原型中:

Function.prototype.computed = function() {
  return { computed: true, func: this };
};
複製代碼

這樣就能夠在每一個Function定義後直接調用computed函數了。

name: function() {
  ...
}.computed()
複製代碼

name屬性再也不是一個函數,而變成一個對象: { computed: true, func: this } 。其中 computed 等於true, func屬性指向本來的函數。

真正神奇的事情發生在Computize helper的實現中。它遍歷對象的全部屬性,對全部的計算屬性使用object.defineproperty:

var Computize = function(obj) {
  for(var prop in obj) {
    if(typeof obj[prop] == 'object' && obj[prop].computed === true) {
      var func = obj[prop].func;
      delete obj[prop];
      Object.defineProperty(obj, prop, {
        get: func,
        set: func
      });
    }
  }
  return obj;
}
複製代碼

注意: 咱們將計算屬性name刪除了,緣由是Object.defineProperty在某些瀏覽器下僅對未定義的屬性起做用。

下面是使用.computed()函數的用戶對象的最終版本:

var User = Computize({
  firstName: 'Kobe',
  lastName: 'Bryant',
  name: function() {
    if(arguments.length > 0) {
      var parts = arguments[0].toString().split(/ /);
      this.firstName = parts[0];
      this.lastName = parts[1] ? parts[1] : this.lastName;
    }
    return this.firstName + ' ' + this.lastName;
  }.computed()
});
複製代碼

函數的邏輯就是,判斷是否有參數,若是有參數就直接將參數進行分割處理,並分別爲firstname和lastname賦值,最終返回完整的名字。

結束

在大型框架和庫的背後包含着許多優秀前輩的經驗。經過學習這些框架可以讓咱們更好理解這些框架背後的原理,可以脫離框架開發,這點很重要。

相關文章
相關標籤/搜索