美元符號 $

Qwik 會將您的應用程式拆分成許多小片段,我們稱之為「符號」。一個元件可以被拆分成多個符號,因此符號比元件更小。拆分動作是由 Qwik 優化器 執行的。

$ 後綴用於在進行此轉換時向優化器和開發人員發出信號。作為開發人員,您需要了解每當看到 $ 時,都會套用特殊的規則(並非所有有效的 JavaScript 都是有效的 Qwik 優化器轉換)。

編譯時期的含義

優化器 在打包過程中以 Vite 外掛的形式執行。優化器的目的是將應用程式拆分成許多可延遲載入的小區塊。優化器會將表達式(通常是函數)移到新的檔案中,並留下一個指向表達式被移出位置的參考。

$ 會告訴優化器哪些函數要提取到單獨的檔案中,哪些函數要保持不變。優化器不會保留一個神奇函數的內部清單,而是僅依靠 $ 後綴來知道要轉換哪些函數。該系統是可擴展的,開發人員可以創建自己的 $ 函數,例如 myCustomFunction$()

import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  console.log('render');
  return <button onClick$={() => console.log('hello')}>Hello Qwik</button>;
});

由於使用了 $ 語法,上面的元件被拆分成了多個區塊。

app.js
import { componentQrl, qrl } from '@builder.io/qwik';
 
const App = /*#__PURE__*/ componentQrl(
  qrl(() => import('./app_component_akbu84a8zes.js'), 'App_component_AkbU84a8zes')
);
 
export { App };
app_component_akbu84a8zes.js
import { jsx as _jsx } from '@builder.io/qwik/jsx-runtime';
import { qrl } from '@builder.io/qwik';
export const App_component_AkbU84a8zes = () => {
  console.log('render');
  return /*#__PURE__*/ _jsx('button', {
    onClick$: qrl(
      () => import('./app_component_button_onclick_01pegc10cpw'),
      'App_component_button_onClick_01pEgC10cpw'
    ),
    children: 'Hello Qwik',
  });
};
app_component_button_onclick_01pegc10cpw.js
export const App_component_button_onClick_01pEgC10cpw = () => console.log('hello');

規則

優化器使用 $ 作為提取程式碼的信號。開發人員需要了解,提取帶有限制,因此每當出現 $ 時,都會套用特殊的規則。(並非所有有效的 JavaScript 程式碼都是優化器的有效程式碼。)

最糟糕的程式碼魔法就是開發人員看不到的那種。

允許的表達式

任何以 $ 結尾的函數的第一個參數都有一定的限制。

沒有本地識別碼的字面值

const bar = 'bar';
const foo = 'foo';
 
// Invalid expressions
foo$({ value: bar }); // it contains a local identifier "bar"
foo$(`Hello, ${bar}`); // it contains a local identifier "bar"
foo$(count + 1); // it contains a local identifier "count"
foo$(foo); // foo is not exported, so it's not importable
 
// Valid expressions
foo$(`Hello, bar`); // string literal without local identifiers
foo$({ value: 'stuff' }); // object literal without local identifiers
foo$(1 + 3); // expression without local identifiers

可匯入的識別碼

// Invalid
const foo = 'foo';
foo$(foo); // foo is not exported, so it's not importable
 
// Valid
export const bar = 'bar';
foo$(bar);
 
// Valid
import { bar } from './bar';
foo$(bar);

閉包

對於閉包,規則會放鬆一些,可以參考和捕獲本地識別碼。

規則:如果一個函數詞法上捕獲了一個變數(或參數),那麼該變數必須是

  1. 一個 const
  2. 值必須是可序列化的。
捕獲的變數必須聲明為 const

無效

component$(() => {
  let foo = 'value'; // variable is not a const
  return <div onClick$={() => console.log(foo)}/>
});

有效

component$(() => {
  const foo = 'value';
  return <div onClick$={() => console.log(foo)}/>
});
本地捕獲的變數必須是可序列化的。
// Invalid
component$(() => {
  const foo = new MyCustomClass(12); // MyCustomClass is not serializable
  return <div onClick$={() => console.log(foo)}/>
});
 
// Valid
component$(() => {
  const foo = { data: 12 };
  return <div onClick$={() => console.log(foo)}/>
});
模組聲明的變數可以是可匯入的。

如果優化器正在提取的函數引用了一個頂級符號,那麼該符號必須被匯入或匯出。

// Invalid
const foo = new MyCustomClass(12);
component$(() => {
  // Foo is declared at the module level, but it's not exported
  console.log(foo);
});
 
// Valid
export const foo = new MyCustomClass(12);
component$(() => {
  console.log(foo);
});
 
// Valid
import { foo } from './foo';
component$(() => {
  console.log(foo);
});

深入探討

讓我們來看看處理滾動事件的假設性問題。您可能會想這樣寫程式碼。

function onScroll(fn: () => void) {
  document.addEventListener('scroll', fn);
}
onScroll(() => alert('scroll'));

這種方法的問題是,即使從未觸發滾動事件,事件處理程式也會被急切地載入。我們需要的是一種以延遲載入的方式引用程式碼的方法。

開發人員可以這樣寫。

export scrollHandler = () => alert('scroll');
 
onScroll(() => (await import('./some-chunk')).scrollHandler());

這種做法可行,但工作量很大。開發人員需要將程式碼放在不同的檔案中,並手動編寫程式碼區塊名稱。相對地,我們可以使用 Optimizer 自動執行這些工作。但我們需要一種方法來告訴 Optimizer 我們想要執行這樣的重構。我們使用 $() 作為標記函式來達到這個目的。

function onScroll(fnQrl: QRL<() => void>) {
  document.addEventListener('scroll', async () => {
    const fn = await fnQrl.resolve();
    fn();
  });
}
 
onScroll($(() => alert('scroll')));

Optimizer 將會產生

onScroll(qrl('./chunk-a.js', 'onScroll_1'));
chunk-a.js
export const onScroll_1 = () => alert('scroll');
  1. 開發人員只需要將函式包裝在 $() 中,就能向 Optimizer 發出信號,表示該函式應該被移到新的檔案中,並進行懶載入。
  2. onScroll 函式的實作方式略有不同,因為它需要考慮到在使用 QRL 之前需要先載入它。在實務上,在 Qwik 應用程式中很少使用 QRL.resolve(),因為 Qwik 框架提供了更高級別的 API,很少要求開發人員直接使用 QRL.resolve()

然而,將程式碼包裝在 $() 中有點不方便。因此,可以使用 implicit$FirstArg() 自動執行包裝和類型匹配,該函式會將 QRL 作為參數。傳遞給 implicit$FirstArg() 的函式應該具有 Qrl 的後綴,並且該函式的結果應該設置為具有 $ 的後綴;

const onScroll$ = implicit$FirstArg(onScrollQrl);
 
onScroll$(() => alert('scroll'));

現在,開發人員可以使用簡單的語法來表示應該對特定函式進行懶載入。

符號提取

假設您有以下程式碼

export const MyComp = component$(() => {
  /* my component definition */
});

Optimizer 將程式碼分成兩個檔案

原始檔案

const MyComp = component(qrl('./chunk-a.js', 'MyComp_onMount'));

和一個程式碼區塊

chunk-a.js
export const MyComp_onMount = () => {
  /* my component definition */
};

Optimizer 的結果是將 MyComponMount 方法提取到一個新的檔案中。這樣做有幾個好處

  • 父元件可以引用 MyComp,而無需引入 MyComp 的實作細節。
  • 應用程式現在有更多入口點,讓打包器有更多方式將程式碼庫分割成塊。

捕獲詞彙範圍

Optimizer 將表達式(通常是函式)提取到新的檔案中,並留下指向懶載入位置的 QRL

讓我們看一個簡單的例子

export const Greeter = component$(() => {
  return <div>Hello World!</div>;
});

這將會產生

const Greeter = component(qrl('./chunk-a.js', 'Greeter_onMount'));
chunk-a.js
const Greeter_onMount = () => {
  return qrl('./chunk-b.js', 'Greeter_onRender');
};
chunk-b.js
const Greeter_onRender = () => <span>Hello World!</span>;

以上是針對提取的函式閉包不捕獲任何變數的簡單情況。讓我們看一個更複雜的情況,其中提取的函式閉包會在詞彙上捕獲變數。

export const Greeter = component$((props: { name: string }) => {
  const salutation = 'Hello';
 
  return (
    <div>
      {salutation} {props.name}!
    </div>
  );
});

提取函式的簡單方法將無法運作。

const Greeter = component(qrl('./chunk-a.js', 'Greeter_onMount'));
chunk-a.js
const Greeter_onMount = (props) => {
  const salutation = 'Hello';
  return qrl('./chunk-b.js', 'Greeter_onRender');
};
chunk-b.js
const Greeter_onRender = () => (
  <div>
    {salutation} {props.name}!
  </div>
);

問題出現在 chunk-b.js 中。提取的函式引用了 salutationprops,而這些變數不再位於函式的詞彙範圍內。因此,生成的程式碼必須略有不同。

chunk-a.js
const Greeter_onMount = (props) => {
  const salutation = 'Hello';
  return qrl('./chunk-b.js', 'Greeter_onRender', [salutation, props]);
};
chunk-b.js
const Greeter_onRender = () => {
  const [salutation, props] = useLexicalScope();
 
  return (
    <div>
      {salutation} {props.name}!
    </div>
  );
};

請注意兩個變化

  1. Greeter_onMount 中的 QRL 現在存儲了 salutationprops。這扮演了捕獲閉包內常數的角色。
  2. 生成的閉包 Greeter_onRender 現在有一個前導碼,用於恢復 salutationprops (const [salutation, props] = useLexicalScope().)

Optimizer(和 Qwik 運行時)捕獲詞彙範圍常數的能力顯著改善了哪些函式可以提取到懶載入資源中。這是將複雜應用程式分解成更小的懶載入區塊的強大工具。

貢獻者

感謝所有讓這份文件變得更好的貢獻者!

  • the-r3aper7
  • manucorporat
  • adamdbradley
  • saikatdas0790
  • anthonycaron
  • ubmit
  • literalpie
  • forresst
  • mhevery
  • AnthonyPAlicea
  • zanettin
  • mrhoodz
  • thejackshelton
  • hamatoyogi