javascript三大框架,從零開始定義自己的JavaScript框架(一)

 2023-10-04 阅读 29 评论 0

摘要:來自:http://www.ituring.com.cn/article/48461 ? 1.1 模塊的定義 一個框架想要能支撐較大的應用,首先要考慮怎么做模塊化。有了內核和模塊加載系統,外圍的模塊就可以一個一個增加。不同的JavaScript框架,實現模塊化方式各有不同,我們來選

來自:http://www.ituring.com.cn/article/48461

?

1.1 模塊的定義

一個框架想要能支撐較大的應用,首先要考慮怎么做模塊化。有了內核和模塊加載系統,外圍的模塊就可以一個一個增加。不同的JavaScript框架,實現模塊化方式各有不同,我們來選擇一種比較優雅的方式作個講解。

先問個問題:我們做模塊系統的目的是什么?如果覺得這個問題難以回答,可以從反面來考慮:假如不做模塊系統,有什么樣的壞處?

我們經歷過比較粗放、混亂的前端開發階段,頁面里充滿了全局變量,全局函數。那時候要復用js文件,就是把某些js函數放到一個文件里,然后讓多個頁面都來引用。

考慮到一個頁面可以引用多個這樣的js,這些js互相又不知道別人里面寫了什么,很容易造成命名的沖突,而產生這種沖突的時候,又沒有哪里能夠提示出來。所以我們要有一種辦法,把作用域比較好地隔開。

JavaScript這種語言比較奇怪,奇怪在哪里呢,它的現有版本里沒package跟class,要是有,我們也沒必要來考慮什么自己做模塊化了。那它是要用什么東西來隔絕作用域呢?

在很多傳統高級語言里,變量作用域的邊界是大括號,在{}里面定義的變量,作用域不會傳到外面去,但我們的JavaScript大人不是這樣的,他的邊界是function。所以我們這段代碼,i仍然能打出值:

