React 17 要來了,非常特別的一版


寫在前面

React 最近發布了v17.0.0-rc.0,距上一個大版本v16.0(發布於 2017/9/27)已經過去近 3 年了

新特性雲集的 React 16及先前的大版本相比,React 17 顯得格外特殊——沒有新特性

React v17.0 Release Candidate: No New Features

不僅如此,還帶上來了 7 個 breaking change……

 

一.真沒有新特性?

React 官方對 v17 的定位是一版技術改造,主要目標是降低后續版本的升級成本

This release is primarily focused on making it easier to upgrade React itself.

因此 v17 只是一個鋪墊,並不想發布重大的新特性,而是為了 v18、v19……等后續版本能夠更平滑、更快速地升上來:

When React 18 and the next future versions come out, you will now have more options.

但其中有些改造不得不打破向后兼容,於是提出了 v17 這個大版本變更,順便搭車卸掉兩年多積攢的一些歷史包袱

 

二.漸進式升級成為了可能

在 v17 之前,不同版本的 React 無法混用(事件系統會出問題),所以,開發者要么沿用舊版本,要么花大力氣整個升級到新版本,甚至一些常年沒有需求的長尾模塊也要整體適配、回歸測試。考慮到開發者的升級適配成本,React 維護團隊同樣束手束腳,廢棄 API 不敢輕易下掉,要么長時間、甚至無休止地維護下去,要么選擇放棄那些老舊的應用

而 React 17 提供了一個新的選項——漸進式升級,允許 React 多版本並存,對大型前端應用十分友好,比如彈窗組件、部分路由下的長尾頁面可以先不升級,一塊一塊地平滑過渡到新版本(參考官方 Demo

P.S.注意,(按需)加載多個版本的 React 存在着不小的性能開銷,同樣應該慎重考慮

 

多版本並存與微前端架構

多版本並存、新舊混用的支持讓微前端架構所期望的漸進式重構成為了可能:

漸進地升級、更新甚至重寫部分前端功能成為了可能

與 React 支持多版本並存、漸進地完成版本升級相比,微前端更在意的是允許不同技術棧並存,平滑地過渡到升級后的架構,解決的是一個更寬的問題

另一方面,當 React 技術棧下多版本混用難題不復存在時,也有必要對微前端進行反思:

  • 一些問題是不是由技術棧自身來解決更為合適?

  • 多技術棧並存是常態還是短期過渡?

  • 對於短期過渡,是否存在更輕量的解決方案?

關於微前端在解決什么問題的更多思考,見Why micro-frontends?

 

三.7 個 Breaking change

 

事件委托不再掛到 document 上

之前多版本並存的主要問題在於React 事件系統默認的委托機制,出於性能考慮,React 只會給document掛上事件監聽,DOM 事件觸發后冒泡到document,React 找到對應的組件,造一個 React 事件(SyntheticEvent)出來,並按組件樹模擬一遍事件冒泡(此時原生 DOM 事件早已冒出document了):

[caption id="attachment_2263" align="alignnone" width="625"]react 16 delegation react 16 delegation[/caption]

因此,不同版本的 React 組件嵌套使用時,e.stopPropagation()無法正常工作(兩個不同版本的事件系統是獨立的,都到document已經太晚了):

If a nested tree has stopped propagation of an event, the outer tree would still receive it.

P.S.實際上,Atom 在早些年就遇到了這個問題

為了解決這個問題,React 17 不再往document上掛事件委托,而是掛到 DOM 容器上

[caption id="attachment_2264" align="alignnone" width="625"]react 17 delegation react 17 delegation[/caption]

例如:

const rootNode = document.getElementById('root');
// 以為 render 為例
ReactDOM.render(<App />, rootNode);
// Portals 也一樣
// ReactDOM.createPortal(<App />, rootNode)
// React 16 事件委托(掛到 document 上)
document.addEventListener()
// React 17 事件委托(掛到 DOM container 上)
rootNode.addEventListener()

另一方面,將事件系統從document縮回來,也讓 React 更容易與其它技術棧共存(至少在事件機制上少了一些差異)

 

向瀏覽器原生事件靠攏

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

  • onScroll不再冒泡

  • onFocus/onBlur直接采用原生focusin/focusout事件

  • 捕獲階段的事件監聽直接采用原生 DOM 事件監聽機制

注意,onFocus/onBlur的下層實現方案切換並不影響冒泡,也就是說,React 里的onFocus仍然會冒泡(並且不打算改,認為這個特性很有用)

 

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()(或者直接做值緩存)

React 17 去掉了事件復用機制,因為在現代瀏覽器下這種性能優化沒有意義,反而給開發者帶來了困擾

 

Effect Hook 清理操作改為異步執行

useEffect本身是異步執行的,但其清理工作卻是同步執行的(就像 Class 組件的componentWillUnmount同步執行一樣),可能會拖慢切 Tab 之類的場景,因此 React 17 改為異步執行清理工作:

useEffect(() => {
  // This is the effect itself.
  return () => {
    // 以前同步執行,React 17之后改為異步執行
    // This is its cleanup.
  };
});

同時還糾正了清理函數的執行順序,按組件樹上的順序來執行(之前並不嚴格保證順序)

P.S.對於某些需要同步清理的特殊場景,可換用LayoutEffect Hook

 

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 />;
}

在后來的迭代中卻沒對forwardRefmemo加以檢查,在 React 17 補上了。之后無論類組件、函數式組件,還是forwardRefmemo等期望返回 React 組件的地方都會檢查undefined

P.S.空組件可返回null,不會引發報錯

 

報錯信息透出組件“調用棧”

React 16 起,遇到 Error 能夠透出組件的“調用棧”,輔助定位問題,但比起 JavaScript 的錯誤棧還有不小的差距,體現在:

  • 缺少源碼位置(文件名、行列號等),Console 里無法點擊跳轉到到出錯的地方

  • 無法在生產環境中使用(displayName被壓壞了)

React 17 采用了一種新的組件棧生成機制,能夠達到媲美 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 還原回來

P.S.重建組件棧的過程中會重新執行 render,以及 Class 組件的構造函數,這部分屬於 Breaking change

P.S.關於重建組件棧的更多信息,見Build Component Stacks from Native Stack Frames、以及react/packages/shared/ReactComponentStackFrame.js

 

部分暴露出來的私有 API 被刪除

React 17 刪除了一些私有 API,大多是當初暴露給React Native for Web使用的,目前 React Native for Web 新版本已經不再依賴這些 API

另外,修改事件系統時還順手刪除了ReactTestUtils.SimulateNative工具方法,因為其行為與語義不符,建議換用React Testing Library

 

四.總結

總之,React 17 是一個鋪墊,這個版本的核心目標是讓 React 能夠漸進地升級,因此最大的變化是允許多版本混用,為將來新特性的平穩落地做好准備

We’ve postponed other changes until after React 17. The goal of this release is to enable gradual upgrades.

 

參考資料


免責聲明!

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



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