整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          JavaScript 調用提速 40% 的實踐

          JavaScript 調用提速 40% 的實踐

          數適配器機制不僅復雜,而且成本很高。

          本文最初發表于 v8.dev(Faster JavaScript calls),基于 CC 3.0 協議分享,由 InfoQ 翻譯并發布。

          JavaScript 允許使用與預期形式參數數量不同的實際參數來調用一個函數,也就是傳遞的實參可以少于或者多于聲明的形參數量。前者稱為申請不足(under-application),后者稱為申請過度(over-application)。

          在申請不足的情況下,剩余形式參數會被分配 undefined 值。在申請過度的情況下,可以使用 rest 參數和 arguments 屬性訪問剩余實參,或者如果它們是多余的可以直接忽略。如今,許多 Web/Node.js 框架都使用這個 JS 特性來接受可選形參,并創建更靈活的 API。

          直到最近,V8 都有一種專門的機制來處理參數大小不匹配的情況:這種機制叫做參數適配器框架。不幸的是,參數適配是有性能成本的,但在現代的前端和中間件框架中這種成本往往是必須的。但事實證明,我們可以通過一個巧妙的技巧來拿掉這個多余的框架,簡化 V8 代碼庫并消除幾乎所有的開銷。

          我們可以通過一個微型基準測試來計算移除參數適配器框架可以獲得的性能收益。

          console.time();
          function f(x, y, z) {}
          for (let i=0; i <  N; i++) {
            f(1, 2, 3, 4, 5);
          }
          console.timeEnd();

          移除參數適配器框架的性能收益,通過一個微基準測試來得出。

          上圖顯示,在無 JIT 模式(Ignition)下運行時,開銷消失,并且性能提高了 11.2%。使用 TurboFan 時,我們的速度提高了 40%。

          這個微基準測試自然是為了最大程度地展現參數適配器框架的影響而設計的。但是,我們也在許多基準測試中看到了顯著的改進,例如我們內部的 JSTests/Array 基準測試(7%)和 Octane2(Richards 子項為 4.6%,EarleyBoyer 為 6.1%)。

          太長不看版:反轉參數

          這個項目的重點是移除參數適配器框架,這個框架在訪問棧中被調用者的參數時為其提供了一個一致的接口。為此,我們需要反轉棧中的參數,并在被調用者框架中添加一個包含實際參數計數的新插槽。下圖顯示了更改前后的典型框架示例。

          移除參數適配器框架之前和之后的典型 JavaScript 棧框架。


          加快 JavaScript 調用

          為了講清楚我們如何加快調用,首先我們來看看 V8 如何執行一個調用,以及參數適配器框架如何工作。

          當我們在 JS 中調用一個函數調用時,V8 內部會發生什么呢?用以下 JS 腳本為例:

          function add42(x) {
            return x + 42;
          }
          add42(3);

          在函數調用期間 V8 內部的執行流程。


          Ignition

          V8 是一個多層 VM。它的第一層稱為 Ignition,是一個具有累加器寄存器的字節碼棧機。V8 首先會將代碼編譯為 Ignition 字節碼。上面的調用被編譯為以下內容:

          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

          調用的第一個參數通常稱為接收器(receiver)。接收器是 JSFunction 中的 this 對象,并且每個 JS 函數調用都必須有一個 this。CallNoFeedback 的字節碼處理器需要使用寄存器列表 r2-r3 中的參數來調用對象 r1。

          在深入研究字節碼處理器之前,請先注意寄存器在字節碼中的編碼方式。它們是負的單字節整數:r1 編碼為 fa,r2 編碼為 f9,r3 編碼為 f8。我們可以將任何寄存器 ri 稱為 fb - i,實際上正如我們所見,正確的編碼是- 2 - kFixedFrameHeaderSize - i。寄存器列表使用第一個寄存器和列表的大小來編碼,因此 r2-r3 為 f9 02。

          Ignition 中有許多字節碼調用處理器。可以在此處查看它們的列表。它們彼此之間略有不同。有些字節碼針對 undefined 的接收器調用、屬性調用、具有固定數量的參數調用或通用調用進行了優化。在這里我們分析 CallNoFeedback,這是一個通用調用,在該調用中我們不會積累執行過程中的反饋。

          這個字節碼的處理器非常簡單。它是用 CodeStubAssembler 編寫的,你可以在此處查看。本質上,它會尾調用一個架構依賴的內置 InterpreterPushArgsThenCall。

          這個內置方法實際上是將返回地址彈出到一個臨時寄存器中,壓入所有參數(包括接收器),然后壓回該返回地址。此時,我們不知道被調用者是否是可調用對象,也不知道被調用者期望多少個參數,也就是它的形式參數數量。

          內置 InterpreterPushArgsThenCall 執行后的框架狀態。

          最終,執行會尾調用到內置的 Call。它會在那里檢查目標是否是適當的函數、構造器或任何可調用對象。它還會讀取共享 shared function info 結構以獲得其形式參數計數。

          如果被調用者是一個函數對象,它將對內置的 CallFunction 進行尾部調用,并在其中進行一系列檢查,包括是否有 undefined 對象作為接收器。如果我們有一個 undefined 或 null 對象作為接收器,則應根據 ECMA 規范對其修補,以引用全局代理對象。

          執行隨后會對內置的 InvokeFunctionCode 進行尾調用。在沒有參數不匹配的情況下,InvokeFunctionCode 只會調用被調用對象中字段 Code 所指向的內容。這可以是一個優化函數,也可以是內置的 InterpreterEntryTrampoline。

          如果我們假設要調用的函數尚未優化,則 Ignition trampoline 將設置一個 IntepreterFrame。你可以在此處查看V8 中框架類型的簡短摘要。

          接下來發生的事情就不用多談了,我們可以看一個被調用者執行期間的解釋器框架快照。

          我們看到框架中有固定數量的插槽:返回地址、前一個框架指針、上下文、我們正在執行的當前函數對象、該函數的字節碼數組以及我們當前正在執行的字節碼偏移量。最后,我們有一個專用于此函數的寄存器列表(你可以將它們視為函數局部變量)。add42 函數實際上沒有任何寄存器,但是調用者具有類似的框架,其中包含 3 個寄存器。

          如預期的那樣,add42 是一個簡單的函數:

          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

          請注意我們在 Ldar(Load Accumulator Register)字節碼中編碼參數的方式:參數 1(a0)用數字 02 編碼。實際上,任何參數的編碼規則都是[ai]=2 + parameter_count - i - 1,接收器[this]=2 + parameter_count,或者在本例中[this]=3。此處的參數計數不包括接收器。

          現在我們就能理解為什么用這種方式對寄存器和參數進行編碼。它們只是表示一個框架指針的偏移量。然后,我們可以用相同的方式處理參數/寄存器的加載和存儲。框架指針的最后一個參數偏移量為 2(先前的框架指針和返回地址)。這就解釋了編碼中的 2。解釋器框架的固定部分是 6 個插槽(4 個來自框架指針),因此寄存器零位于偏移量-5 處,也就是 fb,寄存器 1 位于 fa 處。很聰明是吧?

          但請注意,為了能夠訪問參數,該函數必須知道棧中有多少個參數!無論有多少參數,索引 2 都指向最后一個參數!

          Return 的字節碼處理器將調用內置的 LeaveInterpreterFrame 來完成。該內置函數本質上是從框架中讀取函數對象以獲取參數計數,彈出當前框架,恢復框架指針,將返回地址保存在一個暫存器中,根據參數計數彈出參數并跳轉到暫存器中的地址。

          這套流程很棒!但是,當我們調用一個實參數量少于或多于其形參數量的函數時,會發生什么呢?這個聰明的參數/寄存器訪問流程將失敗,我們該如何在調用結束時清理參數?

          參數適配器框架

          現在,我們使用更少或更多的實參來調用 add42:

          add42();
          add42(1, 2, 3);

          JS 開發人員會知道,在第一種情況下,x 將被分配 undefined,并且該函數將返回 undefined + 42=NaN。在第二種情況下,x 將被分配 1,函數將返回 43,其余參數將被忽略。請注意,調用者不知道是否會發生這種情況。即使調用者檢查了參數計數,被調用者也可以使用 rest 參數或 arguments 對象訪問其他所有參數。實際上,在 sloppy 模式下甚至可以在 add42 外部訪問 arguments 對象。

          如果我們執行與之前相同的步驟,則將首先調用內置的 InterpreterPushArgsThenCall。它將像這樣將參數推入棧:

          內置 InterpreterPushArgsThenCall 執行后的框架狀態。


          繼續與以前相同的過程,我們檢查被調用者是否為函數對象,獲取其參數計數,并將接收器補到全局代理。最終,我們到達了 InvokeFunctionCode。

          在這里我們不會跳轉到被調用者對象中的 Code。我們檢查參數大小和參數計數之間是否存在不匹配,然后跳轉到 ArgumentsAdaptorTrampoline。

          在這個內置組件中,我們構建了一個額外的框架,也就是臭名昭著的參數適配器框架。這里我不會解釋內置組件內部發生了什么,只會向你展示內置組件調用被調用者的 Code 之前的框架狀態。請注意,這是一個正確的 x64 call(不是 jmp),在被調用者執行之后,我們將返回到 ArgumentsAdaptorTrampoline。這與進行尾調用的 InvokeFunctionCode 正好相反。

          我們創建了另一個框架,該框架復制了所有必需的參數,以便在被調用者框架頂部精確地包含參數的形參計數。它創建了一個被調用者函數的接口,因此后者無需知道參數數量。被調用者將始終能夠使用與以前相同的計算結果來訪問其參數,即[ai]=2 + parameter_count - i - 1。

          V8 具有一些特殊的內置函數,它們在需要通過 rest 參數或 arguments 對象訪問其余參數時能夠理解適配器框架。它們始終需要檢查被調用者框架頂部的適配器框架類型,然后采取相應措施。

          如你所見,我們解決了參數/寄存器訪問問題,但是卻添加了很多復雜性。需要訪問所有參數的內置組件都需要了解并檢查適配器框架的存在。不僅如此,我們還需要注意不要訪問過時的舊數據。考慮對 add42 的以下更改:

          function add42(x) {
            x +=42;
            return x;
          }

          現在,字節碼數組為:

          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

          如你所見,我們現在修改 a0。因此,在調用 add42(1, 2, 3)的情況下,參數適配器框架中的插槽將被修改,但調用者框架仍將包含數字 1。我們需要注意,參數對象正在訪問修改后的值,而不是舊值。

          從函數返回很簡單,只是會很慢。還記得 LeaveInterpreterFrame 做什么嗎?它基本上會彈出被調用者框架和參數,直到到達最大形參計數為止。因此,當我們返回參數適配器存根時,棧如下所示:

          被調用者 add42 執行之后的框架狀態。

          我們需要彈出參數數量,彈出適配器框架,根據實際參數計數彈出所有參數,然后返回到調用者執行。

          簡單總結:參數適配器機制不僅復雜,而且成本很高。

          移除參數適配器框架

          我們可以做得更好嗎?我們可以移除適配器框架嗎?事實證明我們確實可以。

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


          1. 我們需要能夠像以前一樣無縫訪問參數和寄存器。訪問它們時無法進行檢查。那成本太高了。
          2. 我們需要能夠從棧中構造 rest 參數和 arguments 對象。
          3. 從一個調用返回時,我們需要能夠輕松清理未知數量的參數。
          4. 此外,當然我們希望沒有額外的框架!

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

          被調用者框架中的參數

          假設我們將參數放在被調用者框架中。這似乎是一個好主意,因為無論何時彈出框架,我們都會一次彈出所有參數!

          參數必須位于保存的框架指針和框架末尾之間的某個位置。這就要求框架的大小不會被靜態地知曉。訪問參數仍然很容易,它就是一個來自框架指針的簡單偏移量。但現在訪問寄存器要復雜得多,因為它會根據參數的數量而變化。

          棧指針總是指向最后一個寄存器,然后我們可以使用它來訪問寄存器而無需知道參數計數。這種方法可能行得通,但它有一個關鍵缺陷。它需要復制所有可以訪問寄存器和參數的字節碼。我們將需要 LdaArgument 和 LdaRegister,而不是簡單的 Ldar。當然,我們還可以檢查我們是否正在訪問一個參數或寄存器(正或負偏移量),但這將需要檢查每個參數和寄存器訪問。顯然這種方法太昂貴了!

          調用者框架中的參數

          好的,如果我們在調用者框架中放參數呢?

          記住如何計算一個框架中參數 i 的偏移量:[ai]=2 + parameter_count - i - 1。如果我們擁有所有參數(不僅是形式參數),則偏移量將為[ai]=2 + parameter_count - i - 1.也就是說,對于每個參數訪問,我們都需要加載實際的參數計數。

          但如果我們反轉參數會發生什么呢?現在可以簡單地將偏移量計算為[ai]=2 + i。我們不需要知道棧中有多少個參數,但如果我們可以保證棧中至少有形參計數那么多的參數,那么我們就能一直使用這種方案來計算偏移量。

          換句話說,壓入棧的參數數量將始終是參數數量和形參數量之間的最大值,并且在需要時使用 undefined 對象進行填充。

          這還有另一個好處!對于任何 JS 函數,接收器始終位于相同的偏移量處,就在返回地址的正上方:[this]=2。

          對于我們的第 1 和第 4 條要求,這是一個干凈的解決方案。另外兩個要求又如何呢?我們如何構造 rest 參數和 arguments 對象?返回調用者時如何清理棧中的參數?為此,我們缺少的只是參數計數而已。我們需要將其保存在某個地方。只要可以輕松訪問此信息即可,具體怎么做沒那么多限制。兩種基本選項分別是:將其推送到調用者框架中的接收者之后,或被調用者框架中的固定標頭部分。我們實現了后者,因為它合并了 Interpreter 和 Optimized 框架的固定標頭部分。


          如果在 V8 v8.9 中運行前面的示例,則在 InterpreterArgsThenPush 之后將看到以下棧(請注意,現在參數已反轉):

          內置 InterpreterPushArgsThenCall 執行后的框架狀態。

          所有執行都遵循類似的路徑,直到到達 InvokeFunctionCode。在這里,我們在申請不足的情況下處理參數,根據需要推送盡可能多的 undefined 對象。請注意,在申請過度的情況下,我們不會進行任何更改。最后,我們通過一個寄存器將參數數量傳遞給被調用者的 Code。在 x64 的情況下,我們使用寄存器 rax。

          如果被調用者尚未進行優化,我們將到達 InterpreterEntryTrampoline,它會構建以下棧框架。

          沒有參數適配器的棧框架。

          被調用者框架有一個額外的插槽,其中包含的參數計數可用于構造 rest 參數或 arguments 對象,并在返回到調用者之前清除棧中參數。

          返回時,我們修改 LeaveInterpreterFrame 以讀取棧中的參數計數,并彈出參數計數和形式參數計數之間的較大數字。

          TurboFan

          那么代碼優化呢?我們來稍微更改一下初始腳本,以強制 V8 使用 TurboFan 對其進行編譯:

          function add42(x) { return x + 42; }
          function callAdd42() { add42(3); }
          %PrepareFunctionForOptimization(callAdd42);
          callAdd42();
          %OptimizeFunctionOnNextCall(callAdd42);
          callAdd42();

          在這里,我們使用 V8 內部函數來強制 V8 優化調用,否則 V8 僅在我們的小函數變熱(經常使用)時才對其進行優化。我們在優化之前調用它一次,以收集一些可用于指導編譯的類型信息。在此處閱讀有關 TurboFan 的更多信息(https://v8.dev/docs/turbofan)。

          這里,我只展示與主題相關的部分生成代碼。

          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!

          盡管這段代碼使用了匯編來編寫,但如果你仔細看我的注釋應該很容易能懂。本質上,在編譯調用時,TF 需要完成之前在 InterpreterPushArgsThenCall、Call、CallFunction 和 InvokeFunctionCall 內置組件中完成的所有工作。它應該會有更多的靜態信息來執行此操作并發出更少的計算機指令。

          帶參數適配器框架的 TurboFan

          現在,讓我們來看看參數數量和參數計數不匹配的情況。考慮調用 add42(1, 2, 3)。它會編譯為:

          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

          如你所見,不難為 TF 添加對參數和參數計數不匹配的支持。只需調用參數適配器 trampoline 即可!

          然而這種方法成本很高。對于每個優化的調用,我們現在都需要進入參數適配器 trampoline,并像未優化的代碼一樣處理框架。這就解釋了為什么在優化的代碼中移除適配器框架的性能收益比在 Ignition 上大得多。

          但是,生成的代碼非常簡單。從中返回非常容易(結尾):

          movq rsp,rbp   ;; Clean callee frame
          pop rbp
          ret 0x8        ;; Pops a single argument (the receiver)

          我們彈出框架并根據參數計數發出一個返回指令。如果實參計數和形參計數不匹配,則適配器框架 trampoline 將對其進行處理。

          沒有參數適配器框架的 TurboFan

          生成的代碼本質上與參數計數匹配的調用代碼相同。考慮調用 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!

          該函數的結尾如何?我們不再回到參數適配器 trampoline 了,因此結尾確實比以前復雜了一些。

          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

          小結

          參數適配器框架是一個臨時解決方案,用于實際參數和形式參數計數不匹配的調用。這是一個簡單的解決方案,但它帶來了很高的性能成本,并增加了代碼庫的復雜性。如今,許多 Web 框架使用這一特性來創建更靈活的 API,結果帶來了更高的性能成本。反轉棧中參數這個簡單的想法可以大大降低實現復雜性,并消除了此類調用的幾乎所有開銷。

          原文鏈接:

          https://v8.dev/blog/adaptor-frame

          延伸閱讀:

          Deno 2020 年大事記-InfoQ

          關注我并轉發此篇文章,即可獲得學習資料~若想了解更多,也可移步InfoQ官網,獲取InfoQ最新資訊~

          • 現在很多App里都內置了Web網頁(Hybrid App),比如說很多電商平臺,淘寶、京東、聚劃算等等,如下圖

          • 上述功能是由Android的WebView實現的,其中涉及到Android客戶端與Web網頁交互的實現
          • 今天我將全面介紹Android通過WebView與JS交互的全面方式

          閱讀本文前請先閱讀:Android開發:最全面、最易懂的Webview詳解


          目錄


          1. 交互方式總結

          Android與JS通過WebView互相調用方法,實際上是:

          • Android去調用JS的代碼
          • JS去調用Android的代碼

          二者溝通的橋梁是WebView

          對于Android調用JS代碼的方法有2種:

          1. 通過WebView的loadUrl()

          2. 通過WebView的evaluateJavascript()

          對于JS調用Android代碼的方法有3種:

          1. 通過WebView的addJavascriptInterface()進行對象映射

          2. 通過 WebViewClient 的shouldOverrideUrlLoading ()方法回調攔截 url

          3. 通過 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調攔截JS對話框alert()、confirm()、prompt() 消息


          2. 具體分析

          2.1 Android通過WebView調用 JS 代碼

          對于Android調用JS代碼的方法有2種:

          1. 通過WebView的loadUrl()

          2. 通過WebView的evaluateJavascript()

          方式1:通過WebView的loadUrl()

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

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

          • 為了方便展示,本文是采用Andorid調用本地JS代碼說明;
          • 實際情況時,Android更多的是調用遠程JS代碼,即將加載的JS代碼路徑改成url即可

          需要加載JS代碼:javascript.html

          // 文本名:javascript

          <!DOCTYPE html>

          <html>

          <head>

          <meta charset="utf-8">

          <title>Carson_Ho</title>

          // JS代碼

          <script>

          // Android需要調用的方法

          function callJS(){

          alert("Android調用了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里通過WebView設置調用JS代碼

          Android代碼: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();

          // 設置與Js交互的權限

          webSettings.setJavaScriptEnabled(true);

          // 設置允許JS彈窗

          webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

          // 先載入JS代碼

          // 格式規定為: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) {

          // 通過Handler發送消息

          mWebView.post(new Runnable() {

          @Override

          public void run() {

          // 注意調用的JS方法名要對應上

          // 調用javascript的callJS()方法

          mWebView.loadUrl("javascript:callJS()");

          }

          });

          }

          });

          // 由于設置了彈窗檢驗調用結果,所以需要支持js對話框

          // webview只是載體,內容的渲染需要使用webviewChromClient類去實現

          // 通過設置WebChromeClient對象處理JavaScript的對話框

          //設置響應js 的Alert()函數

          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代碼調用一定要在 onPageFinished() 回調之后才能調用,否則不會調用。

          onPageFinished()屬于WebViewClient類的方法,主要在頁面加載結束時調用

          方式2:通過WebView的evaluateJavascript()

          • 優點:該方法比第一種方法效率更高、使用更簡潔。
          • 因為該方法的執行不會使頁面刷新,而第一種方法(loadUrl )的執行則會。
          • Android 4.4 后才可使用
          • 具體使用

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

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

          @Override

          public void onReceiveValue(String value) {

          //此處為 js 返回的結果

          }

          });

          }

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

          2.1.2 方法對比

          2.1.3 使用建議

          兩種方法混合使用,即Android 4.4以下使用方法1,Android 4.4以上方法2

          // Android版本變量

          final int version=Build.VERSION.SDK_INT;

          // 因為該方法在 Android 4.4 版本才可使用,所以使用時需進行版本判斷

          if (version < 18) {

          mWebView.loadUrl("javascript:callJS()");

          } else {

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

          @Override

          public void onReceiveValue(String value) {

          //此處為 js 返回的結果

          }

          });

          }

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

          2.2 JS通過WebView調用 Android 代碼

          對于JS調用Android代碼的方法有3種:

          1. 通過WebView的addJavascriptInterface()進行對象映射

          2. 通過 WebViewClient 的shouldOverrideUrlLoading ()方法回調攔截 url

          3. 通過 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調攔截JS對話框alert()、confirm()、prompt() 消息

          2.2.1 方法分析

          方式1:通過 WebView的addJavascriptInterface()進行對象映射

          步驟1:定義一個與JS對象映射關系的Android類:AndroidtoJs

          AndroidtoJs.java(注釋已經非常清楚)

          // 繼承自Object類

          public class AndroidtoJs extends Object {

          // 定義JS需要調用的方法

          // 被JS調用的方法必須加入@JavascriptInterface注解

          @JavascriptInterface

          public void hello(String msg) {

          System.out.println("JS調用了Android的hello方法");

          }

          }

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

          步驟2:將需要調用的JS代碼以.html格式放到src/main/assets文件夾里

          需要加載JS代碼:javascript.html

          <!DOCTYPE html>

          <html>

          <head>

          <meta charset="utf-8">

          <title>Carson</title>

          <script>

          function callAndroid(){

          // 由于對象映射,所以調用test對象等于調用Android映射的對象

          test.hello("js調用了android中的hello方法");

          }

          </script>

          </head>

          <body>

          //點擊按鈕則調用callAndroid函數

          <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里通過WebView設置Android類與JS代碼的映射

          詳細請看注釋

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

          // 設置與Js交互的權限

          webSettings.setJavaScriptEnabled(true);

          // 通過addJavascriptInterface()將Java對象映射到JS對象

          //參數1:Javascript對象名

          //參數2:Java對象名

          mWebView.addJavascriptInterface(new AndroidtoJs(), "test");//AndroidtoJS類對象映射到js的test對象

          // 加載JS代碼

          // 格式規定為: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

          特點

          • 優點:使用簡單

          僅將Android對象和JS對象映射即可

          • 缺點:存在嚴重的漏洞問題,具體請看文章:你不知道的 Android WebView 使用漏洞

          方式2:通過 WebViewClient 的方法shouldOverrideUrlLoading ()回調攔截 url

          • 具體原理:
          1. Android通過 WebViewClient 的回調方法shouldOverrideUrlLoading ()攔截 url
          2. 解析該 url 的協議
          3. 如果檢測到是預先約定好的協議,就調用相應方法

          即JS需要調用Android的方法

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

          以.html格式放到src/main/assets文件夾里

          <!DOCTYPE html>

          <html>

          <head>

          <meta charset="utf-8">

          <title>Carson_Ho</title>

          <script>

          function callAndroid(){

          /*約定的url協議為:js://webview?arg1=111&arg2=222*/

          document.location="js://webview?arg1=111&arg2=222";

          }

          </script>

          </head>

          <!-- 點擊按鈕則調用callAndroid()方法 -->

          <body>

          <button type="button" id="button1" onclick="callAndroid()">點擊調用Android代碼</button>

          </body>

          </html>

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

          當該JS通過Android的mWebView.loadUrl("file:///android_asset/javascript.html")加載后,就會回調shouldOverrideUrlLoading (),接下來繼續看步驟2:

          步驟2:在Android通過WebViewClient復寫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();

          // 設置與Js交互的權限

          webSettings.setJavaScriptEnabled(true);

          // 設置允許JS彈窗

          webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

          // 步驟1:加載JS代碼

          // 格式規定為:file:///android_asset/文件名.html

          mWebView.loadUrl("file:///android_asset/javascript.html");

          // 復寫WebViewClient類的shouldOverrideUrlLoading方法

          mWebView.setWebViewClient(new WebViewClient() {

          @Override

          public boolean shouldOverrideUrlLoading(WebView view, String url) {

          // 步驟2:根據協議的參數,判斷是否是所需要的url

          // 一般根據scheme(協議格式) & authority(協議名)判斷(前兩個參數)

          //假定傳入進來的 url="js://webview?arg1=111&arg2=222"(同時也是約定好的需要攔截的)

          Uri uri=Uri.parse(url);

          // 如果url的協議=預先約定的 js 協議

          // 就解析往下解析參數

          if ( uri.getScheme().equals("js")) {

          // 如果 authority=預先約定協議里的 webview,即代表都符合約定的協議

          // 所以攔截url,下面JS開始調用Android需要的方法

          if (uri.getAuthority().equals("webview")) {

          // 步驟3:

          // 執行JS所需要調用的邏輯

          System.out.println("js調用了Android的方法");

          // 可以在協議上帶有參數并傳遞到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

          特點

          • 優點:不存在方式1的漏洞;
          • 缺點:JS獲取Android方法的返回值復雜。

          如果JS想要得到Android方法的返回值,只能通過 WebView 的 loadUrl ()去執行 JS 方法把返回值傳遞回去,相關的代碼如下:

          // 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:通過 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調攔截JS對話框alert()、confirm()、prompt() 消息

          在JS中,有三個常用的對話框方法:

          方式3的原理:Android通過 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回調分別攔截JS對話框

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

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

          • 常用的攔截是:攔截 JS的輸入框(即prompt()方法)
          • 因為只有prompt()可以返回任意類型的值,操作最全面方便、更加靈活;而alert()對話框沒有返回值;confirm()對話框只能返回兩種狀態(確定 / 取消)兩個值

          步驟1:加載JS代碼,如下:

          javascript.html

          以.html格式放到src/main/assets文件夾里

          <!DOCTYPE html>

          <html>

          <head>

          <meta charset="utf-8">

          <title>Carson_Ho</title>

          <script>

          function clickprompt(){

          // 調用prompt()

          var result=prompt("js://demo?arg1=111&arg2=222");

          alert("demo " + result);

          }

          </script>

          </head>

          <!-- 點擊按鈕則調用clickprompt() -->

          <body>

          <button type="button" id="button1" onclick="clickprompt()">點擊調用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

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

          • 如果是攔截警告框(即alert()),則觸發回調onJsAlert();
          • 如果是攔截確認框(即confirm()),則觸發回調onJsConfirm();

          步驟2:在Android通過WebChromeClient復寫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();

          // 設置與Js交互的權限

          webSettings.setJavaScriptEnabled(true);

          // 設置允許JS彈窗

          webSettings.setJavaScriptCanOpenWindowsAutomatically(true);

          // 先加載JS代碼

          // 格式規定為:file:///android_asset/文件名.html

          mWebView.loadUrl("file:///android_asset/javascript.html");

          mWebView.setWebChromeClient(new WebChromeClient() {

          // 攔截輸入框(原理同方式2)

          // 參數message:代表promt()的內容(不是url)

          // 參數result:代表輸入框的返回值

          @Override

          public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {

          // 根據協議的參數,判斷是否是所需要的url(原理同方式2)

          // 一般根據scheme(協議格式) & authority(協議名)判斷(前兩個參數)

          //假定傳入進來的 url="js://webview?arg1=111&arg2=222"(同時也是約定好的需要攔截的)

          Uri uri=Uri.parse(message);

          // 如果url的協議=預先約定的 js 協議

          // 就解析往下解析參數

          if ( uri.getScheme().equals("js")) {

          // 如果 authority=預先約定協議里的 webview,即代表都符合約定的協議

          // 所以攔截url,下面JS開始調用Android需要的方法

          if (uri.getAuthority().equals("webview")) {

          //

          // 執行JS所需要調用的邏輯

          System.out.println("js調用了Android的方法");

          // 可以在協議上帶有參數并傳遞到Android上

          HashMap<String, String> params=new HashMap<>();

          Set<String> collection=uri.getQueryParameterNames();

          //參數result:代表消息框的返回值(輸入值)

          result.confirm("js調用了Android的方法成功啦");

          }

          return true;

          }

          return super.onJsPrompt(view, url, message, defaultValue, result);

          }

          // 通過alert()和confirm()攔截的原理相同,此處不作過多講述

          // 攔截JS的警告框

          @Override

          public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

          return super.onJsAlert(view, url, message, result);

          }

          // 攔截JS的確認框

          @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 三種方式的對比 & 使用場景


          3. 總結

          • 本文主要對Android通過WebView與JS的交互方式進行了全面介紹

          • 關于WebView的系列文章對你有所幫助
          • Android開發:最全面、最易懂的Webview詳解
          • Android:你不知道的 WebView 使用漏洞
          • 手把手教你構建 Android WebView 的緩存機制 & 資源預加載方案
          • 接下來我會繼續講解其他安卓開發的知識,有興趣可以繼續關注Carson_Ho的安卓開發筆記!!!!

          請評論點贊!因為你們的贊同/鼓勵是我寫作的最大動力!

          很長的一段時間中,Vue 官方都以簡單上手作為其推廣的重點。這確實給 Vue 帶來了非常大的用戶量,尤其是最追求需求開發效率, 往往不那么在意工程代碼質量的國內中小企業中,Vue 占據的份額極速增長。但是作為開發者自身,我們必須要認清一個重點,簡單易用從來不應該在技術選型中占據很大的份額,可維護性才是。

          以防萬一有的同學實在不看官方文檔,我先提一嘴,SFC 就是寫 Vue 組件的時候寫的.vue文件,這一個文件就是一個 SFC,全稱 Single File Component,也即單文件組件。

          在開始說我個人的觀點之前,我們先來看幾個事實:

          一是:Vue3 的定義原生支持 JSX,并且 Vue3 源碼中有jsx.d.ts來便于使用 JSX。 不知道同學們看到這里會想到什么, 我的第一反應是:社區對于 JSX 的需求聲音是不小的,所以會反向推動 Vue3 官方對于 JSX 的支持。

          二是:AntDesign 的 vue3 版本,基本全部都是用 JSX 開發的,而且 Vue3 現在官方的 babel-jsx 插件就是阿里的人一開始維護的, 雖然我向來不喜歡阿里系的 KPI 推動技術方式,而且現在的 JSX 語法支持也不是很符合我的期望,但至少在使用 JSX 開發是更優秀的選擇這點上,我還是很認可 AntDesign 團隊的。

          OK,說這些呢,主要是先擺出一些事實作為依據,讓有些同學可以不需要拿什么:

          • 啊,這都是你空想的,你太自以為是了
          • 你再怎么想都沒用,咱們 Vue 就是應該用 SFC 開發

          這些觀點來批斗我,首先我都會從客觀的角度來分析為什么,至少是我是能講出優劣勢的理由的。

          OK,前言差不多到這里,接下來咱給您分析分析,為什么你應該選擇 JSX 來開發 Vue。


          TypeScript 支持

          其實第一點就已經是殺手了,對于想要使用 TypeScript 來開發 Vue3 應用的同學來說,這簡直就是 SFC 無法克服的世界難題。

          一句話概括:TypeScript 原生支持 JSX 語法,而基本無望 TS 官方能支持 SFC 的 template 語法

          TS 毫無疑問在前端社區的重要性越來越大,但凡未來對于代碼質量有一定要求的前端團隊,都應該會選擇使用 TS 來進行開發。 而且現在基本上在 NPM 上都能看到包你都能找到對應的 TS 定義,現在使用 TS 開發成本已經只剩下你是不是會 TS 語法了,在這種情況下是否支持 TS 則是開發模式在未來走不走的遠的重要原因。

          目前 SFC 只能通過shim讓 TS 可以引入.vue文件,但是對于所有 SFC 的組件的定義都是一樣的:

          declare module '*.vue' {
              import { DefineComponent } from 'vue'
              const component: DefineComponent<{}, {}, {}, any>
              export default component
          }
          

          也就是說你引入的 SFC 組件,TS 是不知道這個組件的 Props 應該接收什么的。所以你無法享受到這些 TS 的優勢:

          • 開發時的自動提示
          • 編譯時的 TS 校驗,讓你盡早發現問題
          • 編譯組件生成你的組件定義(對于類庫開發尤其重要)

          當然你會說既然 Vue 官方能開發處 SFC 的語法,自然會支持這些特性。我表示這當然有可能,但是這個難度是非常大的,需要很多方面的支持,甚至可能需要 TS 官方團隊愿意協助, 但是我想不到 TS 官方有什么理由來支持 SFC,因為這只是 Vue 自己創建的方言,在其他場景下是沒有使用的,TS 是面向全社區的,我覺得他們不會考慮主動來支持 SFC。

          那么有同學要問了,JSX 不也是非原生的 JS 語法么,他怎么就能讓 TS 官方支持了呢,是不是 FB 和微硬之間有什么 PY 交易?

          這就涉及第二點了,JSX 和靜態模板的靈活性區別。

          JSX 其實并不是方言

          很多人弄錯了一個問題,就是覺得 SFC 的模板語法和 JSX 是一樣的,都是一種別人發明的語法,并不是 JS 原生的。這是事實,但又有一些區別,這個區別主要是體現在對于 JSX 的認知上。

          一句話概括:JSX 并沒有擴展 JS 的語法,他只是縮略了 JS 的寫法!其本質就是 JS 的語法糖

          就像 es6 給增加的語法糖,比如

          const a=1
          const b=2
          
          const obj={ a, b }
          
          // 其實就等價于
          const obj={ a: a, b: b }
          

          這種寫法并沒有擴展 JS 的能力,只是簡便了寫法,JSX 也是一樣的。

          JSX 其實就是方法調用,他和 JS 是有一對一對應關系的,我們來看一個例子:

          const element=<div id="root">Hello World</div>
          

          這里的 JSX 語法編譯之后其實就是:

          const element=createElement('div', { id: 'root' }, 'Hello World')
          

          而 JSX 就是這些了,沒有什么更多的內容,所以說 JSX 只是方便我們寫嵌套的函數調用的語法糖,而其本身沒有擴展任何其他的內容。

          但是 SFC 就不一樣了。

          SFC 定義的不僅是語法,更是文件。

          SFC 的具體定義是單文件組件,它本身就是把一個文件看作一個單位,所以他的約束性是要大很多的,你必須具有固定的文件結構才能使用 SFC,這做了很多的限制:

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

          我們一點點來講

          一個文件只能寫一個組件

          這個說實話非常非常不方便,很多時候我們寫一個頁面的時候其實經常會需要把一些小的節點片段拆分到小組件里面進行復用(如果你現在沒有這個習慣可能就是因為 SFC 的限制讓你習慣了全部寫在一個文件內)。

          React 生態中豐富的 css-in-js 方案就是很好的例子,我們可以通過:

          const StyledButton=styled('button', {
              color: 'red',
          })
          

          如果我們這個頁面需要使用特定樣式的按鈕,通過這種方式在頁面文件里面封裝一下是非常常見的。因為沒必要把這個組件拆分出去,他也不是一個可復用的組件,拆分出去了還要多一次import。

          Vue 生態基本沒有 css-in-js 的成熟方案其實跟這個限制也很有關系。

          再來一個例子,比如我們封裝了一個 Input 組件,我們希望同時導出 Password 組件和 Textarea 組件來方便用戶根據實際需求使用,而這兩個組件本身內部就是用的 Input 組件,只是定制了一些 props:

          const Input={ ... }
          
          export default Input
          
          export const Textarea=(props)=> <Input multiline={true} {...props} />
          
          export const Password=(props)=> <Input type="password" {...props} />
          

          在 JSX 中可以非常簡單地實現,但是如果通過 SFC,你可能就要強行拆成三個文件,另外為了方便,你可能還要增加一個index.js來導出這三個組件,你能想象這多了多少工作量么。

          節點片段只能寫在 template 里面,非常不靈活

          我不知道有多少同學看過 Vue 的 template 編譯出來之后的代碼,以我的經驗來說看過的可能不會超過 50%(樂觀估計),建議同學們如果還不了解的,可以去嘗試看一下。

          為什么要看這個呢?因為你看了之后你會發現,你在 template 里面寫的類似 HTMl 的內容,其實跟 HTML 根本沒啥關系,他們也會被編譯成類似 JSX 編譯出來的結果。

          {
              render(h) {
                  return h('div', {on: {}, props: {}}, h('span'))
              }
          }
          

          類似這樣的結果,而這里面h函數調用的結果就是一個 VNode,是 Vue 中的節點的基礎單元。那么既然這些單元就是一個對象,其實理所當然的,他們是可以作為參數傳遞的。 也就是說,理論上他們是可以通過props把節點當作參數傳遞給其他組件的。

          這個做法在 React 中非常常見,叫做renderProps,并且其非常靈活:

          const Comp=()=> <Layout header={<MyHeader />} footer={<MyFooter />} />
          

          但是因為 SFC 模板的限制,我們很難在 SFC 里面的 props 上寫節點:

          <template>
              <Layout :header="<MyHeader/>"></Layout>
          </template>
          

          這樣寫是不行的,因為 SFC 定義了:header綁定接受的只能是 js 表達式,而<MyHeader/>顯然不是。

          因為通過 props 傳遞不行,所以 Vue 才發明了 slot 插槽的概念

          雖然我們一直再說 Vue 簡單,但是事實上ScopedSlots一度成為新手理解 Vue 的噩夢,很多同學都被這個繞來繞去的作用域整的死去活來。

          我們看一個ScopedSlots的例子:

          <template>
              <Comp>
                  <template v-slot:scope="ctx">
                      <div>{{ctx.name}}</div>
                  </template>
              </Comp>
          </template>
          

          這里ctx是Comp里面的屬性,通過這種方式傳遞出來,讓我們在當前組件可以調用父組件里面的屬性。這簡直就是理解的噩夢,但是如果用 JSX 實現類似功能就非常簡單:

          <Comp scope={name=> <div>{name}</div>} />
          

          我們只是給一個叫做scope的 props 傳遞來一個函數,這個函數接受一個name屬性,在Comp里面會調用這個函數并傳入name。 簡單來說我們傳入的就是一個構建節點片段的函數,就是這么簡單。

          這就是因為 SFC 的模板的限制,導致靈活性不足,Vue 需要去創造概念,創造關鍵字來抹平這些能力的不足,而創造的概念自然就引入了學習成本。

          所以其實我一直不認可 Vue 比 React 好學的說法的,如果你真的認真研究所有用法,并且總是嘗試用最合理的方式實現功能,那么 Vue 絕對不會比 React 簡單。

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

          這個體現在兩個方面,一個是我們定義在全局的一些固定數據如果要在組件內使用的話,就要通過this掛載到組件上。

          比如我們緩存了一份城市數據,這種數據基本上是不會改的,所以也沒必要掛載到組件上讓其能夠響應式。但是在 SFC 里面這是做不到的, 因為模板的執行上下文是在編譯時綁定。你在模板里面訪問的變量,都會在編譯時自動綁定到this上,因為模板需要編譯,其本身也是字符串不具有作用域的概念。

          而這在 JSX 中則不復存在:

          const citys=[]
          
          const Comp=()=> {
              return citys.map(c=> <div>{c}</div>)
          }
          

          另外一個方面則是在組件使用上,在 SFC 中,組件必須事先注冊,因為我們在模板里面寫的只能是字符串而不能是具體某個組件變量。 那么模板中的組件和真實的組件對象只能通過字符串匹配來實現綁定。這帶來了以下問題:

          • 多了注冊組件這個步驟,增加代碼量
          • 通過字符串名注冊自然就會出現可能的沖突問題
          • 模板解析組件支持不同的樣式,比如<MyComp>和<my-comp>,容易導致風格不一的問題

          在 JSX 中則沒有這些問題,因為 JSX 里面直接使用組件引用作為參數:

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

          需要通過directive來擴展能力

          其實上面能看出來,除了 SFC 本身的問題之外,Vue 使用字符串模板也會帶來很多的靈活性問題。 最直接的證據,就是 Vue 使用了directive來擴展功能(當然這不是 Vue 發明的,老早的模板引擎就有類似問題)。

          為什么說directive是不得已的選擇呢?因為靜態模板缺失邏輯處理的能力。我們拿列表循環舉例,在 JS 中我們可以非常方便地通過map函數來創建列表:

          const list=arr.map(name=> <span key={name}>{name}</span>)
          

          而因為 JSX 本身就是函數調用,所以上面的代碼和 JSX 結合起來也非常自然:

          const App=()=> (
              <div>
                  <Header />
                  {arr.map(name=> (
                      <span key={name}>{name}</span>
                  ))}
              </div>
          )
          

          上面的例子對應到 JS 如下:

          const App=()=>
              createElement('div', {}, [
                  <Header />,
                  arr.map(name=> createElement('span', { key: name }, name)),
              ])
          

          這仍然是因為 JSX 只是 JS 的語法糖的原因,所有能在 JS 中實現的在 JSX 里面都能實現。

          而 SFC 的模板是基于字符串編譯的,其本身就是一段字符串,我們不能直接在模板里面寫map來循環節點,(當然我們可以在可以接收表達式的地方寫,比如v-on里面)。

          那么我們不能循環節點,有需要這樣的功能來渲染列表,怎么辦呢?就是發明一個標志來告訴編譯器這里需要循環,在 Vue 中的體現就是v-for指令。

          同學們可能要問了,既然 Vue 能實現v-for,為什么不直接實現表達式循環列表呢?他當然也可以實現,但是他肯定不會這么選,因為成本太高了。 他要這么做就相當于他要實現一個 JS 引擎,而其實里面很多內容又是不必須的,一個v-for其實就能夠適用大部分情況了。

          但有了v-for就需要v-if,那么后面還會需要其他各種能力,這就是一種方言的產生和發展的過程。

          當然指令也不僅僅是 JS 表達式的代替品,其本身也是增加了一些其他能力的,比如它能夠讓我們更方便地訪問 DOM 節點, 但是嘛,我們用框架的理由不就是為了能夠盡可能的屏蔽 DOM 操作嘛~

          總結

          以上就是我對應該選擇使用 JSX 還是 SFC 進行開發的分析,其實歸根到底 SFC 的問題在于其沒有擁抱 JS, 他的語法是自己發明的,他需要有一個 JS 實現的 compiler 來讓其最終能在 JS 環境中運行,這本質上就是一種發明, 我們不能否認發明確實有優點,但我們也不能只看有點不看問題,沒能擁抱 JS 自然就很難完全復用 JS 社區的優勢 而 JS 社區一直在蓬勃發展,好用的工具一直在涌現,而 SFC 想要使用 JS 社區的這些工具還要自己再實現一份,我們可以細數以下 SFC 做了哪些兼容

          • vue-loader 之于 webpack
          • eslint-plugin-vue 之于 eslint
          • rollup-plugin-vue 之于 rollup
          • vue-jest 之于 jest
          • Vetur 用來做代碼提醒

          基本上常用的工具我們都需要等待 Vue 社區或者官方開發了插件之后才能運行。而 JSX 因為有 babel 和 typescript 的官方支持, 基本上所有新的 JS 生態工具原生都是支持的。

          在這 Vue3 開始預備發力的階段,我們還是希望 Vue 社區能夠使用更優秀更規范的方式來進行開發, 其實如果我們直接使用 JSX 開發 Vue3,我們會發現很多時候我們都不需要用到emit、attrs這些概念, 甚至如果 Vue3 的 JSX 插件支持,我們甚至能夠拋棄slots。

          但是因為 Vue3 一定要考慮兼容 Vue2,導致本身潛力很好的 Vue3 總是顯得縮手縮腳,這不得不說是一種遺憾。


          主站蜘蛛池模板: 在线视频一区二区三区| 蜜桃无码一区二区三区| 亚洲乱码日产一区三区| 精品国产日韩亚洲一区在线| 无码人妻精品一区二区三区夜夜嗨| 色欲AV蜜桃一区二区三| 精品人妻AV一区二区三区| 中文字幕一区二区人妻性色| 久久久99精品一区二区| 白丝爆浆18禁一区二区三区 | 久久无码一区二区三区少妇 | 久久精品国产一区二区三区不卡 | 亚洲视频在线一区二区| 国产肥熟女视频一区二区三区| 亚洲午夜精品一区二区麻豆| 国产香蕉一区二区在线网站| 精品人妻一区二区三区浪潮在线 | 国产精品av一区二区三区不卡蜜| 99久久无码一区人妻a黑| 亚洲综合一区二区精品导航| 亚洲国产国产综合一区首页| 日本免费电影一区二区| 无码精品人妻一区二区三区中| 亚洲一区二区三区无码中文字幕| 国产亚洲自拍一区| 无码日韩精品一区二区三区免费| 国产精品特级毛片一区二区三区| 日本一区二区不卡在线| 色窝窝无码一区二区三区色欲| 无码精品视频一区二区三区| 午夜影院一区二区| 韩国福利视频一区二区| V一区无码内射国产| 国产精品无圣光一区二区| 久久久久人妻一区精品色| 亚洲AV无码一区二区乱子仑 | 亚洲一区影音先锋色资源| 国产91精品一区二区麻豆网站| 在线视频一区二区三区四区| 一级毛片完整版免费播放一区| 亚洲色精品vr一区二区三区|