寫在前面
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 委託[/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 委託[/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 />;
}
在後來的迭代中卻沒對 forwardRef、memo 加以檢查,在 React 17 補上了。之後無論類組件、函數式組件,還是 forwardRef、memo 等期望返回 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.
暫無評論,快來發表你的看法吧