[譯] React 中的調度

在現代的應用程序中,用戶界面一般要同時處理多個任務。例如,一個搜索組件可能要在響應用戶輸入的同時自動補全結果,一個交互式儀表盤可能須要在從服務器加載數據並將分析數據發送到後端的同時更新圖表。javascript

全部這些並行的步驟都有可能致使交互界面響應緩慢甚至無響應,拉低用戶的滿意度,因此讓咱們學習下如何解決這個問題。html

用戶界面中的調度

咱們的用戶指望及時反饋。不管是點擊打開模態框的按鈕仍是在輸入框中添加文字,他們都不想在看到某種確認狀態以前進行等待。例如,點擊按鈕能夠當即打開模態框,輸入框能夠當即顯示剛剛輸入的關鍵字。前端

爲了想象在並行操做下會發生什麼,讓咱們來看看 Dan Abramov 在 JSConf Iceland 2018 上以超越 React 16 爲主題的演講中演示的應用程序。java

這個應用程序的工做原理以下:你在輸入框中的輸入越多,下面的圖表中的細節就會越多。因爲兩個更新(輸入框和圖表)同時進行,因此瀏覽器必須進行大量計算以致於會丟棄其中的一些幀。這會致使明顯的延時以及糟糕的用戶體驗。react

視頻地址android

可是,當有新鍵入時,優先更新用戶界面上輸入框的版本對用戶來講彷佛運行得更快。由於用戶能收到及時的反饋,儘管它們須要相同的計算時間。ios

視頻地址git

不幸的是,當前的用戶界面體系架構使得實現這種優先級變得很是重要,解決此問題的一種方法是經過防抖(debouncing)進行圖表更新。這種方法的問題在於當防抖函數的回調函數執行時,圖表依舊在同步地渲染,這會再次致使用戶界面在一段時間內無響應。咱們能夠作的更好!github

瀏覽器事件循環

在咱們學習如何正確地實現更新優先級以前,讓咱們深刻挖掘並理解瀏覽器爲什麼在處理用戶交互時存在問題。web

JavaScript 代碼在單線程中執行,意味着在任意給定的時間段內只有一行 JavaScript 代碼能夠運行。同時,這個線程也負責處理其餘文檔的生命週期,例如佈局和繪製。1意味着每當 JavaScript 代碼運行時,瀏覽器會中止作任何其餘的事情。

爲了保證用戶界面的及時響應,在可以接收下一次輸入以前,咱們只有很短的一個時間段進行準備。在 2018 年的 Chrome Dev 峯會(Chrome Dev Summit 2018)上,Shubhie Panicker 和 Jason Miller 發表了以保證響應性的追求(A Quest to Guarantee Responsiveness)爲主題的演講。在演講中,他們對瀏覽器事件循環進行了可視化描述,咱們能夠看到在繪製下一幀以前咱們只有 16ms(在典型的 60Hz 屏幕上),緊接着瀏覽器就須要處理下一個事件:

大多數 JavaScript 框架(包括當前版本的 React)將同步進行更新。咱們能夠將此行爲視爲一個函數 render(),而此函數只有在 DOM 更新後纔會返回。在此期間,主線程被阻塞。

當前解決方案存在的問題

有了上面的信息,咱們能夠擬定兩個必須解決的問題,以便得到更具響應性的用戶界面:

  1. 長時間運行的任務會致使幀丟失。 咱們須要確保全部任務都很小,能夠在幾毫秒內完成,以即可以在一幀內運行。

  2. 不一樣的任務有不一樣的優先級。 在上面的示例應用程序中,咱們看到優先考慮用戶輸入能夠帶來更好的總體體驗。爲此,咱們須要一種方法來定義優先級的排序並按照排序進行任務調度。

併發的 React 和調度器

⚠️ 警告:下面的 API 尚不穩定而且會發生變化。我會盡量地保持更新。

