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 国产视频2021,亚洲欧洲日韩在线,亚洲国产精品第一页

          整合營(yíng)銷(xiāo)服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢(xún)熱線(xiàn):

          JavaScript 調(diào)用提速 40% 的實(shí)踐

          JavaScript 調(diào)用提速 40% 的實(shí)踐

          數(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%)。

          太長(zhǎng)不看版:反轉(zhuǎn)參數(shù)

          這個(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 棧框架。


          加快 JavaScript 調(diào)用

          為了講清楚我們?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í)行流程。


          Ignition

          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ù)?

          參數(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í)可以。

          我們回顧一下之前的需求:


          1. 我們需要能夠像以前一樣無(wú)縫訪(fǎng)問(wèn)參數(shù)和寄存器。訪(fǎng)問(wèn)它們時(shí)無(wú)法進(jìn)行檢查。那成本太高了。
          2. 我們需要能夠從棧中構(gòu)造 rest 參數(shù)和 arguments 對(duì)象。
          3. 從一個(gè)調(diào)用返回時(shí),我們需要能夠輕松清理未知數(shù)量的參數(shù)。
          4. 此外,當(dāng)然我們希望沒(méi)有額外的框架!

          如果要消除多余的框架,則需要確定將參數(shù)放在何處:在被調(diào)用者框架中還是在調(diào)用者框架中。

          被調(diào)用者框架中的參數(shù)

          假設(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)。顯然這種方法太昂貴了!

          調(diào)用者框架中的參數(shù)

          好的,如果我們?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ù)字。

          TurboFan

          那么代碼優(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ī)指令。

          帶參數(shù)適配器框架的 TurboFan

          現(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)行處理。

          沒(méi)有參數(shù)適配器框架的 TurboFan

          生成的代碼本質(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

          小結(jié)

          參數(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

          延伸閱讀:

          Deno 2020 年大事記-InfoQ

          關(guān)注我并轉(zhuǎn)發(fā)此篇文章,即可獲得學(xué)習(xí)資料~若想了解更多,也可移步InfoQ官網(wǎng),獲取InfoQ最新資訊~

          • 現(xiàn)在很多App里都內(nèi)置了Web網(wǎng)頁(yè)(Hybrid App),比如說(shuō)很多電商平臺(tái),淘寶、京東、聚劃算等等,如下圖

          • 上述功能是由Android的WebView實(shí)現(xiàn)的,其中涉及到Android客戶(hù)端與Web網(wǎng)頁(yè)交互的實(shí)現(xiàn)
          • 今天我將全面介紹Android通過(guò)WebView與JS交互的全面方式

          閱讀本文前請(qǐng)先閱讀:Android開(kāi)發(fā):最全面、最易懂的Webview詳解


          目錄


          1. 交互方式總結(jié)

          Android與JS通過(guò)WebView互相調(diào)用方法,實(shí)際上是:

          • Android去調(diào)用JS的代碼
          • JS去調(diào)用Android的代碼

          二者溝通的橋梁是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. 具體分析

          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()

          • 實(shí)例介紹:點(diǎn)擊Android按鈕,即調(diào)用WebView JS(文本名為javascript)中callJS()
          • 具體使用:

          步驟1:將需要調(diào)用的JS代碼以.html格式放到src/main/assets文件夾里

          • 為了方便展示,本文是采用Andorid調(diào)用本地JS代碼說(shuō)明;
          • 實(shí)際情況時(shí),Android更多的是調(diào)用遠(yuǎn)程JS代碼,即將加載的JS代碼路徑改成url即可

          需要加載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>

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19

          步驟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;

          }

          });

          }

          }

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19
          • 20
          • 21
          • 22
          • 23
          • 24
          • 25
          • 26
          • 27
          • 28
          • 29
          • 30
          • 31
          • 32
          • 33
          • 34
          • 35
          • 36
          • 37
          • 38
          • 39
          • 40
          • 41
          • 42
          • 43
          • 44
          • 45
          • 46
          • 47
          • 48
          • 49
          • 50
          • 51
          • 52
          • 53
          • 54
          • 55
          • 56
          • 57
          • 58
          • 59
          • 60
          • 61
          • 62
          • 63
          • 64
          • 65
          • 66
          • 67
          • 68
          • 69

          特別注意:JS代碼調(diào)用一定要在 onPageFinished() 回調(diào)之后才能調(diào)用,否則不會(huì)調(diào)用。

          onPageFinished()屬于WebViewClient類(lèi)的方法,主要在頁(yè)面加載結(jié)束時(shí)調(diào)用

          方式2:通過(guò)WebView的evaluateJavascript()

          • 優(yōu)點(diǎn):該方法比第一種方法效率更高、使用更簡(jiǎn)潔。
          • 因?yàn)樵摲椒ǖ膱?zhí)行不會(huì)使頁(yè)面刷新,而第一種方法(loadUrl )的執(zhí)行則會(huì)。
          • Android 4.4 后才可使用
          • 具體使用

          // 只需要將第一種方法的loadUrl()換成下面該方法即可

          mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {

          @Override

          public void onReceiveValue(String value) {

          //此處為 js 返回的結(jié)果

          }

          });

          }

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8

          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é)果

          }

          });

          }

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13

          2.2 JS通過(guò)WebView調(diào)用 Android 代碼

          對(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方法");

          }

          }

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10

          步驟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>

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19

          步驟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");

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19
          • 20
          • 21
          • 22
          • 23
          • 24

          特點(diǎn)

          • 優(yōu)點(diǎn):使用簡(jiǎn)單

          僅將Android對(duì)象和JS對(duì)象映射即可

          • 缺點(diǎn):存在嚴(yán)重的漏洞問(wèn)題,具體請(qǐng)看文章:你不知道的 Android WebView 使用漏洞

          方式2:通過(guò) WebViewClient 的方法shouldOverrideUrlLoading ()回調(diào)攔截 url

          • 具體原理:
          1. Android通過(guò) WebViewClient 的回調(diào)方法shouldOverrideUrlLoading ()攔截 url
          2. 解析該 url 的協(xié)議
          3. 如果檢測(cè)到是預(yù)先約定好的協(xié)議,就調(diào)用相應(yīng)方法

          即JS需要調(diào)用Android的方法

          • 具體使用:
          • 步驟1:在JS約定所需要的Url協(xié)議
          • JS代碼:javascript.html

          以.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>

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19
          • 20

          當(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);

          }

          }

          );

          }

          }

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19
          • 20
          • 21
          • 22
          • 23
          • 24
          • 25
          • 26
          • 27
          • 28
          • 29
          • 30
          • 31
          • 32
          • 33
          • 34
          • 35
          • 36
          • 37
          • 38
          • 39
          • 40
          • 41
          • 42
          • 43
          • 44
          • 45
          • 46
          • 47
          • 48
          • 49
          • 50
          • 51
          • 52
          • 53
          • 54
          • 55
          • 56
          • 57
          • 58
          • 59
          • 60

          特點(diǎn)

          • 優(yōu)點(diǎn):不存在方式1的漏洞;
          • 缺點(diǎn):JS獲取Android方法的返回值復(fù)雜。

          如果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);

          }

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7

          方式3:通過(guò) WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調(diào)攔截JS對(duì)話(huà)框alert()、confirm()、prompt() 消息

          在JS中,有三個(gè)常用的對(duì)話(huà)框方法:

          方式3的原理:Android通過(guò) WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調(diào)分別攔截JS對(duì)話(huà)框

          (即上述三個(gè)方法),得到他們的消息內(nèi)容,然后解析即可。

          下面的例子將用攔截 JS的輸入框(即prompt()方法)說(shuō)明 :

          • 常用的攔截是:攔截 JS的輸入框(即prompt()方法)
          • 因?yàn)橹挥衟rompt()可以返回任意類(lèi)型的值,操作最全面方便、更加靈活;而alert()對(duì)話(huà)框沒(méi)有返回值;confirm()對(duì)話(huà)框只能返回兩種狀態(tài)(確定 / 取消)兩個(gè)值

          步驟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>

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19
          • 20
          • 21
          • 22

          當(dāng)使用mWebView.loadUrl("file:///android_asset/javascript.html")加載了上述JS代碼后,就會(huì)觸發(fā)回調(diào)onJsPrompt(),具體如下:

          • 如果是攔截警告框(即alert()),則觸發(fā)回調(diào)onJsAlert();
          • 如果是攔截確認(rèn)框(即confirm()),則觸發(fā)回調(diào)onJsConfirm();

          步驟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);

          }

          }

          );

          }

          }

          • 1
          • 2
          • 3
          • 4
          • 5
          • 6
          • 7
          • 8
          • 9
          • 10
          • 11
          • 12
          • 13
          • 14
          • 15
          • 16
          • 17
          • 18
          • 19
          • 20
          • 21
          • 22
          • 23
          • 24
          • 25
          • 26
          • 27
          • 28
          • 29
          • 30
          • 31
          • 32
          • 33
          • 34
          • 35
          • 36
          • 37
          • 38
          • 39
          • 40
          • 41
          • 42
          • 43
          • 44
          • 45
          • 46
          • 47
          • 48
          • 49
          • 50
          • 51
          • 52
          • 53
          • 54
          • 55
          • 56
          • 57
          • 58
          • 59
          • 60
          • 61
          • 62
          • 63
          • 64
          • 65
          • 66
          • 67
          • 68
          • 69
          • 70
          • 71
          • 72
          • 73
          • 74
          • 75
          • 76
          • 77
          • 78

          • Demo地址
          • 上述所有代碼均存放在:Carson_Ho的Github地址 : WebView Demo

          2.2.2 三種方式的對(duì)比 & 使用場(chǎng)景


          3. 總結(jié)

          • 本文主要對(duì)Android通過(guò)WebView與JS的交互方式進(jìn)行了全面介紹

          • 關(guān)于WebView的系列文章對(duì)你有所幫助
          • Android開(kāi)發(fā):最全面、最易懂的Webview詳解
          • Android:你不知道的 WebView 使用漏洞
          • 手把手教你構(gòu)建 Android WebView 的緩存機(jī)制 & 資源預(yù)加載方案
          • 接下來(lái)我會(huì)繼續(xù)講解其他安卓開(kāi)發(fā)的知識(shí),有興趣可以繼續(xù)關(guān)注Carson_Ho的安卓開(kāi)發(fā)筆記!!!!

          請(qǐng)?jiān)u論點(diǎn)贊!因?yàn)槟銈兊馁澩?鼓勵(lì)是我寫(xiě)作的最大動(dò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é)可以不需要拿什么:

          • 啊,這都是你空想的,你太自以為是了
          • 你再怎么想都沒(méi)用,咱們 Vue 就是應(yīng)該用 SFC 開(kāi)發(fā)

          這些觀(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。


          TypeScript 支持

          其實(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ì):

          • 開(kāi)發(fā)時(shí)的自動(dòng)提示
          • 編譯時(shí)的 TS 校驗(yàn),讓你盡早發(fā)現(xiàn)問(wèn)題
          • 編譯組件生成你的組件定義(對(duì)于類(lèi)庫(kù)開(kāi)發(fā)尤其重要)

          當(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ū)別。

          JSX 其實(shí)并不是方言

          很多人弄錯(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,這做了很多的限制:

          • 一個(gè)文件只能寫(xiě)一個(gè)組件
          • 節(jié)點(diǎn)片段只能寫(xiě)在 template 里面,非常不靈活
          • 變量綁定只能獲取this上面的內(nèi)容,不能使用全局變量(很多時(shí)候我們都要把全局變量先掛載到this上)

          我們一點(diǎn)點(diǎn)來(lái)講

          一個(gè)文件只能寫(xiě)一個(gè)組件

          這個(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è)組件,你能想象這多了多少工作量么。

          節(jié)點(diǎn)片段只能寫(xiě)在 template 里面,非常不靈活

          我不知道有多少同學(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)單。

          變量綁定只能獲取this上面的內(nèi)容,不能使用全局變量

          這個(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)題:

          • 多了注冊(cè)組件這個(gè)步驟,增加代碼量
          • 通過(guò)字符串名注冊(cè)自然就會(huì)出現(xiàn)可能的沖突問(wèn)題
          • 模板解析組件支持不同的樣式,比如<MyComp>和<my-comp>,容易導(dǎo)致風(fēng)格不一的問(wèn)題

          在 JSX 中則沒(méi)有這些問(wèn)題,因?yàn)?JSX 里面直接使用組件引用作為參數(shù):

          const Comp={...}
          
          const App=()=> <Comp />
          

          需要通過(guò)directive來(lái)擴(kuò)展能力

          其實(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 操作嘛~

          總結(jié)

          以上就是我對(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-loader 之于 webpack
          • eslint-plugin-vue 之于 eslint
          • rollup-plugin-vue 之于 rollup
          • vue-jest 之于 jest
          • Vetur 用來(lái)做代碼提醒

          基本上常用的工具我們都需要等待 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ō)是一種遺憾。


          主站蜘蛛池模板: 日本精品一区二区在线播放 | 亚洲国产精品一区第二页| 在线成人综合色一区| 狠狠色成人一区二区三区| 成人毛片一区二区| 亚洲va乱码一区二区三区| 中文字幕精品亚洲无线码一区| 精品乱码一区二区三区在线| 中文乱码字幕高清一区二区| 中文字幕一区二区三区精彩视频 | 人妻无码一区二区三区免费| 精品日本一区二区三区在线观看| 无码人妻精品一区二区三区久久久| 人妻天天爽夜夜爽一区二区| 久久青草精品一区二区三区| 成人无码一区二区三区| 精品国产不卡一区二区三区| 无码毛片一区二区三区中文字幕 | 少妇激情av一区二区| 久久久久一区二区三区| 午夜福利av无码一区二区| 免费精品一区二区三区第35| 久久国产一区二区| 国产福利一区二区三区视频在线| 精品国产一区二区三区不卡 | 中文字幕精品一区二区| 亚洲av色香蕉一区二区三区| 成人一区专区在线观看| 精品一区二区AV天堂| 视频一区视频二区日韩专区| 亚洲国产一区二区三区| 中文字幕人妻丝袜乱一区三区 | 一区二区和激情视频| 精品少妇人妻AV一区二区 | 国产一区二区三区高清视频| 国产在线精品一区二区不卡| 亚洲一区精品视频在线 | 日韩精品无码一区二区中文字幕| 亚洲狠狠狠一区二区三区| 亚洲综合无码一区二区痴汉 | 精品一区二区三区中文|