咱們天天都在使用各類各樣的框架,這些框架伴隨着咱們天天的工做。經過使用這些框架的目的是爲了解放咱們,不多人去真正關心這些框架的背後都作了些什麼。我也使用了很多的框架,經過這些流行框架也讓我學習到了一些知識,就想把這些東西分享出來。javascript
每一個標題都是一個獨立的主題,徹底能夠根據須要挑有興趣的閱讀。html
常用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
元素,咱們須要建立兩層嵌套: table
、tbody
。瀏覽器
有了這個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時,它的雙向數據綁定讓人印象深入。此外另外一個神奇特徵就是依賴注入。下面是一個簡單的例子:
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<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注入到任意想要的函數中。咱們甚至能夠實現應用的配置化。再也不須要將參數傳來傳去,代價僅僅是經過register
和 resolve
。
目前爲止咱們的自動注入並非完美的,存在兩個缺點:
一、函數不支持自定義參數。
二、上線代碼壓縮致使參數名字改變,致使沒法獲取正確的依賴項。
這兩個問題AngualrJS
已經所有解決了,有興趣能夠看個人另外一篇文章: javascript實現依賴注入的思路,裏面詳細介紹了依賴注入的完整解決方案。
可能如今大多數人一聽到計算屬性,首先想到的是 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
使用 getter
和 setter
來操做對象的數據。這就簡化了計算屬性的實現,由於咱們以前還有一層要處理實際的變量。可是,若是咱們可以將計算屬性與普通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
方法能夠接受對象、對象的屬性名、getter
和 setter
。咱們要作的就是編寫這兩個方法的實現邏輯。運行上面的代碼,咱們就能獲得想要的結果:
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賦值,最終返回完整的名字。
在大型框架和庫的背後包含着許多優秀前輩的經驗。經過學習這些框架可以讓咱們更好理解這些框架背後的原理,可以脫離框架開發,這點很重要。