爲了使用 React 實現調度得宜的用戶界面,咱們必須看看如下兩個即將推出的 React 新功能:

  • 併發(Concurrent)React,也稱爲時間分片(Time Slicing)。 在 React 16 改寫的新 Fiber 架構幫助下,React 如今能夠容許渲染過程分段完成,中間能夠返回2至主線程執行其餘任務。

    咱們將在以後聽到更多有關併發 React 的消息。如今重要的是理解,當啓用這個模式以後,React 會把同步渲染的 React 組件切分紅小塊,而後在多個幀上運行。

    ➡️ 使用這個模式,在未來咱們就能夠將須要長時間渲染的任務分紅小任務塊。

  • 調度器。 它是由 React Core 團隊開發的通用協做主線程調度程序,能夠在瀏覽器中註冊具備不用優先級的回調函數。

    目前,優先級有這麼幾種:

    • Immediate 當即執行優先級,須要同步執行的任務。
    • UserBlocking 用戶阻塞型優先級(250 ms 後過時),須要做爲用戶交互結果運行的任務(例如,按鈕點擊)。
    • Normal 普通優先級(5 s 後過時),沒必要讓用戶當即感覺到的更新。
    • Low 低優先級(10 s 後過時),能夠推遲但最終仍然須要完成的任務(例如,分析通知)。
    • Idle 空閒優先級(永不過時),沒必要運行的任務(例如,隱藏界面之外的內容)。

    每一個優先級都有對應的過時時間,這些過時時間是必須的,這樣才能確保即便在高優先級任務多得能夠連續運行的狀況下,優先級較低的任務仍能運行。在調度算法中,這個問題被稱爲飢餓(starvation)。過時時間能夠保證每個調度任務最終均可以被執行。例如,即便咱們的應用中有正在運行的動畫,咱們也不會錯過任何一個分析通知。

    在引擎中,調度器將全部已經註冊的回調函數按照過時時間(回調函數註冊的時間加上該優先級的過時時間)排序而後存儲在列表中。接着,調度器將本身註冊在瀏覽器繪製下一幀以後的回調函數裏。3在這個回調函數中,瀏覽器將執行儘量多的已註冊回調函數,直到瀏覽開始繪製下一幀爲止。

    ➡️ 經過這個特性,咱們能夠調度具備不一樣優先級的任務。

方法中的調度

讓咱們來看看如何使用這些特性讓應用程序更具響應性。爲此,咱們先來看看 ScheduleTron 3000,這是我本身構建的應用程序,它容許用戶在姓名列表中高亮搜索詞。咱們先來看一下它的初始實現:

// 應用程序包含一個搜索框以及一個姓名列表。
// 列表的顯示內容由 searchValue 狀態變量控制。
// 該變量由搜索框進行更新。
function App() {
  const [searchValue, setSearchValue] = React.useState();

  function handleChange(value) {
    setSearchValue(value);
  }

  return (
    <div>
      <SearchBox onChange={handleChange} />
      <NameList searchValue={searchValue} />
    </div>
  );
}

// 搜索框渲染了一個原生的 HTML input 元素,
// 用 inputValue 變量對它進行控制。
// 當一個新的按鍵按下時,它會首先更新本地的 inputValue 變量,
// 而後它會更新 App 組件的 searchValue 變量,
// 接着模擬一個發向服務器的分析通知。
function SearchBox(props) {
  const [inputValue, setInputValue] = React.useState();

  function handleChange(event) {
    const value = event.target.value;

    setInputValue(value);
    props.onChange(value);
    sendAnalyticsNotification(value);
  };

  return (
    <input
      type="text"
      value={inputValue}
      onChange={handleChange}
    />
  );
}

ReactDOM.render(<App />, container);
複製代碼

ℹ️ 這個例子使用了 React Hooks。若是你對這個新特性沒有那麼熟悉的話,能夠看看 CodeSandbox code。此外,你可能想知道爲何在這個示例中咱們使用了兩個不一樣的狀態變量。接下來咱們一塊兒來找找看緣由。

試試看!在下面的搜索框中輸入一個名字(例如,「Ada Stewart」),而後看看它是怎麼工做的:

CodeSandbox 中查看

你可能注意到界面響應沒有那麼快。爲了放大這個問題,我故意加長了列表的渲染時間。因爲這個列表很大,它會應用程序的性能影響很大。

咱們的用戶但願獲得即時反饋,可是在按下按鍵後至關長的一段時間內,應用程序是沒有響應的。爲了瞭解正在發生的事情,咱們來看看開發者工具的 Performance 選項卡。這是當我在輸入框中輸入姓名「Ada」時錄製的屏幕截圖:

咱們能夠看到有不少紅色的三角形,這一般不是什麼好信號。對於每一次鍵入,咱們都會看到一個 keypress 事件被觸發。全部的事件在一幀內被觸發,5致使幀的持續時間延長到 733 ms。這遠高於咱們 16 ms 的平均幀預算。

在這個 keypress 事件中,會調用咱們的 React 代碼,更新 inputValue 以及 searchValue,而後發送分析通知。反過來,更新後的狀態值會導致應用程序從新渲染每個姓名項。任務至關繁重可是必須完成,若是使用原生的方法,它會阻塞主進程。

