React 17來了


React 17正式版已經發布,17版本不像16版本中有諸多內容更新。官方對17版本的描述也是沒有新特性,主要定位為一版技術改造,為下一個大版本的更新減輕負擔。
 
1. 無新特性
  React 17 的版本是非比尋常的,因為它沒有添加任何面向開發人員的新功能。而主要側重於升級簡化 React 本身。
因此 v17 只是一個鋪墊,並不想發布重大的新特性,而是為了 v18、v19……等后續版本能夠更平滑、更快速地升上來。但其中有些改造不得不打破向后兼容,於是提出了 v17 這個大版本變更,順便搭車卸掉兩年多積攢的一些歷史包袱。
 
2. 漸進式升級
  v17 版本可以使得由一個React版本管理的代碼樹嵌入到另一個React版本管理的代碼樹中,這是該版本提供的漸進升級的能力。
在以往的版本中,React一直遵循“all-or-nothing”的升級策略。開發者可以繼續使用舊版本,也可以將整個應用升級到新版本,沒有介於兩者之間的情況。這種情況往往會導致兩個主要問題:
  1. 想要讓業務代碼自動化升級過渡,React團隊需要無限期支持過時的API;
  2. 如果react有不兼容版本更新,業務開發者需要在繼續使用舊版本和更新React之間做取舍,因為往往這種面臨較大的風險。
