在PHP和JavaScript所有的不同之中,最核心的一點:PHP在本質上來說是一種順序執行的語言,而JavaScript則是事件敺動類型的。
不光是順序執行或者事件敺動,都是在創建語言的時候所做的一種假設實現。任何一種語言都不是憑空産生的。PHP和JavaScript,都是爲了某一個目的快速開發,然後在幾個月内創建投入使用的。
1994年,Rasmus Lerdorf發明了PHP來替代CGI腳本語言。CGI腳本一般是C(或者C++)程序,用於實現在瀏覽器請求特定URL時,將程序的響應作爲網頁返回。這些C程序時單線程的,並且從頭到尾按順序執行。Lerdorf開發了PHP作爲替代品。當瀏覽器請求一個特定URL時,不需要運行C程序,PHP腳本(頁面)會很快編譯並且執行,腳本的輸出會作爲響應的頁面。PHP是單線程C程序的一個即興的替代品,理所應當地採用了單線程的實現方式。
這些CGI腳本和早期的PHP頁面並沒有任何事件的概念,當時也沒有這種需求。只有兩個事件也許有意義並且有用處:(1)URL調用時間,(2)URL調用完成事件。URL調用事件通過運行PHP腳本處理,重點是,整個PHP腳本就是URL調用的處理。當PHP腳本執行完成,生成了所有的輸出,可以被看作是URL調用完成事件。爲了處理這種完成事件,需要在PHP頁面末尾添加其他處理代碼。所以PHP頁面可以看作是兩個事件之間的一大段無事件代碼,像三明治一樣。
所有這些關於事件的討論只是爲了解釋PHP的實現方式和出路。沒有任何多線程的概念或者:基本上,在做一件事情的時候需要在等待其他事情的完成,這種方式效率非常低。在PHP中,如果有一些任務需要耗費太長時間,你只能一直等待該任務完成,沒有其他更快的
向下執行的方式。
CGI腳本和最早的PHP頁面都是單線程的,是因爲它們都假定了Web服務器會進行多線程的處理。對單獨的 PHP 頁面來說多線程並沒有太大好處,但是如果一個Web服務器需要同時提供多個PHP頁面,可以採用多線程來處理。PHP頁面只是Web服務器中的獨立的操作,配合Web應用工作,但是這些頁面並不是Web服務器本身。
1995年Brendan Eich創建JavaScript時則與前一年出現的PHP有完全不同的目的和理念。設計JavaScript的目的是用於Netscape Navigator瀏覽器來處理時間。在瀏覽器中,用戶需要點擊按鈕和鏈接,填寫表格,在選擇列表中進行選擇,以及其他各種交互操作。相交於PH 毫無事件概念可言,JavaScript充滿的各種事件。早期的網站中,開發人員需要捕獲用戶與頁面交互過程中各種不同的事件,並爲每個事件關聯相應的處理來運行特定功能的代碼。
在等待一個用戶點擊某個按鈕的時候,瀏覽器不能停留在哪裡一直等待。事實上,頁面上有很多種控制(一直都有),這些控制的執行順序和如何操作都是不可預測的。這一點與 PHP完全不同,PHP的執行順序和數量都是完全可以預測的。
因爲JavaScript是基於事件處理設計的,所以在一開始就有了在不同任務間切換的概念:從根本來說,就是在等待一件事情完成的同時做一些其他的事情。在JavaScript中,事件處理可以用於處理用戶和瀏覽器的交互操作,也可以用於處理長時間操作的結束動作。很多操作都可以提供事件處理,開始,然後運行其它代碼。當操作結束的時候,即使此時還有其他的JavaScript代碼在運行,也會調用該操作完成的事件處理。
在2009年Ryan Dahl創建Node.js的時候,他覺得應該繼承JavaScript的傳統。他沒有根據單線程頁面的PHP建模,而是根據多任務多頁面處理的Web服務器來建立模型。每個PHP頁面代表一個獨立工作的但願,但是每個Node.js 腳本則可以代表整個Web服務器。事實上,創建一個Node.js腳本就可能提供任意數量的PHP頁面服務。
但是我們這裡的目標在於將PHP的功能轉換爲Node.js,並不是用Node.js創建一個Web服務器取代C程序,來提供 PHP 網頁。
我們不需要在Node.js中創建PHP的處理機制,而是將整個PHP功能轉換爲Node.js,通過JavaScript運行,不需要在兩種語言之間來回切換。PHP實現的所有功能都會由Node.js重新實現。
PHP和Node.js很多操作都很類似,包括一些相對複雜的操作,比如文档操作和數據庫訪問。有一點好處在於,PHP和JavaScript都是由C實現的,兩者的開發人員都沿用了C語言的很多形式和慣例。而且更好的在於,在 PHP和 JavaScript産生的十五年後,Ryan Dahl借助JavaScript開發Node.js時,整個行業已經形成了一套大槼模高度一致的標準操作,從簡單的到複雜的操作,不僅僅是API看起來是什麽樣子如何使用,甚至包括特定的API命名應該是怎麽樣的。感謝所有Node.js開發人員的貢獻,Node.js遵循了這些槼範。
3.1 線性
我們用文档處理作爲具體的例子,說明一下C、PHP和Node.js有很多相似。下面這段代碼是一個文档叫作fp.txt,用於輸出“Hello World!”:
#include<stdio.h> FILE * fp = fopen(“fp.txt”,”w”); fwrite(“Hello World!”,12,1,fp); fclose(fp);
這段代碼很簡單。C語言的fopen() API 函數創建了一個文档,fwrite() API 將内容寫入文档,而fclose()告訴函數庫我們的文档的操作完成了。Fp變量是一個文档指針,用於保存當前操作的文档便於C API使用。引用studio.h文档程序才可以訪問C文档系統的API。
以下是等價的PHP代碼:
<?php $fp = fopen('fp.txt',’w’); fwrite($fp,’Hello World!’); fclose($fp); ?>
可以看到,這與C的實現非常類似。PHP API函數使用相同的函數名,fopen()、fwrite()和fclose()。
再來看下Node.js的版本:
var fs = require('fs'); fs.open('fp.txt','w',0666,function(error,fp){ fs.write(fp,’Hello World!’,null,’utf-8’,function(){ fs.close(fp,function(error){ }); }); });
Node.js和之前兩種方式有類似也有不同。
Node.js需要使用Node.js fs模塊來進行文档操作。與C類似但是和PHP不同,Node.js也需要聲明來訪問模塊的文档處理函數。
Node.js API函數本身非常相似,但是命名稍有不同。在C和PHP中,打開文档都是用fopen()函數,但是在Node.js中,則調用了fs.oepn()。類似的,C和PHP中的fwrite()和fclose(),對英語Node.js中的fs.write()和fs.close()。
如果你用PHP的示例代碼,並用Node.js API替換PHP API,會創建出一種PHP/Node.js混合代碼。
$fp = fs.open('fp.txt',’w’); fs.write($fp,’Hello World!’); fs.close($fp);
其他一些小的不同很容易修改,比如去掉美元符號,添加Node.js需要的其他變量而PHP沒有的。修改完成之後,PHP/Node.js混合代碼看起來更像Node.js而不是PHP:
fp = fs.open('fp.txt',’w’,0666); fs.write(fp,’Hello World!’,null,’utf-8’); fs.close(fp);
還剩一個步驟。在Node.js中,fp變量並不是Node.js API函數fs.open()的返回值。它會作爲參數傳遞給fs.open()最後一個參數回調函數。我們來修改一個調用,將fp變量放在正確的位置上:
fp = fs.open('fp.txt',’w’,0666,function(error,fp){ }); fs.write(fp,’Hello World!’,null,’utf-8’); fs.close(fp);
Node.js fs.write() API函數無法再訪問fp變量,因爲它不在回調函數的作用域内。即使我們可以通過某種方式提供訪問權限,Node.js fs.write()函數可能會過早調用,甚至在Node.js fs.open()調用之前,fp變量還沒有從open函數中獲取正確的值。
這段簡單的代碼突出了PHP到Node.js轉換中最大的挑戰。挑戰在於,需要找到某種方式將不同源頭的PHP 和Node.js整合到一起。單線程序列化的PHP需要適應多任務非序列化的JavaScript。PHP的無事件特性需要適應Node.js的事件,這樣JavaScript代碼才能工作。
PHP是由順序執行的API組成的,也稱作阻塞型。每條語句都是按順序執行,下一條語句一定會等待上一條語句執行完才會開始執行。當調用PHP fopen()時,返回值包含文档指針,因爲只有當文档真正打開的時候fopne()才回返回結果。如果硬盤太慢打開文档需要很長時間,API函數會等待,知道操作完成。
而Node.js更多的則是非阻塞型的API。每條語句依然是按順序執行,但是當語句執行完畢的時候會將一個函數作爲參數傳給調用函數。順序上來講的下一條語句可能在這之前或者之後開始。
作爲轉換,至少需要講阻塞型的PHP轉換爲非阻塞型的Node.js,下一條執行的語句並不是Node.js fs.open() API 調用之後的那一條,而是作爲最後一個參數的回調函數的第一條語句。當我們進行轉換時,我們需要將PHP代碼中順序執行的下一條代碼放在回調函數裡。
修改之後,代碼如下所示:
fs.open('fp.txt',’w’,0666,function(error,fp){ fs.write(fp,’Hello World!’,null,’utf-8’); }); fs.close(fp);
現在,fs.write()函數調用在回調函數内部。fs.write()函數直到fs.open()函數執行完畢後才會調用執行。fs.write()函數現在回調函作用域内,所以可以獲取fp變量的值,而且我們按照API設計的方式正確使用,所以在fs.write()函數調用的時候可以確保fp變量的值是正確的。
很明顯,這還沒結束。fs.write()函數有自己的回調函數,fs.close()函數無法獲取fp變量的值,就像fs.write()函數在放進回調函數之前的狀況一樣。解決方案與之前一樣,對fs.write()函數添加回調函數,然後將fs.close()放在回調函數内:
fs.open('fp.txt',’w’,0666,function(error,fp){ fs.write(fp,’Hello World!’,null,’utf-8’,function(){ fs.close(fp); }); });
最後,爲fs.close()添加一個回調,調用自己來結束操作:
fs.open('fp.txt',’w’,0666,function(error,fp){ fs.write(fp,’Hello World!’,null,’utf-8’,function(){ fs.close(fp,function(error){ }); }); });
原始的PHP代碼中是三個阻塞型PHP API的調用。移植到Node.js之後,這三個阻塞型的調用轉換爲三個非阻塞型的Node.js調用。
這總讓我想起嵌套:Node.js fs.open()函數調用擴展包含fs.write()函數調用,fs.write()函數又擴展包含fs.close()函數調用,fs.close()函數還可能進行擴展包含更多的代碼。
最簡單的情況下,一段順序執行的PHP語句可以通過爲等價的Node.js API函數添加回調函數的方式轉換爲Node.js,只需要拷貝其他的代碼到回調中就可以了。
從這個角度來看,我們可以總結一個我稱之爲“轉換清單”的内容。轉換清單是一系列半機械化的步驟,可以用於將PHP代碼轉換爲一種更能兼容Node.js的代碼。
注意到我寫的是“更兼容”。轉換清單並不需要將可運行的PHP代碼直接轉換爲可運行的Node.js代碼。轉換清單可能在一行代碼中只定位一個問題,然後將半轉換的代碼作爲PHP/Node.js的混合形式。
轉換清單還有其他的一些重要的特性。
轉換清單需要機械化或者半機械化執行,這樣可以快速轉換PHP代碼而無需深入了解相關的代碼。轉換清單不應該遺漏任何信息或者代碼,不能在之後需要重寫代碼。轉換清單到保持代碼的可讀性,正確的順序(即使不能工作),不應該使代碼存在語法問題,比如缺少小括號或者花括號,或者把代碼放在沒有意義的地方。
文档處理的API並不是一個轉換清單,只是用於解釋轉換中遇到的問題。如果用它作爲一個轉換清單,代碼的順序是混亂的。一下是一個基於我們所學過内容的轉換清單。
原始代碼如下:
$fp = fopen('fp.txt',’w’); fwrite($fp,’Hello World!’); fclose($fp);
將PHP fopen()轉換爲Node.js的fs.open()調用,修改調用函數的參數,將其餘部分剪切到fs.open()函數的回調函數中,如下:
fs.open('fp.txt',’w’,0666,function(error,fp){ fwrite(fp,’Hello World!’); fclose(fp); });
注意到fopen()已經轉換爲Node.js的fs.open(),但是回調函數中還是PHP代碼。將PHP代碼放在回調中保证了代碼的執行順序。
我們採用同樣的步驟轉換PHP的fwrite()函數。步驟是,轉換fwrite()爲Node.js的fs.write(),修改調用函數的參數,剪切剩下的代碼粘貼到回調函數中:
fs.open('fp.txt',’w’,0666,function(error,fp){ fs.write(fp,’Hello World!’,null,’utf-8’,function(){ fclose(fp); }); });
轉換fclose()函數使用同樣的方式,但是在fclose()操作之後沒有其他操作了,所以創建一個空的回調函數:
fs.open('fp.txt',’w’,0666,function(error,fp){ fs.write(fp,’Hello World!’,null,’utf-8’,function(){ fs.close(fp,function(error){ }); }); });
這是一個非常簡單而且有效的轉換清單。這本來可以作爲本章的結束,但是:這個轉換清單在稍微複雜一點的情況下很快就失敗了。
這個轉換清單可以工作是因爲PHP是線性的。在沒有類似if條件語句或者for
While循環的情況下代碼是一行行按順序執行的。我稱之爲“線性”,是因爲可以想象出代碼是從一行直線運行到下一行,沒有交叉或者重組。
如果沒有任何條件語句的存在,我們可以稱之爲PHP代碼是完全線性的。一下就是一段完全線性的PHP代碼:
$msg = 'start'; $fp = fopen('fp.txt',’w’); $msg = 'opened'; fwrite($fp,’Hello World!’); $msg = 'written'; fclose($fp); $msg = 'closed';
轉換清單對對線性PHP代碼非常有效,所以很容易將上述PHP代碼轉換爲以下代碼;
var msg = 'start'; fs.open('fp.txt',’w’,0666,function(error,fp){ msg = 'opened'; fs.write(fp,’Hello World!’,null,’utf-8’,function(){ msg = 'written'; fs.close(fp,function(error){ mag = 'closed'; }); }); });
但是,如果PHP代碼包含條件語句,PHP代碼也可能是線性的,也就是說條件語句不會影響該轉換清單的實施過程。爲了具有線性特征,任何一個阻塞型的PHP調用轉換成非阻塞型Node.js調用,從調用開始到代碼結束都是線性的。
如果條件語句是獨立的並且與阻塞型的PHP 調用分離,那麽可以收PHP代碼是具有線性特性的。例如:
$msg = 'start'; $fp = fopen('fp.txt',’w’); if($fp !== null){ $msg = 'opened'; } fwrite($fp,’Hello World!’); fclose($fp);
if語句是獨立的,並且在該語句之前、之中、之後的代碼都是線性的。根據轉換清單會生成以下的Node.js代碼:
var msg = 'start'; fs.open('fp.txt',’w’,0666,function(error,fp){ if(fp !== null){ msg = 'opeed'; } fs.write(fp,’Hello World!’,null,’utf-8’,function(){ fs.close(fp,function(error){ }); }); });
對於PHP代碼來說,即使包含需要轉換爲Node.js的阻塞型PHP調用,如果所有的條件判斷都保持線性,本質上那來說該代碼也是線性的。例如:
$fp = fopen('fp.txt',’w’); if($fp !== null){ fwrite($fp,’Hello World!’); fclose($fp); }
上述PHP代碼很容易轉換爲以下Node.js代碼:
fs.open('fp.txt',’w’,0666,function(error,fp){ if(fp !== null){ fs.write(fp,’Hello World!’,null,’utf-8’,function(){ fs.close(fp,function(error){ }); }); } });
並不是所有的代碼都是線性的或者具有線性特征。有一些PHP代碼就是非線性的。當代碼交叉執行並且嘗試用一種打破之前轉換清單的方式重組時代碼就是非線性的。當使用轉換清單時,所有剩餘代碼都會被放置在Node.js API調用的回調函數之内。但是對非線性PHP代碼,新的Node.js代碼在回調函數外部也需要完全形同的代碼,這樣才能覆蓋不使用非阻塞型的Node.js API的路徑。在下述例子中可以更清楚地看到這點。
當一個阻塞型的PHP API調用在if語句模塊中並且if語句模塊結束後還有其他語句,PHP代碼一定是非線性的。PHP的fwrite()調用可以使代碼稱爲非線性的:
$fp = fopen('fp.txt',’w’); if($msg == ‘say’){ fwrite($fp,’Hello World!’); } $msg = 'done';
如果生搬硬套轉換清單,Node.js的代碼沒辦法編譯甚至語法都是錯誤的!以下這段代碼失效是因爲只適用於線性PHP代碼的轉換清單應用在了非線性代碼上:
fs.open('fp.txt',’w’,0666,function(error,fp){ if(msg == 'say'){ fs.write(fp,’Hello World!’,null,’utf-8’,function(){ }//opps! msg = 'done';//oops! }); } });
難點在於,最後一條語句,msg變量被設置爲‘done’。這條語句既不在if語句模塊之内也不屬於fs.write(),將PHP代碼從具有線性特征轉換爲了非線性。看起來這條語句很簡單,並且與其他語句無關。但是最後一條語句將不同路徑連結在了一起,它要求不管是執行if語句内部或者跳過if語句的代碼都需要執行相同的一行代碼,阻塞型的PHP API在一條路徑中有回調函數而另一條路徑中沒有。如果刪除最後一行語句,PHP代碼就成爲線性的了。並且,如果刪除阻塞型的API調用或者換成其他沒有回調函數的代碼,PHP代碼也會稱爲線性的。
直到現在,“代碼的結束”仍然是個模糊的概念。
代碼的結束是指PHP代碼結束運行,PHP頁面完成,Web服務器重新掌握控制權。每個PHP頁面在運行時都有有限的生命周期,它從URL調用的偽事件運行到URL調用結束。最後的代碼是在URL釣魚能夠結束的偽事件觸發時執行的。
你可以看到,在你逆向工作的時候轉換清單非常有效。如果你在PHP頁面結束時開始,你可以逆向追蹤,找到結束點,對每個結束點之前的先行部分按照轉換清單操作。頁面的入口和線性與轉換清單無關。
比如if語句、for語句和while循環,即可以具有線性特征也可以是非線性的。一個有線性特征的語句可以是如下這種:
$total = 0; $fp = fopen('fp.txt',’w’); for($i=0; $i < 10; ++$i){ $total +=$i; } fwrite($fp,’Total = ’.$total);
for語句與此無關,並且不影響轉換清單的過程。一下Node.js可以正常工作:
var total = 0; fs.open('fp.txt',’w’,0666,function(error,fp){ for(var 1=0; I < 10; ++$i){ total += i; } fs.write(fp,’Total = ’+total,null,’utf-8’,function(){ }); })
for語句也可以是非線性的。轉換清單對以下PHP代碼無效:
$fp = fopen('fp.txt',’w’); for($i=0; $i < 3; ++$i){ fwrite($fp,’Hello World!’); }
如果你非要使用轉換清單,Node.js代碼會報語法錯誤,無法修複:
fs.open('fp.txt',’w’,0666,function(error,fp){ for(var 1=0; I < 3; ++$i){ fs.write(fp,’Hello World!’,null,’utf-8’,function(){ }//opps! }); });
經過一些練習,你可以訓練你的眼睛很輕松地是被嗲嗎的線性性,以此確定是否可以使用轉換清單以及轉換後的Node.js代碼是什麽樣子。
在任何你想要轉換的PHP頁面中,你需要在頁面的退出點只愛錢找到所有的線性代碼,你想追溯代碼執行路徑知道你發現非線性的代碼。