改進如今這個狀態的第一步是使用並不穩定的併發模式。實現方法是,使用 <React.unstable_ConcurrentMode> 組件把咱們的 React 樹的一部分包裹起來,就像下面這樣4

- ReactDOM.render(<App />, container);
+ ReactDOM.render(
+  <React.unstable_ConcurrentMode>
+    <App />
+  </React.unstable_ConcurrentMode>,
+  rootElement
+ );
複製代碼

可是,在這個例子中,僅僅使用併發模式並不會改變咱們的體驗。React 仍然會同時收到兩個狀態值的更新,沒辦法知道哪個更重要。

咱們想要首先設置 inputValue,而後更新 searchValue 以及發送分析通知,因此咱們只須要在開始的時候更新輸入框。爲此,咱們使用了調度器暴露的 API(可使用 npm i scheduler 進行安裝)對低優先級的回調函數進行排序:

import { unstable_next } from "scheduler";
function SearchBox(props) {
  const [inputValue, setInputValue] = React.useState();

  function handleChange(event) {
    const value = event.target.value;

    setInputValue(value);
    unstable_next(function() {      
      props.onChange(value);      
      sendAnalyticsNotification(value);    
    });  
  }
  
  return <input type="text" value={inputValue} onChange={handleChange} />;
}
複製代碼

在咱們使用的 API unstable_next() 中,全部的 React 更新都會被設置成 Normal 優先級,這個優先級低於 onChange 監聽器內部默認的優先級。

事實上,經過這種改變,咱們的輸入框響應速度已經快了很多,而且咱們打字的時候不會再有幀被丟棄。讓咱們再看看 Performance 選項卡:

咱們能夠看到須要長時間運行的任務如今被分解成能夠在單個幀內完成的較小任務。提示咱們有幀丟棄的紅色三角也消失了。

可是,分析通知(在上面的截圖中高亮的部分)仍然不理想,它依舊在渲染的同時執行。由於咱們的用戶不會看到這個任務,因此能夠給它安排一個更低的優先級。

import {
  unstable_LowPriority,
  unstable_runWithPriority,
  unstable_scheduleCallback
} from "scheduler";

function sendDeferredAnalyticsNotification(value) {
  unstable_runWithPriority(unstable_LowPriority, function() {
    unstable_scheduleCallback(function() {
      sendAnalyticsNotification(value);
    });
  });
}
複製代碼

若是咱們如今在搜索框組件中使用 sendDeferredAnalyticsNotification(),而後再次查看 Performance 選項卡,並拖動到末尾,咱們能夠看到在渲染工做完成後,分析通知才被髮送,程序中的全部任務都被完美地調度了:

試試看:

CodeSandbox 中查看

調度器的限制

使用調度器,咱們能夠控制回調函數的執行順序。它內置於最新的 React 實現中,無需另行設置就可以和併發模式協同使用。

這就是說,調度器有兩個限制:

  1. 資源搶奪。 調度器嘗試全部使用全部的可用資源。若是調度器的多個實例運行在同一個線程上並爭奪資源,就會致使問題。咱們須要確保應用程序的全部部分使用的是同一個調度器實例。
  2. 經過瀏覽器工做平衡用戶定義的任務。 因爲調度器在瀏覽器中運行,所以它只能訪問瀏覽器公開的API。文檔生命週期(如渲染或垃圾回收)可能會以沒法控制的方式干擾工做。

爲了消除這些限制,Google Chrome 團隊正在與 React、Polymer、Ember、Google Maps 和 Web Standards Community 合做,在瀏覽器中建立 Scheduling API。是否是很讓人興奮!

總結

併發的 React 和調度器容許咱們在應用程序中實現任務調度,這將使得咱們能夠建立響應迅速的用戶界面。

React 官方可能會在 2019 第二季度發佈這些功能。在此以前,你們可使用這些不穩定的 API,但要密切關注它的變化。

若是您想成爲第一個知道這些 API 什麼時候更改或者編寫新功能文檔的人,請訂閱 This Week in React ⚛️


1. MDN web docs 上有一篇關於這個問題很棒的文章

2. 這是一個超讚的詞,能夠返回一個支持暫停以後繼續執行的方法。能夠在 generator functions 上查看類似的概念。

3.調度器的目前實現中,它經過在一個 requestAnimationFrame() 回調函數中使用 postMessage() 實現。它會在幀渲染結束後當即被調用。

4. 這是另一個能夠實現併發模式的方法,使用新的 createRoot() API。

5. 在處理第一次的 keypress 事件時,瀏覽器會在它的隊列中查看待處理事件,而後決定在渲染幀以前運行哪一個事件監聽器。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索