因此,React17 提供了一個新的選項——漸進式升級,允許同一個項目中多個React版本並存。這將意味着當React 18或未來版本問世時,業務開發人員將有更多的選擇。對於大型前端應用將十分友好,比如彈窗組件、部分路由下的頁面可以可以一塊塊地平滑過渡到新版本。( 官方Demo
PS:(按需)加載多個版本的React同樣存在着性能開銷,打包產物過大等問題,同樣需要取舍。
 
2.1 多版本並存與為前端架構
  多版本並存、新舊混用的支持讓**微前端架構**所期望的漸進式重構成為了可能。與React支持多版本並存、漸進地完成版本升級相比,微前端更在意允許不同技術棧並存,平滑地過渡到升級后的架構,比如對於在一個JQuery項目中逐步升級支持React等場景。

3. 7個Breaking Change
3.1 事件委托不再掛到 documen 上
  之前多版本並存的主要為題在於React事件系統默認的委托機制,出於性能考慮,React只會給`document`上掛載事件監聽,DOM事件觸發后冒泡到`document`,React找到對應的組件,造一個React事件(SyntheticEvent)出來,並按組件樹模擬一遍事件冒泡(此時原生DOM事件已經冒出`document`了):
 
  因此,v17 之前不同版本的React組件嵌套使用時,e.stopPropagation()無法正常工作:如果嵌套樹結構中阻止了事件冒泡,但外部樹依然能接收到它。這會使不同版本 React 嵌套變得困難重重。這種擔憂並不是沒有根據的 —— 例如,四年前 Atom 編輯器就遇到了 相同的問題
在 React 17 中,React 將不再向 document 附加事件處理器。而會將事件處理器附加到渲染 React 樹的根 DOM 容器中:
 
const rootNode = document.getElementById('root');
// render
ReactDOM.render(<App />, rootNode);
// Portals
ReactDOM.createPortal(<App />, rootNode);
// React16 事件委托(掛在document上)
document.addEventListener();
// React17 事件委托(掛在 root DOM 上)
rootNode.addEventListener();
 
在React16或更早版本中,React會對大多數事件執行`document.addEventListener()`。React17將會在底層調用 `rootNode.addEventListener()`。
 
  由於此更改,現在可以更加安全地進行新舊版本React樹的嵌套。請注意,要使其正常工作,兩個版本都必須是17或更高版本。所以,從某種意義上將,React17是一個“墊腳石”版本,是逐步升級成為可能。
並且,此更改還是得將React嵌入使用其他技術構建的應用程序更加容易。例如,如果應用程序的“外殼”是用JQuery編寫的,但其中交心的代碼是用React編寫的,則React代碼中的`e.stopPropagation()`會阻止它影響JQuery的代碼——這符合預期。換個角度說,如果你不再喜歡React並想重寫應用程序(比如,用JQuery),則可以從外殼開始講React轉換為JQuery,而不會破壞事件冒泡。

 

3.2 更加靠近瀏覽器原生事件

此外,React事件系統還做了一些小的改動,使之更加切近瀏覽器原生事件:

  1. `onScroll` 不再冒泡;

  2. `onFocus/onBlur`直接采用原生`focusin/focusout`事件;
  3. 捕獲階段的事件監聽直接采用原生 DOM 事件監聽機制。
 
PS:`onFocus/onBlur`的下層實現方案切換並不英系那個冒泡,也就是說,React的 `onFocus` 仍然會冒泡(並且不打算改,任務這個特性很有用)。

 

3.3 DOM事件復用池被廢棄

  之前出於性能考慮,為了復用SyntheticEvent,維護了一個事件池,導致React事件只在傳播過程中可用,之后會立即被回收釋放,例如:
<button onClick={(e) => {
    console.log(e.target.nodeName);
    // 輸出 BUTTON
    // e.persist();
    setTimeout(() => {
      // 報錯 Uncaught TypeError: Cannot read property 'nodeName' of null
      console.log(e.target.nodeName);
    });
  }}>
  Click Me!
</button>
 
傳播過程之外的事件對象上的所有狀態會被置為 null,除非手動 `e.persist()`(或者直接做值緩存)。React17去掉了了事件復用機制,因為在現代瀏覽器下這種性能優化沒有意義,反而給開發者帶來了困擾。
 
3.4 Effect Hook 清理操作改為異步執行
  useEffect本身是異步執行的,但其清理工作卻是同步執行的(就像Class組件的`componentWillUnmount`同步執行一樣),可能會拖慢Tab切換之類的場景,因此React17將清理工作改為異步執行:
useEffect(() => {
  // This is the effect itself.
  return () => {
    // 以前同步執行,React 17之后改為異步執行
    // This is its cleanup.
  };
});

 

  同時還糾正了清理函數的執行順序,按組件樹上的順序來執行(之前並不嚴格保證順序)。
PS:對於某些需要同步清理的特殊場景,可以使用 LayoutEffect Hook。

 

3.5 render返回undefined報錯
  React 組件中 render 返回 undefined 會報錯:
function Button() {
  return; // Error: Nothing was returned from render
}
  初衷是為了把忘記寫 return 的常見錯誤提示出來:
function Button() {
  // We forgot to write return, so this component returns undefined.
  // React surfaces this as an error instead of ignoring it.
  <button />;
}

 

  在后來的迭代中卻沒有對 forwardRef、memo 加對應的檢查,在React17 補上了。之后無論是類組件、函數式組件,還是 forwardRef、memo 等期望返回React組件的地方都會檢查 undefined。
PS:空組件可以返回 null,不會引發報錯。

 

3.6 報錯信息透傳組件“調用棧”
  React16 起,遇到 Error 能夠透出組件的“調用棧”,輔助定位問題,但比起 JavaScript 的錯誤棧還有不小的差距,體現在:
  1. 缺少源碼位置(文件名、行列號等),Console 里無法點擊跳閘是你到出錯的地方;
  2. 無法在生產環境中使用(displayName 被壓縮壞了)。
 
  React17 采用了一種新的組件棧生成機制,能夠達到媲美 JavaScript 原生錯誤棧的效果(跳轉到源碼),並且同樣適用於生產環境,大致思路是在 Error 發生時重建組件棧,在每個組件內部引發一個臨時錯誤(對每個組件類型做一次),再從 `error.stack` 提取出關鍵信息構造組件棧:
var prefix;
// 構造div等內置組件的“調用棧”
function describeBuiltInComponentFrame(name, source, ownerFn) {
  if (prefix === undefined) {
    // Extract the VM specific prefix used by each line.
    try {
      throw Error();
    } catch (x) {
      var match = x.stack.trim().match(/\n( *(at )?)/);
      prefix = match && match[1] || '';
    }
  } // We use the prefix to ensure our stacks line up with native stack frames.

  return '\n' + prefix + name;
}
// 以及 describeNativeComponentFrame 用來構造 Class、函數式組件的“調用棧”

 

  因為組件棧是直接從 JavaScript 原生錯誤棧生成的,所以能夠點擊跳回源碼、在生產環境也能按 sourcemap 還原回來。
PS:
  1. 重建組件棧的過程中會重新執行 render,以及Class組件的構造函數,這部分屬於 Breaking Change;

 

3.7 刪除部分暴露出來的私有 API
  React17 刪除了一些私有的API,大多是當初暴露給 React Native for Web 使用的,目前 React Native for Web 新版本已經不再依賴這些API了。
  另外,修改事件系統時還順手刪除了 `ReactTestUtils.SimulateNative`工具方法,因為其行為與語義不符,建議換用 React Testing Library
 
4. 總結
  總之,React17 是一個鋪墊,這個版本的核心目標是讓 React 能夠漸進地升級,因此最大的變化是允許多版本混用,為將來新特性的平穩落地做好准備。
 
參考資料 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM