本文翻譯自React官網(wǎng): React v17.0 Release Candidate: No New Features
今天配紫,我們正在發(fā)布React 17的第一個(gè)候選發(fā)布版厦凤。距離上一個(gè)主要的React版本已經(jīng)過去了兩年半鹿驼,即使按照我們的標(biāo)準(zhǔn),也已經(jīng)很長時(shí)間了!在此博客文章中微宝,我們將描述此主要版本的作用摄凡,可以期望的主要變化以及如何試用該版本。
沒有新功能
React 17版本不尋常畏妖,因?yàn)樗鼪]有添加任何面向開發(fā)人員的新功能。取而代之的是疼阔,此發(fā)行版主要側(cè)重于使其更易于升級React本身戒劫。
我們正在積極開發(fā)新的React功能,但它們不是此版本的一部分婆廊。React 17發(fā)行版是我們將其推廣到任何人的戰(zhàn)略的關(guān)鍵部分迅细。
特別地,React 17是一個(gè)“墊腳石”版本淘邻,它使將由一個(gè)版本的React管理的樹嵌入到由另一個(gè)版本的React管理的樹中更加安全茵典。
逐步升級
在過去的七年中,React升級一直是“全有或全無”列荔。您可以使用舊版本敬尺,也可以將整個(gè)應(yīng)用升級到新版本。中間沒有贴浙。
到目前為止砂吞,這已經(jīng)解決了,但是我們遇到了“全有或全無”升級策略的局限性崎溃。某些API更改(例如蜻直,不贊成使用舊版上下文API)是不可能以自動(dòng)化方式進(jìn)行的。即使今天編寫的大多數(shù)應(yīng)用程序從未使用過它們袁串,我們?nèi)匀辉赗eact中支持它們概而。我們必須選擇無限期地在React中支持它們,還是將某些應(yīng)用程序留在舊版本的React中囱修。這兩個(gè)選項(xiàng)都不是很好赎瑰。
因此,我們想提供另一種選擇破镰。
React 17支持逐步的React升級餐曼。從React 15升級到16(或者很快從React 16升級到17)時(shí)压储,通常會立即升級整個(gè)應(yīng)用程序。這適用于許多應(yīng)用程序源譬。但是集惋,如果代碼庫是在幾年前編寫的,并且沒有得到積極維護(hù)踩娘,則挑戰(zhàn)可能會越來越大刮刑。盡管可以在頁面上使用兩個(gè)版本的React,但是直到React 17仍然脆弱养渴,并導(dǎo)致事件問題雷绢。
我們正在使用React 17解決許多這些問題。這意味著當(dāng)React 18和下一個(gè)未來版本問世時(shí)厚脉,您現(xiàn)在將有更多選擇习寸。第一種選擇是像以前一樣,一次升級整個(gè)應(yīng)用程序傻工。但是您也可以選擇逐個(gè)升級您的應(yīng)用程序。例如孵滞,您可能決定將大部分應(yīng)用程序遷移到React 18中捆,但在React 17上保留一些延遲加載的對話框或子路由。
這并不意味著您必須逐步升級坊饶。對于大多數(shù)應(yīng)用程序泄伪,一次全部升級仍然是最好的解決方案。加載兩個(gè)版本的React(即使其中一個(gè)是按需延遲加載)仍然不是理想的選擇匿级。但是蟋滴,對于沒有積極維護(hù)的大型應(yīng)用程序,可以考慮使用此選項(xiàng)痘绎,并且React 17可以使這些應(yīng)用程序不落伍津函。
要啟用漸進(jìn)式更新,我們需要對React事件系統(tǒng)進(jìn)行一些更改孤页。React 17是主要版本尔苦,因?yàn)檫@些更改可能會被破壞。實(shí)際上行施,我們只需要在100,000個(gè)以上的組件中更改少于二十個(gè)組件允坚,因此我們希望大多數(shù)應(yīng)用程序可以升級到React 17而不會帶來太多麻煩。
逐步升級演示
我們準(zhǔn)備了一個(gè)例子展示了如何在必要時(shí)延遲加載舊版本的React蛾号。該演示使用Create React App稠项,但應(yīng)該可以對其他工具采用類似的方法。我們歡迎使用其他工具作為拉取請求的演示鲜结。
我們已將其他更改推遲到React 17之后展运。此版本的目標(biāo)是實(shí)現(xiàn)逐步升級活逆。如果升級到React 17太困難了,那將無法實(shí)現(xiàn)其目標(biāo)乐疆。
對事件委派的更改
從技術(shù)上講划乖,始終可以嵌套使用不同版本的React開發(fā)的應(yīng)用程序。但是挤土,由于React事件系統(tǒng)的工作原理琴庵,它相當(dāng)脆弱。
在React組件中仰美,通常會內(nèi)聯(lián)編寫事件處理程序:
<button onClick={handleClick}>
等效于此代碼的原始DOM類似于:
myButton.addEventListener('click', handleClick);
但是迷殿,對于大多數(shù)事件,React實(shí)際上不會將它們附加到在其上聲明它們的DOM節(jié)點(diǎn)上咖杂。相反庆寺,React會直接在document
節(jié)點(diǎn)上為每種事件類型附加一個(gè)處理程序。這稱為事件委托诉字。除了在大型應(yīng)用程序樹上具有性能優(yōu)勢外懦尝,它還使添加新功能(如重播事件)更加容易。
自從第一個(gè)版本發(fā)布以來壤圃,React一直在自動(dòng)進(jìn)行事件委派陵霉。當(dāng)文檔上觸發(fā)DOM事件時(shí),React會找出要調(diào)用的組件伍绳,然后React事件會在整個(gè)組件中“冒泡”踊挠。但是在幕后,本機(jī)事件已經(jīng)冒出來冲杀,達(dá)到了document
React安裝其事件處理程序的水平效床。
但是,這是逐步升級的問題权谁。
如果頁面上有多個(gè)React版本剩檀,它們都將在頂部注冊事件處理程序。這將中斷e.stopPropagation()
:如果嵌套樹停止了事件的傳播闯传,則外部樹仍將接收該事件谨朝。這使得嵌套不同版本的React變得很困難。這種擔(dān)心不是假設(shè)的甥绿,例如字币,Atom編輯器在四年前就遇到了這種情況。
這就是為什么我們要改變React在幕后將事件附加到DOM的方式共缕。
在React 17中洗出,React將不再在該document
級別附加事件處理程序。相反图谷,它將把它們附加到渲染您的React樹的根DOM容器中:
const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);
在React 16和更早的版本中翩活,React會document.addEventListener()
處理大多數(shù)事件阱洪。React 17將rootNode.addEventListener()
在后臺調(diào)用。
由于此更改菠镇,現(xiàn)在可以更安全地將由一個(gè)版本管理的React樹嵌入到由其他React版本管理的樹中冗荸。請注意,要使其正常工作利耍,兩個(gè)版本都必須為17或更高版本蚌本,這就是為什么升級到React 17很重要的原因。從某種意義上說隘梨,React 17是一個(gè)“墊腳石”版本程癌,使下一個(gè)逐步升級成為可能。
這一變化還使將React嵌入到使用其他技術(shù)構(gòu)建的應(yīng)用程序中變得更加容易轴猎。例如嵌莉,如果應(yīng)用程序的外部“外殼”是用jQuery編寫的,但其中的較新代碼是用React編寫的捻脖,e.stopPropagation()
那么React代碼內(nèi)部現(xiàn)在將阻止它到達(dá)jQuery代碼-正如您所期望的那樣锐峭。這在另一個(gè)方向上也起作用。如果您不再喜歡React并想重寫您的應(yīng)用程序(例如可婶,在jQuery中)只祠,則可以開始將外殼從React轉(zhuǎn)換為jQuery,而不會破壞事件傳播扰肌。
我們已經(jīng)證實(shí),很多 問題 報(bào)道 過 的 年 對 我們的 問題 跟蹤器與整合與之反應(yīng)的非反應(yīng)的代碼已經(jīng)被固定在新的行為熊杨。
您可能想知道這是否會破壞根容器之外的Portal曙旭。答案是,React也偵聽門戶網(wǎng)站容器上的事件晶府,因此這不是問題桂躏。
解決潛在問題
與任何重大更改一樣,可能需要調(diào)整一些代碼川陆。在Facebook剂习,我們必須調(diào)整總共約10個(gè)模塊(成千上萬個(gè)模塊)以適應(yīng)此更改。
例如较沪,如果您通過添加手動(dòng)DOM偵聽器document.addEventListener(...)
鳞绕,則可能希望它們捕獲所有React事件。在React 16及更早版本中尸曼,即使您調(diào)用e.stopPropagation()
React事件處理程序们何,您的自定義document
偵聽器仍會收到它們,因?yàn)楸緳C(jī)事件已經(jīng)在文檔級別控轿。使用React 17冤竹,傳播將停止(按要求7鞣狻),因此您的document
處理程序?qū)⒉粫|發(fā):
document.addEventListener('click', function() {
// This custom handler will no longer receive clicks
// from React components that called e.stopPropagation()
});
您可以通過將偵聽器轉(zhuǎn)換為使用捕獲階段來修復(fù)此類代碼鹦蠕。為此冒签,您可以將{ capture: true }
第三個(gè)參數(shù)傳遞給document.addEventListener
:
document.addEventListener('click', function() {
// Now this event handler uses the capture phase,
// so it receives *all* click events below!
}, { capture: true });
請注意,此策略在整體上如何更具彈性-例如钟病,它可能會修復(fù)代碼中在e.stopPropagation()
React事件處理程序外部調(diào)用時(shí)發(fā)生的現(xiàn)有錯(cuò)誤萧恕。換句話說,React 17中的事件傳播更接近常規(guī)DOM档悠。
其他重大變化
我們將React 17中的重大更改保持在最低水平廊鸥。例如,它不會刪除以前版本中已棄用的任何方法辖所。但是惰说,它的確包含一些其他重大更改,根據(jù)我們的經(jīng)驗(yàn)缘回,這些更改相對安全吆视。總體而言酥宴,由于這些因素啦吧,我們必須在100,000+個(gè)組件中調(diào)整少于20個(gè)。
與瀏覽器對應(yīng)
我們對事件系統(tǒng)進(jìn)行了一些較小的更改:
- 該
onScroll
事件不再冒泡拙寡, 以防止常見的混亂授滓。 - React
onFocus
和onBlur
event已轉(zhuǎn)為使用幕后的nativefocusin
和focusout
events,這與React的現(xiàn)有行為更加接近肆糕,有時(shí)還會提供額外的信息般堆。 - 捕獲階段事件(例如
onClickCapture
)現(xiàn)在使用真實(shí)的瀏覽器捕獲階段偵聽器。
這些更改使React與瀏覽器行為更加接近诚啃,并提高了互操作性淮摔。
盡管該事件從React 17切換
focus
到focusin
了幕后,但onFocus
請注意始赎,這并未影響冒泡行為和橙。在React中,onFocus
事件總是冒泡的造垛,它在React 17中繼續(xù)冒泡魔招,因?yàn)橥ǔK且粋€(gè)更有用的默認(rèn)值。請參閱此沙箱筋搏,了解可以針對不同的特定用例添加的不同檢查仆百。
沒有事件池
React 17從React移除了“事件池”優(yōu)化。它不會提高現(xiàn)代瀏覽器的性能奔脐,甚至?xí)菇?jīng)驗(yàn)豐富的React用戶感到困惑:
function handleChange(e) {
setData(data => ({
...data,
// This crashes in React 16 and earlier:
text: e.target.value
}));
}
這是因?yàn)镽eact在舊瀏覽器中重用了不同事件之間的事件對象以提高性能俄周,并將所有事件字段都設(shè)置null
在它們之間吁讨。在React 16及更早版本中,您必須調(diào)用e.persist()
以正確使用該事件峦朗,或讀取您之前需要的屬性建丧。
在React 17中,此代碼可以按您期望的那樣工作波势。舊的事件池優(yōu)化已被完全刪除翎朱,因此您可以在需要時(shí)閱讀事件字段。
這是一種行為更改尺铣,這就是我們將其標(biāo)記為破壞的原因拴曲,但實(shí)際上,在Facebook上我們還沒有看到它破壞任何東西凛忿。(也許它甚至修復(fù)了一些錯(cuò)誤3鹤啤)請注意,e.persist()
React事件對象仍然可用店溢,但是現(xiàn)在它什么也沒做叁熔。
高效的清理
我們正在使useEffect
清理功能的時(shí)間更加一致。
useEffect(() => {
// This is the effect itself.
return () => { // This is its cleanup. };});
大多數(shù)效果不需要延遲屏幕更新床牧,因此React在屏幕上反映出更新后立即異步運(yùn)行它們荣回。(在極少數(shù)情況下,您需要一種效果來阻止油漆戈咳,例如心软,測量和定位工具提示,請使用useLayoutEffect
著蛙。)
但是糯累,在卸載組件時(shí),效果清理函數(shù)將用于同步運(yùn)行(類似于componentWillUnmount
類中的同步)册踩。我們發(fā)現(xiàn)這不適用于大型應(yīng)用程序,因?yàn)樗鼤p慢大屏幕過渡(例如切換選項(xiàng)卡)的速度效拭。
在React 17中暂吉,清除功能始終異步運(yùn)行-例如,如果要卸載組件缎患,則在更新屏幕后運(yùn)行清除慕的。**
這反映了效果本身如何更緊密地運(yùn)行。在極少數(shù)情況下挤渔,您可能希望依靠同步執(zhí)行肮街,可以useLayoutEffect
改為使用。
您可能想知道這是否意味著您現(xiàn)在將無法修復(fù)有關(guān)
setState
未安裝組件的警告判导。別擔(dān)心-專門針對這種情況作出反應(yīng)檢查嫉父,確實(shí)沒有觸發(fā)setState
在卸載和清理之間的短間隔警告沛硅。因此,取消代碼的請求或間隔幾乎總是可以保持不變绕辖。
另外摇肌,React 17將在運(yùn)行任何新效果之前始終執(zhí)行所有效果清理功能(針對所有組件)。React 16僅保證組件中效果的這種順序仪际。
潛在問題
盡管可重用的庫可能需要對其進(jìn)行更徹底的測試围小,但我們僅看到幾個(gè)組件隨此更改而中斷。有問題的代碼的一個(gè)示例可能如下所示:
useEffect(() => {
someRef.current.someSetupMethod();
return () => {
someRef.current.someCleanupMethod();
};
});
問題是someRef.current
可變的树碱,因此在運(yùn)行清除功能時(shí)肯适,可能已將其設(shè)置為null
。解決方案是捕獲效果內(nèi)的任何可變值:
useEffect(() => {
const instance = someRef.current;
instance.someSetupMethod();
return () => {
instance.someCleanupMethod();
};
});
我們不希望這是一個(gè)常見的問題成榜,因?yàn)?a target="_blank">我們的eslint-plugin-react-hooks/exhaustive-deps
(請確保您使用它?蛱颉)始終對此發(fā)出警告。
返回未定義的一致錯(cuò)誤
在React 16和更早的版本中伦连,返回undefined
始終是一個(gè)錯(cuò)誤:
function Button() {
return; // Error: Nothing was returned from render
}
這部分是因?yàn)楹苋菀?code>undefined無意地返回:
function Button() {
// We forgot to write return, so this component returns undefined.
// React surfaces this as an error instead of ignoring it.
<button />;
}
以前雨饺,React僅對類和函數(shù)組件執(zhí)行此操作,但不檢查forwardRef
andmemo
組件的返回值惑淳。這是由于編碼錯(cuò)誤额港。
在React 17中,forwardRef
和memo
組件的行為與常規(guī)函數(shù)和類組件一致歧焦。undefined
從他們那里回來是錯(cuò)誤的移斩。
let Button = forwardRef(() => {
// We forgot to write return, so this component returns undefined.
// React 17 surfaces this as an error instead of ignoring it.
<button />;
});
let Button = memo(() => {
// We forgot to write return, so this component returns undefined.
// React 17 surfaces this as an error instead of ignoring it.
<button />;
});
對于您要有意不渲染任何內(nèi)容的情況,請改為返回null
绢馍。
本機(jī)組件堆棧
當(dāng)您在瀏覽器中引發(fā)錯(cuò)誤時(shí)向瓷,瀏覽器會為您提供帶有JavaScript函數(shù)名稱及其位置的堆棧跟蹤。但是舰涌,JavaScript堆棧通常不足以診斷問題猖任,因?yàn)镽eact樹的層次結(jié)構(gòu)可能同樣重要。您不僅要知道Button
引發(fā)了錯(cuò)誤瓷耙,還想知道在React樹中的哪個(gè)位置Button
朱躺。
為了解決這個(gè)問題,當(dāng)您遇到錯(cuò)誤時(shí)搁痛,React 16開始打印“組件堆棾げ螅”。盡管如此鸡典,它們?nèi)匀徊蝗缭鶭avaScript堆棧源请。特別是,它們在控制臺中不可單擊,因?yàn)镽eact不知道函數(shù)在源代碼中聲明的位置谁尸。此外舅踪,它們在生產(chǎn)中幾乎毫無用處。與常規(guī)的最小化JavaScript堆椫⑿冢可以通過源映射自動(dòng)恢復(fù)到原始函數(shù)名稱不同硫朦,使用React組件堆棧,您必須在生產(chǎn)堆棧和捆綁包大小之間進(jìn)行選擇背镇。
在React 17中咬展,使用不同的機(jī)制生成組件堆棧,該機(jī)制將它們與常規(guī)的本機(jī)JavaScript堆椔髡叮縫合在一起破婆。這使您可以在生產(chǎn)環(huán)境中獲得完全符號化的React組件堆棧跟蹤。
React實(shí)現(xiàn)這一點(diǎn)的方式有些不合常規(guī)胸囱。當(dāng)前祷舀,瀏覽器沒有提供獲取函數(shù)的堆棧框架(源文件和位置)的方法烹笔。因此裳扯,當(dāng)React捕獲到錯(cuò)誤時(shí),它現(xiàn)在將通過在可能的情況下從上面每個(gè)組件內(nèi)部拋出(并捕獲)一個(gè)臨時(shí)錯(cuò)誤來重建其組件堆棧谤职。這會增加少量的崩潰性能損失饰豺,但是每個(gè)組件類型只會發(fā)生一次。
如果您感到好奇允蜈,可以在pull請求中閱讀更多詳細(xì)信息冤吨,但是在大多數(shù)情況下,這種確切的機(jī)制不會影響您的代碼饶套。從您的角度來看漩蟆,新功能是現(xiàn)在可以單擊組件堆棧(因?yàn)樗鼈円蕾囉诒緳C(jī)瀏覽器堆棧框架)妓蛮,并且可以像常規(guī)JavaScript錯(cuò)誤那樣在生產(chǎn)中對其進(jìn)行解碼怠李。
構(gòu)成重大變化的部分是,要使此工作正常進(jìn)行蛤克,React將在捕獲錯(cuò)誤后在堆棧中重新執(zhí)行上面某些React函數(shù)和React類構(gòu)造函數(shù)的部分扔仓。由于渲染函數(shù)和類構(gòu)造函數(shù)不應(yīng)具有副作用(這對于服務(wù)器渲染也很重要),因此這不應(yīng)引起任何實(shí)際問題咖耘。
刪除私有導(dǎo)出
最后,最后一個(gè)值得注意的重大變化是我們刪除了一些以前暴露給其他項(xiàng)目的React內(nèi)部組件撬码。特別是儿倒,React Native for Web過去曾經(jīng)依賴于事件系統(tǒng)的某些內(nèi)部組件,但是這種依賴關(guān)系是脆弱的并且經(jīng)常被破壞。
在React 17中夫否,這些私有導(dǎo)出已被刪除彻犁。據(jù)我們所知,React Native for Web是唯一使用它們的項(xiàng)目凰慈,并且他們已經(jīng)完成了向不依賴于那些私有導(dǎo)出的其他方法的遷移汞幢。
這意味著舊版本的React Native for Web不會與React 17兼容,但是新版本將與它兼容微谓。實(shí)際上森篷,這并沒有太大變化,因?yàn)镽eact Native for Web必須發(fā)布新版本以適應(yīng)內(nèi)部React更改豺型。
此外仲智,我們還刪除了ReactTestUtils.SimulateNative
輔助方法。他們從未被記錄下來姻氨,沒有按照他們的名字所暗示的那樣去做钓辆,并且不能與我們對事件系統(tǒng)所做的更改一起使用。如果您想要一種方便的方法來在測試中觸發(fā)本機(jī)瀏覽器事件肴焊,請查看React測試庫前联。