Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
數(shù)適配器機(jī)制不僅復(fù)雜,而且成本很高。
本文最初發(fā)表于 v8.dev(Faster JavaScript calls),基于 CC 3.0 協(xié)議分享,由 InfoQ 翻譯并發(fā)布。
JavaScript 允許使用與預(yù)期形式參數(shù)數(shù)量不同的實(shí)際參數(shù)來(lái)調(diào)用一個(gè)函數(shù),也就是傳遞的實(shí)參可以少于或者多于聲明的形參數(shù)量。前者稱(chēng)為申請(qǐng)不足(under-application),后者稱(chēng)為申請(qǐng)過(guò)度(over-application)。
在申請(qǐng)不足的情況下,剩余形式參數(shù)會(huì)被分配 undefined 值。在申請(qǐng)過(guò)度的情況下,可以使用 rest 參數(shù)和 arguments 屬性訪(fǎng)問(wèn)剩余實(shí)參,或者如果它們是多余的可以直接忽略。如今,許多 Web/Node.js 框架都使用這個(gè) JS 特性來(lái)接受可選形參,并創(chuàng)建更靈活的 API。
直到最近,V8 都有一種專(zhuān)門(mén)的機(jī)制來(lái)處理參數(shù)大小不匹配的情況:這種機(jī)制叫做參數(shù)適配器框架。不幸的是,參數(shù)適配是有性能成本的,但在現(xiàn)代的前端和中間件框架中這種成本往往是必須的。但事實(shí)證明,我們可以通過(guò)一個(gè)巧妙的技巧來(lái)拿掉這個(gè)多余的框架,簡(jiǎn)化 V8 代碼庫(kù)并消除幾乎所有的開(kāi)銷(xiāo)。
我們可以通過(guò)一個(gè)微型基準(zhǔn)測(cè)試來(lái)計(jì)算移除參數(shù)適配器框架可以獲得的性能收益。
console.time();
function f(x, y, z) {}
for (let i=0; i < N; i++) {
f(1, 2, 3, 4, 5);
}
console.timeEnd();
移除參數(shù)適配器框架的性能收益,通過(guò)一個(gè)微基準(zhǔn)測(cè)試來(lái)得出。
上圖顯示,在無(wú) JIT 模式(Ignition)下運(yùn)行時(shí),開(kāi)銷(xiāo)消失,并且性能提高了 11.2%。使用 TurboFan 時(shí),我們的速度提高了 40%。
這個(gè)微基準(zhǔn)測(cè)試自然是為了最大程度地展現(xiàn)參數(shù)適配器框架的影響而設(shè)計(jì)的。但是,我們也在許多基準(zhǔn)測(cè)試中看到了顯著的改進(jìn),例如我們內(nèi)部的 JSTests/Array 基準(zhǔn)測(cè)試(7%)和 Octane2(Richards 子項(xiàng)為 4.6%,EarleyBoyer 為 6.1%)。
這個(gè)項(xiàng)目的重點(diǎn)是移除參數(shù)適配器框架,這個(gè)框架在訪(fǎng)問(wèn)棧中被調(diào)用者的參數(shù)時(shí)為其提供了一個(gè)一致的接口。為此,我們需要反轉(zhuǎn)棧中的參數(shù),并在被調(diào)用者框架中添加一個(gè)包含實(shí)際參數(shù)計(jì)數(shù)的新插槽。下圖顯示了更改前后的典型框架示例。
移除參數(shù)適配器框架之前和之后的典型 JavaScript 棧框架。
為了講清楚我們?nèi)绾渭涌煺{(diào)用,首先我們來(lái)看看 V8 如何執(zhí)行一個(gè)調(diào)用,以及參數(shù)適配器框架如何工作。
當(dāng)我們?cè)?JS 中調(diào)用一個(gè)函數(shù)調(diào)用時(shí),V8 內(nèi)部會(huì)發(fā)生什么呢?用以下 JS 腳本為例:
function add42(x) {
return x + 42;
}
add42(3);
在函數(shù)調(diào)用期間 V8 內(nèi)部的執(zhí)行流程。
V8 是一個(gè)多層 VM。它的第一層稱(chēng)為 Ignition,是一個(gè)具有累加器寄存器的字節(jié)碼棧機(jī)。V8 首先會(huì)將代碼編譯為 Ignition 字節(jié)碼。上面的調(diào)用被編譯為以下內(nèi)容:
0d LdaUndefined ;; Load undefined into the accumulator
26 f9 Star r2 ;; Store it in register r2
13 01 00 LdaGlobal [1] ;; Load global pointed by const 1 (add42)
26 fa Star r1 ;; Store it in register r1
0c 03 LdaSmi [3] ;; Load small integer 3 into the accumulator
26 f8 Star r3 ;; Store it in register r3
5f fa f9 02 CallNoFeedback r1, r2-r3 ;; Invoke call
調(diào)用的第一個(gè)參數(shù)通常稱(chēng)為接收器(receiver)。接收器是 JSFunction 中的 this 對(duì)象,并且每個(gè) JS 函數(shù)調(diào)用都必須有一個(gè) this。CallNoFeedback 的字節(jié)碼處理器需要使用寄存器列表 r2-r3 中的參數(shù)來(lái)調(diào)用對(duì)象 r1。
在深入研究字節(jié)碼處理器之前,請(qǐng)先注意寄存器在字節(jié)碼中的編碼方式。它們是負(fù)的單字節(jié)整數(shù):r1 編碼為 fa,r2 編碼為 f9,r3 編碼為 f8。我們可以將任何寄存器 ri 稱(chēng)為 fb - i,實(shí)際上正如我們所見(jiàn),正確的編碼是- 2 - kFixedFrameHeaderSize - i。寄存器列表使用第一個(gè)寄存器和列表的大小來(lái)編碼,因此 r2-r3 為 f9 02。
Ignition 中有許多字節(jié)碼調(diào)用處理器。可以在此處查看它們的列表。它們彼此之間略有不同。有些字節(jié)碼針對(duì) undefined 的接收器調(diào)用、屬性調(diào)用、具有固定數(shù)量的參數(shù)調(diào)用或通用調(diào)用進(jìn)行了優(yōu)化。在這里我們分析 CallNoFeedback,這是一個(gè)通用調(diào)用,在該調(diào)用中我們不會(huì)積累執(zhí)行過(guò)程中的反饋。
這個(gè)字節(jié)碼的處理器非常簡(jiǎn)單。它是用 CodeStubAssembler 編寫(xiě)的,你可以在此處查看。本質(zhì)上,它會(huì)尾調(diào)用一個(gè)架構(gòu)依賴(lài)的內(nèi)置 InterpreterPushArgsThenCall。
這個(gè)內(nèi)置方法實(shí)際上是將返回地址彈出到一個(gè)臨時(shí)寄存器中,壓入所有參數(shù)(包括接收器),然后壓回該返回地址。此時(shí),我們不知道被調(diào)用者是否是可調(diào)用對(duì)象,也不知道被調(diào)用者期望多少個(gè)參數(shù),也就是它的形式參數(shù)數(shù)量。
內(nèi)置 InterpreterPushArgsThenCall 執(zhí)行后的框架狀態(tài)。
最終,執(zhí)行會(huì)尾調(diào)用到內(nèi)置的 Call。它會(huì)在那里檢查目標(biāo)是否是適當(dāng)?shù)暮瘮?shù)、構(gòu)造器或任何可調(diào)用對(duì)象。它還會(huì)讀取共享 shared function info 結(jié)構(gòu)以獲得其形式參數(shù)計(jì)數(shù)。
如果被調(diào)用者是一個(gè)函數(shù)對(duì)象,它將對(duì)內(nèi)置的 CallFunction 進(jìn)行尾部調(diào)用,并在其中進(jìn)行一系列檢查,包括是否有 undefined 對(duì)象作為接收器。如果我們有一個(gè) undefined 或 null 對(duì)象作為接收器,則應(yīng)根據(jù) ECMA 規(guī)范對(duì)其修補(bǔ),以引用全局代理對(duì)象。
執(zhí)行隨后會(huì)對(duì)內(nèi)置的 InvokeFunctionCode 進(jìn)行尾調(diào)用。在沒(méi)有參數(shù)不匹配的情況下,InvokeFunctionCode 只會(huì)調(diào)用被調(diào)用對(duì)象中字段 Code 所指向的內(nèi)容。這可以是一個(gè)優(yōu)化函數(shù),也可以是內(nèi)置的 InterpreterEntryTrampoline。
如果我們假設(shè)要調(diào)用的函數(shù)尚未優(yōu)化,則 Ignition trampoline 將設(shè)置一個(gè) IntepreterFrame。你可以在此處查看V8 中框架類(lèi)型的簡(jiǎn)短摘要。
接下來(lái)發(fā)生的事情就不用多談了,我們可以看一個(gè)被調(diào)用者執(zhí)行期間的解釋器框架快照。
我們看到框架中有固定數(shù)量的插槽:返回地址、前一個(gè)框架指針、上下文、我們正在執(zhí)行的當(dāng)前函數(shù)對(duì)象、該函數(shù)的字節(jié)碼數(shù)組以及我們當(dāng)前正在執(zhí)行的字節(jié)碼偏移量。最后,我們有一個(gè)專(zhuān)用于此函數(shù)的寄存器列表(你可以將它們視為函數(shù)局部變量)。add42 函數(shù)實(shí)際上沒(méi)有任何寄存器,但是調(diào)用者具有類(lèi)似的框架,其中包含 3 個(gè)寄存器。
如預(yù)期的那樣,add42 是一個(gè)簡(jiǎn)單的函數(shù):
25 02 Ldar a0 ;; Load the first argument to the accumulator
40 2a 00 AddSmi [42] ;; Add 42 to it
ab Return ;; Return the accumulator
請(qǐng)注意我們?cè)?Ldar(Load Accumulator Register)字節(jié)碼中編碼參數(shù)的方式:參數(shù) 1(a0)用數(shù)字 02 編碼。實(shí)際上,任何參數(shù)的編碼規(guī)則都是[ai]=2 + parameter_count - i - 1,接收器[this]=2 + parameter_count,或者在本例中[this]=3。此處的參數(shù)計(jì)數(shù)不包括接收器。
現(xiàn)在我們就能理解為什么用這種方式對(duì)寄存器和參數(shù)進(jìn)行編碼。它們只是表示一個(gè)框架指針的偏移量。然后,我們可以用相同的方式處理參數(shù)/寄存器的加載和存儲(chǔ)。框架指針的最后一個(gè)參數(shù)偏移量為 2(先前的框架指針和返回地址)。這就解釋了編碼中的 2。解釋器框架的固定部分是 6 個(gè)插槽(4 個(gè)來(lái)自框架指針),因此寄存器零位于偏移量-5 處,也就是 fb,寄存器 1 位于 fa 處。很聰明是吧?
但請(qǐng)注意,為了能夠訪(fǎng)問(wèn)參數(shù),該函數(shù)必須知道棧中有多少個(gè)參數(shù)!無(wú)論有多少參數(shù),索引 2 都指向最后一個(gè)參數(shù)!
Return 的字節(jié)碼處理器將調(diào)用內(nèi)置的 LeaveInterpreterFrame 來(lái)完成。該內(nèi)置函數(shù)本質(zhì)上是從框架中讀取函數(shù)對(duì)象以獲取參數(shù)計(jì)數(shù),彈出當(dāng)前框架,恢復(fù)框架指針,將返回地址保存在一個(gè)暫存器中,根據(jù)參數(shù)計(jì)數(shù)彈出參數(shù)并跳轉(zhuǎn)到暫存器中的地址。
這套流程很棒!但是,當(dāng)我們調(diào)用一個(gè)實(shí)參數(shù)量少于或多于其形參數(shù)量的函數(shù)時(shí),會(huì)發(fā)生什么呢?這個(gè)聰明的參數(shù)/寄存器訪(fǎng)問(wèn)流程將失敗,我們?cè)撊绾卧谡{(diào)用結(jié)束時(shí)清理參數(shù)?
現(xiàn)在,我們使用更少或更多的實(shí)參來(lái)調(diào)用 add42:
add42();
add42(1, 2, 3);
JS 開(kāi)發(fā)人員會(huì)知道,在第一種情況下,x 將被分配 undefined,并且該函數(shù)將返回 undefined + 42=NaN。在第二種情況下,x 將被分配 1,函數(shù)將返回 43,其余參數(shù)將被忽略。請(qǐng)注意,調(diào)用者不知道是否會(huì)發(fā)生這種情況。即使調(diào)用者檢查了參數(shù)計(jì)數(shù),被調(diào)用者也可以使用 rest 參數(shù)或 arguments 對(duì)象訪(fǎng)問(wèn)其他所有參數(shù)。實(shí)際上,在 sloppy 模式下甚至可以在 add42 外部訪(fǎng)問(wèn) arguments 對(duì)象。
如果我們執(zhí)行與之前相同的步驟,則將首先調(diào)用內(nèi)置的 InterpreterPushArgsThenCall。它將像這樣將參數(shù)推入棧:
內(nèi)置 InterpreterPushArgsThenCall 執(zhí)行后的框架狀態(tài)。
繼續(xù)與以前相同的過(guò)程,我們檢查被調(diào)用者是否為函數(shù)對(duì)象,獲取其參數(shù)計(jì)數(shù),并將接收器補(bǔ)到全局代理。最終,我們到達(dá)了 InvokeFunctionCode。
在這里我們不會(huì)跳轉(zhuǎn)到被調(diào)用者對(duì)象中的 Code。我們檢查參數(shù)大小和參數(shù)計(jì)數(shù)之間是否存在不匹配,然后跳轉(zhuǎn)到 ArgumentsAdaptorTrampoline。
在這個(gè)內(nèi)置組件中,我們構(gòu)建了一個(gè)額外的框架,也就是臭名昭著的參數(shù)適配器框架。這里我不會(huì)解釋內(nèi)置組件內(nèi)部發(fā)生了什么,只會(huì)向你展示內(nèi)置組件調(diào)用被調(diào)用者的 Code 之前的框架狀態(tài)。請(qǐng)注意,這是一個(gè)正確的 x64 call(不是 jmp),在被調(diào)用者執(zhí)行之后,我們將返回到 ArgumentsAdaptorTrampoline。這與進(jìn)行尾調(diào)用的 InvokeFunctionCode 正好相反。
我們創(chuàng)建了另一個(gè)框架,該框架復(fù)制了所有必需的參數(shù),以便在被調(diào)用者框架頂部精確地包含參數(shù)的形參計(jì)數(shù)。它創(chuàng)建了一個(gè)被調(diào)用者函數(shù)的接口,因此后者無(wú)需知道參數(shù)數(shù)量。被調(diào)用者將始終能夠使用與以前相同的計(jì)算結(jié)果來(lái)訪(fǎng)問(wèn)其參數(shù),即[ai]=2 + parameter_count - i - 1。
V8 具有一些特殊的內(nèi)置函數(shù),它們?cè)谛枰ㄟ^(guò) rest 參數(shù)或 arguments 對(duì)象訪(fǎng)問(wèn)其余參數(shù)時(shí)能夠理解適配器框架。它們始終需要檢查被調(diào)用者框架頂部的適配器框架類(lèi)型,然后采取相應(yīng)措施。
如你所見(jiàn),我們解決了參數(shù)/寄存器訪(fǎng)問(wèn)問(wèn)題,但是卻添加了很多復(fù)雜性。需要訪(fǎng)問(wèn)所有參數(shù)的內(nèi)置組件都需要了解并檢查適配器框架的存在。不僅如此,我們還需要注意不要訪(fǎng)問(wèn)過(guò)時(shí)的舊數(shù)據(jù)。考慮對(duì) add42 的以下更改:
function add42(x) {
x +=42;
return x;
}
現(xiàn)在,字節(jié)碼數(shù)組為:
25 02 Ldar a0 ;; Load the first argument to the accumulator
40 2a 00 AddSmi [42] ;; Add 42 to it
26 02 Star a0 ;; Store accumulator in the first argument slot
ab Return ;; Return the accumulator
如你所見(jiàn),我們現(xiàn)在修改 a0。因此,在調(diào)用 add42(1, 2, 3)的情況下,參數(shù)適配器框架中的插槽將被修改,但調(diào)用者框架仍將包含數(shù)字 1。我們需要注意,參數(shù)對(duì)象正在訪(fǎng)問(wèn)修改后的值,而不是舊值。
從函數(shù)返回很簡(jiǎn)單,只是會(huì)很慢。還記得 LeaveInterpreterFrame 做什么嗎?它基本上會(huì)彈出被調(diào)用者框架和參數(shù),直到到達(dá)最大形參計(jì)數(shù)為止。因此,當(dāng)我們返回參數(shù)適配器存根時(shí),棧如下所示:
被調(diào)用者 add42 執(zhí)行之后的框架狀態(tài)。
我們需要彈出參數(shù)數(shù)量,彈出適配器框架,根據(jù)實(shí)際參數(shù)計(jì)數(shù)彈出所有參數(shù),然后返回到調(diào)用者執(zhí)行。
簡(jiǎn)單總結(jié):參數(shù)適配器機(jī)制不僅復(fù)雜,而且成本很高。
我們可以做得更好嗎?我們可以移除適配器框架嗎?事實(shí)證明我們確實(shí)可以。
我們回顧一下之前的需求:
如果要消除多余的框架,則需要確定將參數(shù)放在何處:在被調(diào)用者框架中還是在調(diào)用者框架中。
假設(shè)我們將參數(shù)放在被調(diào)用者框架中。這似乎是一個(gè)好主意,因?yàn)闊o(wú)論何時(shí)彈出框架,我們都會(huì)一次彈出所有參數(shù)!
參數(shù)必須位于保存的框架指針和框架末尾之間的某個(gè)位置。這就要求框架的大小不會(huì)被靜態(tài)地知曉。訪(fǎng)問(wèn)參數(shù)仍然很容易,它就是一個(gè)來(lái)自框架指針的簡(jiǎn)單偏移量。但現(xiàn)在訪(fǎng)問(wèn)寄存器要復(fù)雜得多,因?yàn)樗鼤?huì)根據(jù)參數(shù)的數(shù)量而變化。
棧指針總是指向最后一個(gè)寄存器,然后我們可以使用它來(lái)訪(fǎng)問(wèn)寄存器而無(wú)需知道參數(shù)計(jì)數(shù)。這種方法可能行得通,但它有一個(gè)關(guān)鍵缺陷。它需要復(fù)制所有可以訪(fǎng)問(wèn)寄存器和參數(shù)的字節(jié)碼。我們將需要 LdaArgument 和 LdaRegister,而不是簡(jiǎn)單的 Ldar。當(dāng)然,我們還可以檢查我們是否正在訪(fǎng)問(wèn)一個(gè)參數(shù)或寄存器(正或負(fù)偏移量),但這將需要檢查每個(gè)參數(shù)和寄存器訪(fǎng)問(wèn)。顯然這種方法太昂貴了!
好的,如果我們?cè)谡{(diào)用者框架中放參數(shù)呢?
記住如何計(jì)算一個(gè)框架中參數(shù) i 的偏移量:[ai]=2 + parameter_count - i - 1。如果我們擁有所有參數(shù)(不僅是形式參數(shù)),則偏移量將為[ai]=2 + parameter_count - i - 1.也就是說(shuō),對(duì)于每個(gè)參數(shù)訪(fǎng)問(wèn),我們都需要加載實(shí)際的參數(shù)計(jì)數(shù)。
但如果我們反轉(zhuǎn)參數(shù)會(huì)發(fā)生什么呢?現(xiàn)在可以簡(jiǎn)單地將偏移量計(jì)算為[ai]=2 + i。我們不需要知道棧中有多少個(gè)參數(shù),但如果我們可以保證棧中至少有形參計(jì)數(shù)那么多的參數(shù),那么我們就能一直使用這種方案來(lái)計(jì)算偏移量。
換句話(huà)說(shuō),壓入棧的參數(shù)數(shù)量將始終是參數(shù)數(shù)量和形參數(shù)量之間的最大值,并且在需要時(shí)使用 undefined 對(duì)象進(jìn)行填充。
這還有另一個(gè)好處!對(duì)于任何 JS 函數(shù),接收器始終位于相同的偏移量處,就在返回地址的正上方:[this]=2。
對(duì)于我們的第 1 和第 4 條要求,這是一個(gè)干凈的解決方案。另外兩個(gè)要求又如何呢?我們?nèi)绾螛?gòu)造 rest 參數(shù)和 arguments 對(duì)象?返回調(diào)用者時(shí)如何清理?xiàng)V械膮?shù)?為此,我們?nèi)鄙俚闹皇菂?shù)計(jì)數(shù)而已。我們需要將其保存在某個(gè)地方。只要可以輕松訪(fǎng)問(wèn)此信息即可,具體怎么做沒(méi)那么多限制。兩種基本選項(xiàng)分別是:將其推送到調(diào)用者框架中的接收者之后,或被調(diào)用者框架中的固定標(biāo)頭部分。我們實(shí)現(xiàn)了后者,因?yàn)樗喜⒘?Interpreter 和 Optimized 框架的固定標(biāo)頭部分。
如果在 V8 v8.9 中運(yùn)行前面的示例,則在 InterpreterArgsThenPush 之后將看到以下棧(請(qǐng)注意,現(xiàn)在參數(shù)已反轉(zhuǎn)):
內(nèi)置 InterpreterPushArgsThenCall 執(zhí)行后的框架狀態(tài)。
所有執(zhí)行都遵循類(lèi)似的路徑,直到到達(dá) InvokeFunctionCode。在這里,我們?cè)谏暾?qǐng)不足的情況下處理參數(shù),根據(jù)需要推送盡可能多的 undefined 對(duì)象。請(qǐng)注意,在申請(qǐng)過(guò)度的情況下,我們不會(huì)進(jìn)行任何更改。最后,我們通過(guò)一個(gè)寄存器將參數(shù)數(shù)量傳遞給被調(diào)用者的 Code。在 x64 的情況下,我們使用寄存器 rax。
如果被調(diào)用者尚未進(jìn)行優(yōu)化,我們將到達(dá) InterpreterEntryTrampoline,它會(huì)構(gòu)建以下棧框架。
沒(méi)有參數(shù)適配器的棧框架。
被調(diào)用者框架有一個(gè)額外的插槽,其中包含的參數(shù)計(jì)數(shù)可用于構(gòu)造 rest 參數(shù)或 arguments 對(duì)象,并在返回到調(diào)用者之前清除棧中參數(shù)。
返回時(shí),我們修改 LeaveInterpreterFrame 以讀取棧中的參數(shù)計(jì)數(shù),并彈出參數(shù)計(jì)數(shù)和形式參數(shù)計(jì)數(shù)之間的較大數(shù)字。
那么代碼優(yōu)化呢?我們來(lái)稍微更改一下初始腳本,以強(qiáng)制 V8 使用 TurboFan 對(duì)其進(jìn)行編譯:
function add42(x) { return x + 42; }
function callAdd42() { add42(3); }
%PrepareFunctionForOptimization(callAdd42);
callAdd42();
%OptimizeFunctionOnNextCall(callAdd42);
callAdd42();
在這里,我們使用 V8 內(nèi)部函數(shù)來(lái)強(qiáng)制 V8 優(yōu)化調(diào)用,否則 V8 僅在我們的小函數(shù)變熱(經(jīng)常使用)時(shí)才對(duì)其進(jìn)行優(yōu)化。我們?cè)趦?yōu)化之前調(diào)用它一次,以收集一些可用于指導(dǎo)編譯的類(lèi)型信息。在此處閱讀有關(guān) TurboFan 的更多信息(https://v8.dev/docs/turbofan)。
這里,我只展示與主題相關(guān)的部分生成代碼。
movq rdi,0x1a8e082126ad ;; Load the function object <JSFunction add42>
push 0x6 ;; Push SMI 3 as argument
movq rcx,0x1a8e082030d1 ;; <JSGlobal Object>
push rcx ;; Push receiver (the global proxy object)
movl rax,0x1 ;; Save the arguments count in rax
movl rcx,[rdi+0x17] ;; Load function object {Code} field in rcx
call rcx ;; Finally, call the code object!
盡管這段代碼使用了匯編來(lái)編寫(xiě),但如果你仔細(xì)看我的注釋?xiě)?yīng)該很容易能懂。本質(zhì)上,在編譯調(diào)用時(shí),TF 需要完成之前在 InterpreterPushArgsThenCall、Call、CallFunction 和 InvokeFunctionCall 內(nèi)置組件中完成的所有工作。它應(yīng)該會(huì)有更多的靜態(tài)信息來(lái)執(zhí)行此操作并發(fā)出更少的計(jì)算機(jī)指令。
現(xiàn)在,讓我們來(lái)看看參數(shù)數(shù)量和參數(shù)計(jì)數(shù)不匹配的情況。考慮調(diào)用 add42(1, 2, 3)。它會(huì)編譯為:
movq rdi,0x4250820fff1 ;; Load the function object <JSFunction add42>
;; Push receiver and arguments SMIs 1, 2 and 3
movq rcx,0x42508080dd5 ;; <JSGlobal Object>
push rcx
push 0x2
push 0x4
push 0x6
movl rax,0x3 ;; Save the arguments count in rax
movl rbx,0x1 ;; Save the formal parameters count in rbx
movq r10,0x564ed7fdf840 ;; <ArgumentsAdaptorTrampoline>
call r10 ;; Call the ArgumentsAdaptorTrampoline
如你所見(jiàn),不難為 TF 添加對(duì)參數(shù)和參數(shù)計(jì)數(shù)不匹配的支持。只需調(diào)用參數(shù)適配器 trampoline 即可!
然而這種方法成本很高。對(duì)于每個(gè)優(yōu)化的調(diào)用,我們現(xiàn)在都需要進(jìn)入?yún)?shù)適配器 trampoline,并像未優(yōu)化的代碼一樣處理框架。這就解釋了為什么在優(yōu)化的代碼中移除適配器框架的性能收益比在 Ignition 上大得多。
但是,生成的代碼非常簡(jiǎn)單。從中返回非常容易(結(jié)尾):
movq rsp,rbp ;; Clean callee frame
pop rbp
ret 0x8 ;; Pops a single argument (the receiver)
我們彈出框架并根據(jù)參數(shù)計(jì)數(shù)發(fā)出一個(gè)返回指令。如果實(shí)參計(jì)數(shù)和形參計(jì)數(shù)不匹配,則適配器框架 trampoline 將對(duì)其進(jìn)行處理。
生成的代碼本質(zhì)上與參數(shù)計(jì)數(shù)匹配的調(diào)用代碼相同。考慮調(diào)用 add42(1, 2, 3)。這將生成:
movq rdi,0x35ac082126ad ;; Load the function object <JSFunction add42>
;; Push receiver and arguments 1, 2 and 3 (reversed)
push 0x6
push 0x4
push 0x2
movq rcx,0x35ac082030d1 ;; <JSGlobal Object>
push rcx
movl rax,0x3 ;; Save the arguments count in rax
movl rcx,[rdi+0x17] ;; Load function object {Code} field in rcx
call rcx ;; Finally, call the code object!
該函數(shù)的結(jié)尾如何?我們不再回到參數(shù)適配器 trampoline 了,因此結(jié)尾確實(shí)比以前復(fù)雜了一些。
movq rcx,[rbp-0x18] ;; Load the argument count (from callee frame) to rcx
movq rsp,rbp ;; Pop out callee frame
pop rbp
cmpq rcx,0x0 ;; Compare arguments count with formal parameter count
jg 0x35ac000840c6 <+0x86>
;; If arguments count is smaller (or equal) than the formal parameter count:
ret 0x8 ;; Return as usual (parameter count is statically known)
;; If we have more arguments in the stack than formal parameters:
pop r10 ;; Save the return address
leaq rsp,[rsp+rcx*8+0x8] ;; Pop all arguments according to rcx
push r10 ;; Recover the return address
retl
參數(shù)適配器框架是一個(gè)臨時(shí)解決方案,用于實(shí)際參數(shù)和形式參數(shù)計(jì)數(shù)不匹配的調(diào)用。這是一個(gè)簡(jiǎn)單的解決方案,但它帶來(lái)了很高的性能成本,并增加了代碼庫(kù)的復(fù)雜性。如今,許多 Web 框架使用這一特性來(lái)創(chuàng)建更靈活的 API,結(jié)果帶來(lái)了更高的性能成本。反轉(zhuǎn)棧中參數(shù)這個(gè)簡(jiǎn)單的想法可以大大降低實(shí)現(xiàn)復(fù)雜性,并消除了此類(lèi)調(diào)用的幾乎所有開(kāi)銷(xiāo)。
原文鏈接:
https://v8.dev/blog/adaptor-frame
延伸閱讀:
關(guān)注我并轉(zhuǎn)發(fā)此篇文章,即可獲得學(xué)習(xí)資料~若想了解更多,也可移步InfoQ官網(wǎng),獲取InfoQ最新資訊~
閱讀本文前請(qǐng)先閱讀:Android開(kāi)發(fā):最全面、最易懂的Webview詳解
Android與JS通過(guò)WebView互相調(diào)用方法,實(shí)際上是:
二者溝通的橋梁是WebView
對(duì)于Android調(diào)用JS代碼的方法有2種:
1. 通過(guò)WebView的loadUrl()
2. 通過(guò)WebView的evaluateJavascript()
對(duì)于JS調(diào)用Android代碼的方法有3種:
1. 通過(guò)WebView的addJavascriptInterface()進(jìn)行對(duì)象映射
2. 通過(guò) WebViewClient 的shouldOverrideUrlLoading ()方法回調(diào)攔截 url
3. 通過(guò) WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調(diào)攔截JS對(duì)話(huà)框alert()、confirm()、prompt() 消息
2.1 Android通過(guò)WebView調(diào)用 JS 代碼
對(duì)于Android調(diào)用JS代碼的方法有2種:
1. 通過(guò)WebView的loadUrl()
2. 通過(guò)WebView的evaluateJavascript()
方式1:通過(guò)WebView的loadUrl()
步驟1:將需要調(diào)用的JS代碼以.html格式放到src/main/assets文件夾里
需要加載JS代碼:javascript.html
// 文本名:javascript
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Carson_Ho</title>
// JS代碼
<script>
// Android需要調(diào)用的方法
function callJS(){
alert("Android調(diào)用了JS的callJS方法");
}
</script>
</head>
</html>
步驟2:在Android里通過(guò)WebView設(shè)置調(diào)用JS代碼
Android代碼:MainActivity.java
注釋已經(jīng)非常清楚
public class MainActivity extends AppCompatActivity {
WebView mWebView;
Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView=(WebView) findViewById(R.id.webview);
WebSettings webSettings=mWebView.getSettings();
// 設(shè)置與Js交互的權(quán)限
webSettings.setJavaScriptEnabled(true);
// 設(shè)置允許JS彈窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 先載入JS代碼
// 格式規(guī)定為:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");
button=(Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 通過(guò)Handler發(fā)送消息
mWebView.post(new Runnable() {
@Override
public void run() {
// 注意調(diào)用的JS方法名要對(duì)應(yīng)上
// 調(diào)用javascript的callJS()方法
mWebView.loadUrl("javascript:callJS()");
}
});
}
});
// 由于設(shè)置了彈窗檢驗(yàn)調(diào)用結(jié)果,所以需要支持js對(duì)話(huà)框
// webview只是載體,內(nèi)容的渲染需要使用webviewChromClient類(lèi)去實(shí)現(xiàn)
// 通過(guò)設(shè)置WebChromeClient對(duì)象處理JavaScript的對(duì)話(huà)框
//設(shè)置響應(yīng)js 的Alert()函數(shù)
mWebView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
AlertDialog.Builder b=new AlertDialog.Builder(MainActivity.this);
b.setTitle("Alert");
b.setMessage(message);
b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
result.confirm();
}
});
b.setCancelable(false);
b.create().show();
return true;
}
});
}
}
特別注意:JS代碼調(diào)用一定要在 onPageFinished() 回調(diào)之后才能調(diào)用,否則不會(huì)調(diào)用。
onPageFinished()屬于WebViewClient類(lèi)的方法,主要在頁(yè)面加載結(jié)束時(shí)調(diào)用
// 只需要將第一種方法的loadUrl()換成下面該方法即可
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此處為 js 返回的結(jié)果
}
});
}
2.1.2 方法對(duì)比
2.1.3 使用建議
兩種方法混合使用,即Android 4.4以下使用方法1,Android 4.4以上方法2
// Android版本變量
final int version=Build.VERSION.SDK_INT;
// 因?yàn)樵摲椒ㄔ?Android 4.4 版本才可使用,所以使用時(shí)需進(jìn)行版本判斷
if (version < 18) {
mWebView.loadUrl("javascript:callJS()");
} else {
mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//此處為 js 返回的結(jié)果
}
});
}
對(duì)于JS調(diào)用Android代碼的方法有3種:
1. 通過(guò)WebView的addJavascriptInterface()進(jìn)行對(duì)象映射
2. 通過(guò) WebViewClient 的shouldOverrideUrlLoading ()方法回調(diào)攔截 url
3. 通過(guò) WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調(diào)攔截JS對(duì)話(huà)框alert()、confirm()、prompt() 消息
2.2.1 方法分析
方式1:通過(guò) WebView的addJavascriptInterface()進(jìn)行對(duì)象映射
步驟1:定義一個(gè)與JS對(duì)象映射關(guān)系的Android類(lèi):AndroidtoJs
AndroidtoJs.java(注釋已經(jīng)非常清楚)
// 繼承自O(shè)bject類(lèi)
public class AndroidtoJs extends Object {
// 定義JS需要調(diào)用的方法
// 被JS調(diào)用的方法必須加入@JavascriptInterface注解
@JavascriptInterface
public void hello(String msg) {
System.out.println("JS調(diào)用了Android的hello方法");
}
}
步驟2:將需要調(diào)用的JS代碼以.html格式放到src/main/assets文件夾里
需要加載JS代碼:javascript.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Carson</title>
<script>
function callAndroid(){
// 由于對(duì)象映射,所以調(diào)用test對(duì)象等于調(diào)用Android映射的對(duì)象
test.hello("js調(diào)用了android中的hello方法");
}
</script>
</head>
<body>
//點(diǎn)擊按鈕則調(diào)用callAndroid函數(shù)
<button type="button" id="button1" onclick="callAndroid()"></button>
</body>
</html>
步驟3:在Android里通過(guò)WebView設(shè)置Android類(lèi)與JS代碼的映射
詳細(xì)請(qǐng)看注釋
public class MainActivity extends AppCompatActivity {
WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView=(WebView) findViewById(R.id.webview);
WebSettings webSettings=mWebView.getSettings();
// 設(shè)置與Js交互的權(quán)限
webSettings.setJavaScriptEnabled(true);
// 通過(guò)addJavascriptInterface()將Java對(duì)象映射到JS對(duì)象
//參數(shù)1:Javascript對(duì)象名
//參數(shù)2:Java對(duì)象名
mWebView.addJavascriptInterface(new AndroidtoJs(), "test");//AndroidtoJS類(lèi)對(duì)象映射到j(luò)s的test對(duì)象
// 加載JS代碼
// 格式規(guī)定為:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");
僅將Android對(duì)象和JS對(duì)象映射即可
即JS需要調(diào)用Android的方法
以.html格式放到src/main/assets文件夾里
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Carson_Ho</title>
<script>
function callAndroid(){
/*約定的url協(xié)議為:js://webview?arg1=111&arg2=222*/
document.location="js://webview?arg1=111&arg2=222";
}
</script>
</head>
<!-- 點(diǎn)擊按鈕則調(diào)用callAndroid()方法 -->
<body>
<button type="button" id="button1" onclick="callAndroid()">點(diǎn)擊調(diào)用Android代碼</button>
</body>
</html>
當(dāng)該JS通過(guò)Android的mWebView.loadUrl("file:///android_asset/javascript.html")加載后,就會(huì)回調(diào)shouldOverrideUrlLoading (),接下來(lái)繼續(xù)看步驟2:
步驟2:在Android通過(guò)WebViewClient復(fù)寫(xiě)shouldOverrideUrlLoading ()
MainActivity.java
public class MainActivity extends AppCompatActivity {
WebView mWebView;
// Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView=(WebView) findViewById(R.id.webview);
WebSettings webSettings=mWebView.getSettings();
// 設(shè)置與Js交互的權(quán)限
webSettings.setJavaScriptEnabled(true);
// 設(shè)置允許JS彈窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 步驟1:加載JS代碼
// 格式規(guī)定為:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");
// 復(fù)寫(xiě)WebViewClient類(lèi)的shouldOverrideUrlLoading方法
mWebView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 步驟2:根據(jù)協(xié)議的參數(shù),判斷是否是所需要的url
// 一般根據(jù)scheme(協(xié)議格式) & authority(協(xié)議名)判斷(前兩個(gè)參數(shù))
//假定傳入進(jìn)來(lái)的 url="js://webview?arg1=111&arg2=222"(同時(shí)也是約定好的需要攔截的)
Uri uri=Uri.parse(url);
// 如果url的協(xié)議=預(yù)先約定的 js 協(xié)議
// 就解析往下解析參數(shù)
if ( uri.getScheme().equals("js")) {
// 如果 authority=預(yù)先約定協(xié)議里的 webview,即代表都符合約定的協(xié)議
// 所以攔截url,下面JS開(kāi)始調(diào)用Android需要的方法
if (uri.getAuthority().equals("webview")) {
// 步驟3:
// 執(zhí)行JS所需要調(diào)用的邏輯
System.out.println("js調(diào)用了Android的方法");
// 可以在協(xié)議上帶有參數(shù)并傳遞到Android上
HashMap<String, String> params=new HashMap<>();
Set<String> collection=uri.getQueryParameterNames();
}
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
}
);
}
}
如果JS想要得到Android方法的返回值,只能通過(guò) WebView 的 loadUrl ()去執(zhí)行 JS 方法把返回值傳遞回去,相關(guān)的代碼如下:
// Android:MainActivity.java
mWebView.loadUrl("javascript:returnResult(" + result + ")");
// JS:javascript.html
function returnResult(result){
alert("result is" + result);
}
在JS中,有三個(gè)常用的對(duì)話(huà)框方法:
方式3的原理:Android通過(guò) WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調(diào)分別攔截JS對(duì)話(huà)框
(即上述三個(gè)方法),得到他們的消息內(nèi)容,然后解析即可。
下面的例子將用攔截 JS的輸入框(即prompt()方法)說(shuō)明 :
步驟1:加載JS代碼,如下:
javascript.html
以.html格式放到src/main/assets文件夾里
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Carson_Ho</title>
<script>
function clickprompt(){
// 調(diào)用prompt()
var result=prompt("js://demo?arg1=111&arg2=222");
alert("demo " + result);
}
</script>
</head>
<!-- 點(diǎn)擊按鈕則調(diào)用clickprompt() -->
<body>
<button type="button" id="button1" onclick="clickprompt()">點(diǎn)擊調(diào)用Android代碼</button>
</body>
</html>
當(dāng)使用mWebView.loadUrl("file:///android_asset/javascript.html")加載了上述JS代碼后,就會(huì)觸發(fā)回調(diào)onJsPrompt(),具體如下:
步驟2:在Android通過(guò)WebChromeClient復(fù)寫(xiě)onJsPrompt()
public class MainActivity extends AppCompatActivity {
WebView mWebView;
// Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView=(WebView) findViewById(R.id.webview);
WebSettings webSettings=mWebView.getSettings();
// 設(shè)置與Js交互的權(quán)限
webSettings.setJavaScriptEnabled(true);
// 設(shè)置允許JS彈窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 先加載JS代碼
// 格式規(guī)定為:file:///android_asset/文件名.html
mWebView.loadUrl("file:///android_asset/javascript.html");
mWebView.setWebChromeClient(new WebChromeClient() {
// 攔截輸入框(原理同方式2)
// 參數(shù)message:代表promt()的內(nèi)容(不是url)
// 參數(shù)result:代表輸入框的返回值
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// 根據(jù)協(xié)議的參數(shù),判斷是否是所需要的url(原理同方式2)
// 一般根據(jù)scheme(協(xié)議格式) & authority(協(xié)議名)判斷(前兩個(gè)參數(shù))
//假定傳入進(jìn)來(lái)的 url="js://webview?arg1=111&arg2=222"(同時(shí)也是約定好的需要攔截的)
Uri uri=Uri.parse(message);
// 如果url的協(xié)議=預(yù)先約定的 js 協(xié)議
// 就解析往下解析參數(shù)
if ( uri.getScheme().equals("js")) {
// 如果 authority=預(yù)先約定協(xié)議里的 webview,即代表都符合約定的協(xié)議
// 所以攔截url,下面JS開(kāi)始調(diào)用Android需要的方法
if (uri.getAuthority().equals("webview")) {
//
// 執(zhí)行JS所需要調(diào)用的邏輯
System.out.println("js調(diào)用了Android的方法");
// 可以在協(xié)議上帶有參數(shù)并傳遞到Android上
HashMap<String, String> params=new HashMap<>();
Set<String> collection=uri.getQueryParameterNames();
//參數(shù)result:代表消息框的返回值(輸入值)
result.confirm("js調(diào)用了Android的方法成功啦");
}
return true;
}
return super.onJsPrompt(view, url, message, defaultValue, result);
}
// 通過(guò)alert()和confirm()攔截的原理相同,此處不作過(guò)多講述
// 攔截JS的警告框
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
// 攔截JS的確認(rèn)框
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
}
);
}
}
2.2.2 三種方式的對(duì)比 & 使用場(chǎng)景
很長(zhǎng)的一段時(shí)間中,Vue 官方都以簡(jiǎn)單上手作為其推廣的重點(diǎn)。這確實(shí)給 Vue 帶來(lái)了非常大的用戶(hù)量,尤其是最追求需求開(kāi)發(fā)效率, 往往不那么在意工程代碼質(zhì)量的國(guó)內(nèi)中小企業(yè)中,Vue 占據(jù)的份額極速增長(zhǎng)。但是作為開(kāi)發(fā)者自身,我們必須要認(rèn)清一個(gè)重點(diǎn),簡(jiǎn)單易用從來(lái)不應(yīng)該在技術(shù)選型中占據(jù)很大的份額,可維護(hù)性才是。
以防萬(wàn)一有的同學(xué)實(shí)在不看官方文檔,我先提一嘴,SFC 就是寫(xiě) Vue 組件的時(shí)候?qū)懙?vue文件,這一個(gè)文件就是一個(gè) SFC,全稱(chēng) Single File Component,也即單文件組件。
在開(kāi)始說(shuō)我個(gè)人的觀(guān)點(diǎn)之前,我們先來(lái)看幾個(gè)事實(shí):
一是:Vue3 的定義原生支持 JSX,并且 Vue3 源碼中有jsx.d.ts來(lái)便于使用 JSX。 不知道同學(xué)們看到這里會(huì)想到什么, 我的第一反應(yīng)是:社區(qū)對(duì)于 JSX 的需求聲音是不小的,所以會(huì)反向推動(dòng) Vue3 官方對(duì)于 JSX 的支持。
二是:AntDesign 的 vue3 版本,基本全部都是用 JSX 開(kāi)發(fā)的,而且 Vue3 現(xiàn)在官方的 babel-jsx 插件就是阿里的人一開(kāi)始維護(hù)的, 雖然我向來(lái)不喜歡阿里系的 KPI 推動(dòng)技術(shù)方式,而且現(xiàn)在的 JSX 語(yǔ)法支持也不是很符合我的期望,但至少在使用 JSX 開(kāi)發(fā)是更優(yōu)秀的選擇這點(diǎn)上,我還是很認(rèn)可 AntDesign 團(tuán)隊(duì)的。
OK,說(shuō)這些呢,主要是先擺出一些事實(shí)作為依據(jù),讓有些同學(xué)可以不需要拿什么:
這些觀(guān)點(diǎn)來(lái)批斗我,首先我都會(huì)從客觀(guān)的角度來(lái)分析為什么,至少是我是能講出優(yōu)劣勢(shì)的理由的。
OK,前言差不多到這里,接下來(lái)咱給您分析分析,為什么你應(yīng)該選擇 JSX 來(lái)開(kāi)發(fā) Vue。
其實(shí)第一點(diǎn)就已經(jīng)是殺手了,對(duì)于想要使用 TypeScript 來(lái)開(kāi)發(fā) Vue3 應(yīng)用的同學(xué)來(lái)說(shuō),這簡(jiǎn)直就是 SFC 無(wú)法克服的世界難題。
一句話(huà)概括:TypeScript 原生支持 JSX 語(yǔ)法,而基本無(wú)望 TS 官方能支持 SFC 的 template 語(yǔ)法。
TS 毫無(wú)疑問(wèn)在前端社區(qū)的重要性越來(lái)越大,但凡未來(lái)對(duì)于代碼質(zhì)量有一定要求的前端團(tuán)隊(duì),都應(yīng)該會(huì)選擇使用 TS 來(lái)進(jìn)行開(kāi)發(fā)。 而且現(xiàn)在基本上在 NPM 上都能看到包你都能找到對(duì)應(yīng)的 TS 定義,現(xiàn)在使用 TS 開(kāi)發(fā)成本已經(jīng)只剩下你是不是會(huì) TS 語(yǔ)法了,在這種情況下是否支持 TS 則是開(kāi)發(fā)模式在未來(lái)走不走的遠(yuǎn)的重要原因。
目前 SFC 只能通過(guò)shim讓 TS 可以引入.vue文件,但是對(duì)于所有 SFC 的組件的定義都是一樣的:
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, {}, any>
export default component
}
也就是說(shuō)你引入的 SFC 組件,TS 是不知道這個(gè)組件的 Props 應(yīng)該接收什么的。所以你無(wú)法享受到這些 TS 的優(yōu)勢(shì):
當(dāng)然你會(huì)說(shuō)既然 Vue 官方能開(kāi)發(fā)處 SFC 的語(yǔ)法,自然會(huì)支持這些特性。我表示這當(dāng)然有可能,但是這個(gè)難度是非常大的,需要很多方面的支持,甚至可能需要 TS 官方團(tuán)隊(duì)愿意協(xié)助, 但是我想不到 TS 官方有什么理由來(lái)支持 SFC,因?yàn)檫@只是 Vue 自己創(chuàng)建的方言,在其他場(chǎng)景下是沒(méi)有使用的,TS 是面向全社區(qū)的,我覺(jué)得他們不會(huì)考慮主動(dòng)來(lái)支持 SFC。
那么有同學(xué)要問(wèn)了,JSX 不也是非原生的 JS 語(yǔ)法么,他怎么就能讓 TS 官方支持了呢,是不是 FB 和微硬之間有什么 PY 交易?
這就涉及第二點(diǎn)了,JSX 和靜態(tài)模板的靈活性區(qū)別。
很多人弄錯(cuò)了一個(gè)問(wèn)題,就是覺(jué)得 SFC 的模板語(yǔ)法和 JSX 是一樣的,都是一種別人發(fā)明的語(yǔ)法,并不是 JS 原生的。這是事實(shí),但又有一些區(qū)別,這個(gè)區(qū)別主要是體現(xiàn)在對(duì)于 JSX 的認(rèn)知上。
一句話(huà)概括:JSX 并沒(méi)有擴(kuò)展 JS 的語(yǔ)法,他只是縮略了 JS 的寫(xiě)法!其本質(zhì)就是 JS 的語(yǔ)法糖
就像 es6 給增加的語(yǔ)法糖,比如
const a=1
const b=2
const obj={ a, b }
// 其實(shí)就等價(jià)于
const obj={ a: a, b: b }
這種寫(xiě)法并沒(méi)有擴(kuò)展 JS 的能力,只是簡(jiǎn)便了寫(xiě)法,JSX 也是一樣的。
JSX 其實(shí)就是方法調(diào)用,他和 JS 是有一對(duì)一對(duì)應(yīng)關(guān)系的,我們來(lái)看一個(gè)例子:
const element=<div id="root">Hello World</div>
這里的 JSX 語(yǔ)法編譯之后其實(shí)就是:
const element=createElement('div', { id: 'root' }, 'Hello World')
而 JSX 就是這些了,沒(méi)有什么更多的內(nèi)容,所以說(shuō) JSX 只是方便我們寫(xiě)嵌套的函數(shù)調(diào)用的語(yǔ)法糖,而其本身沒(méi)有擴(kuò)展任何其他的內(nèi)容。
但是 SFC 就不一樣了。
SFC 定義的不僅是語(yǔ)法,更是文件。
SFC 的具體定義是單文件組件,它本身就是把一個(gè)文件看作一個(gè)單位,所以他的約束性是要大很多的,你必須具有固定的文件結(jié)構(gòu)才能使用 SFC,這做了很多的限制:
我們一點(diǎn)點(diǎn)來(lái)講
這個(gè)說(shuō)實(shí)話(huà)非常非常不方便,很多時(shí)候我們寫(xiě)一個(gè)頁(yè)面的時(shí)候其實(shí)經(jīng)常會(huì)需要把一些小的節(jié)點(diǎn)片段拆分到小組件里面進(jìn)行復(fù)用(如果你現(xiàn)在沒(méi)有這個(gè)習(xí)慣可能就是因?yàn)?SFC 的限制讓你習(xí)慣了全部寫(xiě)在一個(gè)文件內(nèi))。
React 生態(tài)中豐富的 css-in-js 方案就是很好的例子,我們可以通過(guò):
const StyledButton=styled('button', {
color: 'red',
})
如果我們這個(gè)頁(yè)面需要使用特定樣式的按鈕,通過(guò)這種方式在頁(yè)面文件里面封裝一下是非常常見(jiàn)的。因?yàn)闆](méi)必要把這個(gè)組件拆分出去,他也不是一個(gè)可復(fù)用的組件,拆分出去了還要多一次import。
Vue 生態(tài)基本沒(méi)有 css-in-js 的成熟方案其實(shí)跟這個(gè)限制也很有關(guān)系。
再來(lái)一個(gè)例子,比如我們封裝了一個(gè) Input 組件,我們希望同時(shí)導(dǎo)出 Password 組件和 Textarea 組件來(lái)方便用戶(hù)根據(jù)實(shí)際需求使用,而這兩個(gè)組件本身內(nèi)部就是用的 Input 組件,只是定制了一些 props:
const Input={ ... }
export default Input
export const Textarea=(props)=> <Input multiline={true} {...props} />
export const Password=(props)=> <Input type="password" {...props} />
在 JSX 中可以非常簡(jiǎn)單地實(shí)現(xiàn),但是如果通過(guò) SFC,你可能就要強(qiáng)行拆成三個(gè)文件,另外為了方便,你可能還要增加一個(gè)index.js來(lái)導(dǎo)出這三個(gè)組件,你能想象這多了多少工作量么。
我不知道有多少同學(xué)看過(guò) Vue 的 template 編譯出來(lái)之后的代碼,以我的經(jīng)驗(yàn)來(lái)說(shuō)看過(guò)的可能不會(huì)超過(guò) 50%(樂(lè)觀(guān)估計(jì)),建議同學(xué)們?nèi)绻€不了解的,可以去嘗試看一下。
為什么要看這個(gè)呢?因?yàn)槟憧戳酥竽銜?huì)發(fā)現(xiàn),你在 template 里面寫(xiě)的類(lèi)似 HTMl 的內(nèi)容,其實(shí)跟 HTML 根本沒(méi)啥關(guān)系,他們也會(huì)被編譯成類(lèi)似 JSX 編譯出來(lái)的結(jié)果。
{
render(h) {
return h('div', {on: {}, props: {}}, h('span'))
}
}
類(lèi)似這樣的結(jié)果,而這里面h函數(shù)調(diào)用的結(jié)果就是一個(gè) VNode,是 Vue 中的節(jié)點(diǎn)的基礎(chǔ)單元。那么既然這些單元就是一個(gè)對(duì)象,其實(shí)理所當(dāng)然的,他們是可以作為參數(shù)傳遞的。 也就是說(shuō),理論上他們是可以通過(guò)props把節(jié)點(diǎn)當(dāng)作參數(shù)傳遞給其他組件的。
這個(gè)做法在 React 中非常常見(jiàn),叫做renderProps,并且其非常靈活:
const Comp=()=> <Layout header={<MyHeader />} footer={<MyFooter />} />
但是因?yàn)?SFC 模板的限制,我們很難在 SFC 里面的 props 上寫(xiě)節(jié)點(diǎn):
<template>
<Layout :header="<MyHeader/>"></Layout>
</template>
這樣寫(xiě)是不行的,因?yàn)?SFC 定義了:header綁定接受的只能是 js 表達(dá)式,而<MyHeader/>顯然不是。
因?yàn)橥ㄟ^(guò) props 傳遞不行,所以 Vue 才發(fā)明了 slot 插槽的概念
雖然我們一直再說(shuō) Vue 簡(jiǎn)單,但是事實(shí)上ScopedSlots一度成為新手理解 Vue 的噩夢(mèng),很多同學(xué)都被這個(gè)繞來(lái)繞去的作用域整的死去活來(lái)。
我們看一個(gè)ScopedSlots的例子:
<template>
<Comp>
<template v-slot:scope="ctx">
<div>{{ctx.name}}</div>
</template>
</Comp>
</template>
這里ctx是Comp里面的屬性,通過(guò)這種方式傳遞出來(lái),讓我們?cè)诋?dāng)前組件可以調(diào)用父組件里面的屬性。這簡(jiǎn)直就是理解的噩夢(mèng),但是如果用 JSX 實(shí)現(xiàn)類(lèi)似功能就非常簡(jiǎn)單:
<Comp scope={name=> <div>{name}</div>} />
我們只是給一個(gè)叫做scope的 props 傳遞來(lái)一個(gè)函數(shù),這個(gè)函數(shù)接受一個(gè)name屬性,在Comp里面會(huì)調(diào)用這個(gè)函數(shù)并傳入name。 簡(jiǎn)單來(lái)說(shuō)我們傳入的就是一個(gè)構(gòu)建節(jié)點(diǎn)片段的函數(shù),就是這么簡(jiǎn)單。
這就是因?yàn)?SFC 的模板的限制,導(dǎo)致靈活性不足,Vue 需要去創(chuàng)造概念,創(chuàng)造關(guān)鍵字來(lái)抹平這些能力的不足,而創(chuàng)造的概念自然就引入了學(xué)習(xí)成本。
所以其實(shí)我一直不認(rèn)可 Vue 比 React 好學(xué)的說(shuō)法的,如果你真的認(rèn)真研究所有用法,并且總是嘗試用最合理的方式實(shí)現(xiàn)功能,那么 Vue 絕對(duì)不會(huì)比 React 簡(jiǎn)單。
這個(gè)體現(xiàn)在兩個(gè)方面,一個(gè)是我們定義在全局的一些固定數(shù)據(jù)如果要在組件內(nèi)使用的話(huà),就要通過(guò)this掛載到組件上。
比如我們緩存了一份城市數(shù)據(jù),這種數(shù)據(jù)基本上是不會(huì)改的,所以也沒(méi)必要掛載到組件上讓其能夠響應(yīng)式。但是在 SFC 里面這是做不到的, 因?yàn)槟0宓膱?zhí)行上下文是在編譯時(shí)綁定。你在模板里面訪(fǎng)問(wèn)的變量,都會(huì)在編譯時(shí)自動(dòng)綁定到this上,因?yàn)槟0逍枰幾g,其本身也是字符串不具有作用域的概念。
而這在 JSX 中則不復(fù)存在:
const citys=[]
const Comp=()=> {
return citys.map(c=> <div>{c}</div>)
}
另外一個(gè)方面則是在組件使用上,在 SFC 中,組件必須事先注冊(cè),因?yàn)槲覀冊(cè)谀0謇锩鎸?xiě)的只能是字符串而不能是具體某個(gè)組件變量。 那么模板中的組件和真實(shí)的組件對(duì)象只能通過(guò)字符串匹配來(lái)實(shí)現(xiàn)綁定。這帶來(lái)了以下問(wèn)題:
在 JSX 中則沒(méi)有這些問(wèn)題,因?yàn)?JSX 里面直接使用組件引用作為參數(shù):
const Comp={...}
const App=()=> <Comp />
其實(shí)上面能看出來(lái),除了 SFC 本身的問(wèn)題之外,Vue 使用字符串模板也會(huì)帶來(lái)很多的靈活性問(wèn)題。 最直接的證據(jù),就是 Vue 使用了directive來(lái)擴(kuò)展功能(當(dāng)然這不是 Vue 發(fā)明的,老早的模板引擎就有類(lèi)似問(wèn)題)。
為什么說(shuō)directive是不得已的選擇呢?因?yàn)殪o態(tài)模板缺失邏輯處理的能力。我們拿列表循環(huán)舉例,在 JS 中我們可以非常方便地通過(guò)map函數(shù)來(lái)創(chuàng)建列表:
const list=arr.map(name=> <span key={name}>{name}</span>)
而因?yàn)?JSX 本身就是函數(shù)調(diào)用,所以上面的代碼和 JSX 結(jié)合起來(lái)也非常自然:
const App=()=> (
<div>
<Header />
{arr.map(name=> (
<span key={name}>{name}</span>
))}
</div>
)
上面的例子對(duì)應(yīng)到 JS 如下:
const App=()=>
createElement('div', {}, [
<Header />,
arr.map(name=> createElement('span', { key: name }, name)),
])
這仍然是因?yàn)?JSX 只是 JS 的語(yǔ)法糖的原因,所有能在 JS 中實(shí)現(xiàn)的在 JSX 里面都能實(shí)現(xiàn)。
而 SFC 的模板是基于字符串編譯的,其本身就是一段字符串,我們不能直接在模板里面寫(xiě)map來(lái)循環(huán)節(jié)點(diǎn),(當(dāng)然我們可以在可以接收表達(dá)式的地方寫(xiě),比如v-on里面)。
那么我們不能循環(huán)節(jié)點(diǎn),有需要這樣的功能來(lái)渲染列表,怎么辦呢?就是發(fā)明一個(gè)標(biāo)志來(lái)告訴編譯器這里需要循環(huán),在 Vue 中的體現(xiàn)就是v-for指令。
同學(xué)們可能要問(wèn)了,既然 Vue 能實(shí)現(xiàn)v-for,為什么不直接實(shí)現(xiàn)表達(dá)式循環(huán)列表呢?他當(dāng)然也可以實(shí)現(xiàn),但是他肯定不會(huì)這么選,因?yàn)槌杀咎吡恕?他要這么做就相當(dāng)于他要實(shí)現(xiàn)一個(gè) JS 引擎,而其實(shí)里面很多內(nèi)容又是不必須的,一個(gè)v-for其實(shí)就能夠適用大部分情況了。
但有了v-for就需要v-if,那么后面還會(huì)需要其他各種能力,這就是一種方言的產(chǎn)生和發(fā)展的過(guò)程。
當(dāng)然指令也不僅僅是 JS 表達(dá)式的代替品,其本身也是增加了一些其他能力的,比如它能夠讓我們更方便地訪(fǎng)問(wèn) DOM 節(jié)點(diǎn), 但是嘛,我們用框架的理由不就是為了能夠盡可能的屏蔽 DOM 操作嘛~
以上就是我對(duì)應(yīng)該選擇使用 JSX 還是 SFC 進(jìn)行開(kāi)發(fā)的分析,其實(shí)歸根到底 SFC 的問(wèn)題在于其沒(méi)有擁抱 JS, 他的語(yǔ)法是自己發(fā)明的,他需要有一個(gè) JS 實(shí)現(xiàn)的 compiler 來(lái)讓其最終能在 JS 環(huán)境中運(yùn)行,這本質(zhì)上就是一種發(fā)明, 我們不能否認(rèn)發(fā)明確實(shí)有優(yōu)點(diǎn),但我們也不能只看有點(diǎn)不看問(wèn)題,沒(méi)能擁抱 JS 自然就很難完全復(fù)用 JS 社區(qū)的優(yōu)勢(shì) 而 JS 社區(qū)一直在蓬勃發(fā)展,好用的工具一直在涌現(xiàn),而 SFC 想要使用 JS 社區(qū)的這些工具還要自己再實(shí)現(xiàn)一份,我們可以細(xì)數(shù)以下 SFC 做了哪些兼容
基本上常用的工具我們都需要等待 Vue 社區(qū)或者官方開(kāi)發(fā)了插件之后才能運(yùn)行。而 JSX 因?yàn)橛?babel 和 typescript 的官方支持, 基本上所有新的 JS 生態(tài)工具原生都是支持的。
在這 Vue3 開(kāi)始預(yù)備發(fā)力的階段,我們還是希望 Vue 社區(qū)能夠使用更優(yōu)秀更規(guī)范的方式來(lái)進(jìn)行開(kāi)發(fā), 其實(shí)如果我們直接使用 JSX 開(kāi)發(fā) Vue3,我們會(huì)發(fā)現(xiàn)很多時(shí)候我們都不需要用到emit、attrs這些概念, 甚至如果 Vue3 的 JSX 插件支持,我們甚至能夠拋棄slots。
但是因?yàn)?Vue3 一定要考慮兼容 Vue2,導(dǎo)致本身潛力很好的 Vue3 總是顯得縮手縮腳,這不得不說(shuō)是一種遺憾。
*請(qǐng)認(rèn)真填寫(xiě)需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。