for(var i=0; i<5; i++){//do something}
alert(i);

那么,我們只能選用function做變量的容器,把每個模塊封裝到一個function里。現在問題又來了,這個function本身的作用域是全局的,怎么辦?我們想不到辦法,拔劍四顧心茫然。

我們有沒有什么可參照的東西呢?這時候,腦海中一群語言飄過: C語言飄過:“我不是面向對象語言哦~不需要像你這么組織哦~”,“死開!” Java飄過:“我是純面向對象語言哦,連main都要在類中哦,編譯的時候通過裝箱清單指定入口哦~”,“死開!” C++飄過:“我也是純面向對象語言哦”,等等,C++是純面向對象的語言嗎?你的main是什么???main是特例,不在任何類中!

啊,我們發現了什么,既然無法避免全局的作用域,那與其讓100個function都全局,不如只讓一個來全局,其他的都由它管理。

本來我們打算自己當上帝的,現在只好改行先當個工商局長。你想開店嗎?先來注冊,不然封殺你!于是良民們紛紛來注冊。店名叫什么,從哪進貨,賣什么的,一一登記在案,為了方便下面的討論,我們連進貨的過程都讓工商局管理起來。

店名,指的就是這里的模塊名,從哪里進貨,代表它依賴什么其他模塊,賣什么,表示它對外提供一些什么特性。

好了,考慮到我們的這個注冊管理機構是個全局作用域,我們還得把它掛在window上作為屬性,然后再用一個function隔離出來,要不然,別人也定義一個同名的,就把我們覆蓋掉了。

(function(){window.thin ={define:function(name, dependencies, factory){//register a module}};})();

?

在這個module方法內部,應當怎么去實現呢?我們的module應當有一個地方存儲,但存儲是要在工商局內部的,不是隨便什么人都可以看到的,所以,這個存儲結構也放在工商局同樣的作用域里。

用什么結構去存儲呢?工商局備案的時候,店名不能跟已有的重復,所以我們發現這是用map的很好場景,考慮到JavaScript語言層面沒有map,我們弄個Object來存。

(function(){var moduleMap ={};window.thin ={define:function(name, dependencies, factory){if(!moduleMap[name]){varmodule={name: name,dependencies: dependencies,factory: factory};moduleMap[name]=module;}return moduleMap[name];}};})();

?

現在,模塊的存儲結構就搞好了。

1.2 模塊的使用

存的部分搞好了,我們來看看怎么取。現在來了一個商家,賣木器的,他需要從一個賣釘子的那邊進貨,賣釘子的已經來注冊過了,現在要讓這個木器廠能買到釘子。現在的問題是,兩個商家處于不同的作用域,也就是說,它們互相不可見,那通過什么方式,我們才能讓他們產生調用關系呢?

個人解決不了的問題還是得靠政府,有困難要堅決克服,沒有困難就制造困難來克服。現在困難有了,該克服了。商家說,我能不能給你我的進貨名單,你幫我查一下它們在哪家店,然后告訴我?這么簡單的要求當然一口答應下來,但是采用什么方式傳遞給你呢?這可犯難了。

我們參考AngularJS框架,寫了一個類似的代碼:

thin.define("A",[],function(){//module A});thin.define("B",["A"],function(A){//module Bvar a =new A();});

看這段代碼特別在哪里呢?模塊A的定義,毫無特別之處,主要看模塊B。它在依賴關系里寫了一個字符串的A,然后在工廠方法的形參寫了一個真真切切的A類型。嗯?這個有些奇怪啊,你的A類型要怎么傳遞過來呢?其實是很簡單的,因為我們聲明了依賴項的數組,所以可以從依賴項,挨個得到對應的工廠方法,然后創建實例,傳進來。

use:function(name){varmodule= moduleMap[name];if(!module.entity){var args =[];for(var i=0; i<module.dependencies.length; i++){if(moduleMap[module.dependencies[i]].entity){args.push(moduleMap[module.dependencies[i]].entity);}else{args.push(this.use(module.dependencies[i]));}}module.entity =module.factory.apply(noop, args);}returnmodule.entity;}

?

我們可以看到,這里面遞歸獲取了依賴項,然后當作參數,用這個模塊的工廠方法來實例化了一下。這里我們多做了一個判斷,如果模塊工廠已經執行過,就緩存在entity屬性上,不需要每次都創建。以此類推,假如一個模塊有多個依賴項,也可以用類似的方式寫,毫無壓力:

thin.define("D",["A","B","C"],function(A, B, C){//module Dvar a =new A();var b =new B();var c =new C();});

注意了,D模塊的工廠,實參的名稱未必就要是跟依賴項一致,比如,以后我們代碼較多,可以給依賴項和模塊名稱加命名空間,可能變成這樣:

thin.define("foo.D",["foo.A","foo.B","foo.C"],function(A, B, C){//module Dvar a =new A();var b =new B();var c =new C();});

這段代碼仍然可以正常運行。我們來做另外一個測試,改變形參的順序:

thin.define("A",[],function(){return"a";});thin.define("B",[],function(){return"b";});thin.define("C",[],function(){return"c";});thin.define("D",["A","B","C"],function(B, A, C)
{return B + A + C;});var D = thin.use("D"); alert(D);

javascript三大框架,?

試試看,我們的D打出什么結果呢?結果是"abc",所以說,模塊工廠的實參只跟依賴項的定義有關,跟形參的順序無關。我們看到,在AngularJS里面,并非如此,實參的順序是跟形參一致的,這是怎么做到的呢?

我們先離開代碼,思考這么一個問題:如何得知函數的形參名數組?對,我們是可以用func.length得到形參個數,但無法得到每個形參的變量名,那怎么辦呢?

AngularJS使用了一種比較極端的辦法,分析了函數的字面量。眾所周知,在JavaScript中,任何對象都隱含了toString方法,對于一個函數來說,它的toString就是自己的實現代碼,包含函數簽名和注釋。下面我貼一下AngularJS里面的這部分代碼:

var FN_ARGS =/^function\s*[^\(]*\(\s*([^\)]*)\)/m;var FN_ARG_SPLIT =/,/;var FN_ARG =/^\s*(_?)(\S+?)\1\s*$/;var STRIP_COMMENTS =/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;function annotate(fn){var $inject,fnText,argDecl,last;if(typeof fn =='function'){if(!($inject = fn.$inject)){$inject =[];fnText = fn.toString().replace(STRIP_COMMENTS,'');argDecl = fnText.match(FN_ARGS);forEach(argDecl[1].split(FN_ARG_SPLIT),function(arg){arg.replace(FN_ARG,function(all, underscore, name){$inject.push(name);});});fn.$inject = $inject;}}elseif(isArray(fn)){last= fn.length -1;assertArgFn(fn[last],'fn');$inject = fn.slice(0,last);}else{assertArgFn(fn,'fn',true);}return $inject;}

?

可以看到,這個代碼也不長,重點是類型為function的那段,首先去除了注釋,然后獲取了形參列表字符串,這段正則能獲取到兩個結果,第一個是全函數的實現,第二個才是真正的形參列表,取第二個出來split,就得到了形參的字符串列表了,然后按照這個順序再去加載依賴模塊,就可以讓形參列表不對應于依賴項數組了。

AngularJS的這段代碼很強大,但是要損耗一些性能,考慮到我們的框架首要原則是簡單,甚至可以為此犧牲一些靈活性,我們不做這么復雜的事情了。

1.3 模塊的加載

到目前為止,我們可以把多個模塊都定義在一個文件中,然后手動引入這個js文件,但是如果一個頁面要引用很多個模塊,引入工作就變得比較麻煩,比如說,單頁應用程序(SPA)一般比較復雜,往往包含數以萬計行數的js代碼,這些代碼至少分布在幾十個甚至成百上千的模塊中,如果我們也在主界面就加載它們,載入時間會非常難以接受。但我們可以這樣看:主界面加載的時候,并不是用到了所有這些功能,能否先加載那些必須的,而把剩下的放在需要用的時候再去加載?

所以我們可以考慮萬能的AJAX,從服務端獲取一個js的內容,然后……,怎么辦,你當然說不能eval了,因為據說eval很evil啦,但是它evil在哪里呢?主要是破壞全局作用域啦,怎么怎么,但是如果這些文件里面都是按照我們規定的模塊格式寫,好像也沒有什么在全局作用域的……,好吧。

算了,我們還是用最簡單的方式了,就是動態創建script標簽,然后設置src,添加到document.head里,然后監聽它們的完成事件,做后續操作。真的很簡單,因為我們的框架不需要考慮那么多種情況,不需要AMD,不需要require那么麻煩,用這框架的人必須按照這里的原則寫。

所以,說真的我們這里沒那么復雜啦,要是你們想看更詳細原理的不如去看這個,解釋得比我好哎:http://coolshell.cn/articles/9749.html#jtss-tsina

[補一段,@Franky 大神指出了這篇文章中一些不符合現狀的地方,我把它也貼在這里,供讀者參考]

很多觀點都是 史蒂夫那本老書上的觀點. 和那時候同期產生的一些數據和資料...所以顯得不少東西說的太想當然了... 譬如script標簽的加載和執行會阻塞后面資源的加載和執行之類的.說的過于肯定了. 比如chrome7+就開始逐漸改進的 預加載機制 就分 head 里的資源, body里的資源 .兩個資源是否跨界三種情形. 不提這些瀏覽器. 我們看看ie10也同樣改進了 死循環10秒 這后面的圖片能被提前加載. 就更不用說其他A級瀏覽器的豐富的優化策略了. 所以還是建議博主, 別拿幾年前的老資料作為依據.尤其這些數據是用來說明更新速度像在賽跑一樣的各個瀏覽器了.

關于 defer , 似乎史蒂夫的老書上是這么說的么? 顯然沒有測試全非ie瀏覽器的各個版本.或者是他測試數據的時候ff某大版本的幾個beta子版本還沒出現?

其次是就你的加載器提到的預加載策略. 你有測過所有瀏覽器用object預加載可能涉及到的問題么(比如chrome,8,9的預加載的會話級別的資源類型緩存bug). 拋開這個問題不談,假設你預加載到一半,用戶再次觸發了加載.你覺得這種情況如果頻繁發生.是否合適? 你的預加載策略連script.onload狀態都無法測知,進一步優化的可能性就消失了. 考慮下為什么seajs 的 umd要設計成那個樣子?

最后吐槽下你的代碼. 有注意到你用 document.body.appendChild 來像dom 中插入腳本. 我的建議是 永遠不要這樣做.除非你可以無視ie6用戶.以及ie7缺失某些補丁的子版本.

你可以選擇body 可以.但請用insertBefore. 但在某些極端情況下.這仍然會發生問題. 最佳實踐是 head.insertBefore 向其第一個子節點插入.(你甚至無需檢測是否存在子節點. 這個api會在沒有子節點的時候,行為同appendChild). 而更加穩妥的情況是. 如果注入script. 發現document.head還沒有被構建時. 可以自己造一個. 這才是一個通用加載器要做到的程度...

我也偷懶了,只是貼一下代碼,順便解釋一下,界面把所依賴的js文件路徑放在數組里,然后挨個創建script標簽,src設置為路徑,添加到head中,監聽它們的完成事件。在這個完成時間里,我們要做這么一些事情:在fileMap里記錄當前js文件的路徑,防止以后重復加載,檢查列表中所有文件,看看是否全部加載完了,如果全加載好了,就執行回調。

require:function(pathArr, callback){for(var i =0; i < pathArr.length; i++){var path = pathArr[i];if(!fileMap[path]){var head = document.getElementsByTagName('head')[0];var node = document.createElement('script');node.type ='text/javascript';node.async ='true';node.src = path +'.js';node.onload =function(){fileMap[path]=true;head.removeChild(node);checkAllFiles();};head.appendChild(node);}}function checkAllFiles(){var allLoaded =true;for(var i =0; i < pathArr.length; i++){if(!fileMap[pathArr[i]]){allLoaded =false;break;}}if(allLoaded){callback();}}}

?

1.4 小結

到此為止,我們的簡易框架的模塊定義系統就完成了。完整的代碼如下:

(function(){var moduleMap ={};var fileMap ={};var noop =function(){};var thin ={define:function(name, dependencies, factory){if(!moduleMap[name]){varmodule={name: name,dependencies: dependencies,factory: factory};moduleMap[name]=module;}return moduleMap[name];},use:function(name){varmodule= moduleMap[name];if(!module.entity){var args =[];for(var i=0; i<module.dependencies.length; i++){if(moduleMap[module.dependencies[i]].entity){args.push(moduleMap[module.dependencies[i]].entity);}else{args.push(this.use(module.dependencies[i]));}}module.entity =module.factory.apply(noop, args);}returnmodule.entity;},require:function(pathArr, callback){for(var i =0; i < pathArr.length; i++){var path = pathArr[i];if(!fileMap[path]){var head = document.getElementsByTagName('head')[0];var node = document.createElement('script');node.type ='text/javascript';node.async ='true';node.src = path +'.js';node.onload =function(){fileMap[path]=true;head.removeChild(node);checkAllFiles();};head.appendChild(node);}}function checkAllFiles(){var allLoaded =true;for(var i =0; i < pathArr.length; i++){if(!fileMap[pathArr[i]]){allLoaded =false;break;}}if(allLoaded){callback();}}}};window.thin = thin;})();

?

測試代碼如下:

thin.define("constant.PI",[],function(){return3.14159;});thin.define("shape.Circle",["constant.PI"],function(pi){varCircle=function(r){this.r = r;};Circle.prototype ={area :function(){return pi *this.r *this.r;}}returnCircle;});thin.define("shape.Rectangle",[],function(){varRectangle=function(l, w){this.l = l;this.w = w;};Rectangle.prototype ={area:function(){returnthis.l *this.w;}};returnRectangle;});thin.define("ShapeTypes",["shape.Circle","shape.Rectangle"],function(Circle,Rectangle){return{CIRCLE:Circle,RECTANGLE:Rectangle};});thin.define("ShapeFactory",["ShapeTypes"],function(ShapeTypes){return{getShape:function(type){var shape;switch(type){case"CIRCLE":{shape =newShapeTypes[type](arguments[1]);break;}case"RECTANGLE":{shape =newShapeTypes[type](arguments[1], arguments[2]);break;}}return shape;}};});varShapeFactory= thin.use("ShapeFactory");
alert(ShapeFactory.getShape("CIRCLE",5).area());
alert(ShapeFactory.getShape("RECTANGLE",3,4).area());

?

在這個例子里定義了四個模塊,每個模塊只需要定義自己所直接依賴的模塊,其他的可以不必定義。也可以來這里看測試鏈接:http://xufei.github.io/thin/demo/demo.0.1.html

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://808629.com/118974.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 86后生记录生活 Inc. 保留所有权利。

底部版权信息