React API 和代碼重用的演變!
本文將探究 React API 的演變及其背后的心智模型。從 mixins 到 hooks,再到 RSCs,了解整個(gè)過程中的權(quán)衡。我們將對(duì) React 的過去、現(xiàn)在和未來有一個(gè)更清晰的了解,便于深入研究遺留代碼庫并評(píng)估其他技術(shù)如何采用不同的方法并做出不同的權(quán)衡。
React API 簡史
我們從面向?qū)ο蟮脑O(shè)計(jì)模式在 JS 生態(tài)系統(tǒng)中流行的時(shí)候開始,可以在早期的 React API 中看到這種影響。
Mixins
React.createClass API 是創(chuàng)建組件的原始方式。在 Javascript 支持原生類語法之前,React 就有自己的類表示。Mixins 是一種用于代碼重用的通用 OOP 模式,下面是一個(gè)簡化的例子:
function ShoppingCart() {
this.items = [];
}
var orderMixin = {
calculateTotal() {
// 從 this.items 計(jì)算
}
// .. 其他方法
}
Object.assign(ShoppingCart.prototype, orderMixin)
var cart = new ShoppingCart()
cart.calculateTotal()
Javascript 不支持多重繼承,因此 mixin 是重用共享行為和擴(kuò)充類的一種方式。那該如何在使用 createClass 創(chuàng)建的組件之間共享邏輯呢?Mixins 是一種常用的模式,它可以訪問組件的生命周期方法,允許我們組合邏輯、狀態(tài)等:
var SubscriptionMixin = {
getInitialState: function() {
return {
comments: DataSource.getComments()
};
},
// 當(dāng)一個(gè)組件使用多個(gè) mixin 時(shí),React 會(huì)嘗試合并多個(gè)mixin的生命周期方法,因此每個(gè)都會(huì)被調(diào)用
componentDidMount: function() {
console.log('do something on mount')
},
componentWillUnmount: function() {
console.log('do something on unmount')
},
}
// 將對(duì)象傳遞給 createClass
var CommentList = React.createClass({
// 在 mixins 屬性下定義它們
mixins: [SubscriptionMixin, AnotherMixin, SomeOtherMixin],
render: function() {
var { comments, ...otherStuff } = this.state
return (
<div>
{comments.map(function(comment) {
return <Comment key={comment.id} comment={comment} />
})}
</div>
)
}
})
對(duì)于較小的應(yīng)用,這種方式可以正常運(yùn)行。但是,當(dāng) Mixin 應(yīng)用到大型項(xiàng)目時(shí),它們也有一些缺點(diǎn):
- 名稱沖突:Mixin 具有共享的命名空間,當(dāng)多個(gè) Mixin 使用同一個(gè)方法或狀態(tài)的名稱時(shí),會(huì)發(fā)生沖突。
- 隱式依賴關(guān)系:確定哪個(gè) Mixin 提供了哪些功能或狀態(tài)比較麻煩。它們使用共享的屬性鍵相互交互,從而創(chuàng)建隱式耦合。
- 難以理解和調(diào)試:Mixin 通常使組件更難以理解和調(diào)試。例如,上面多個(gè) Mixin 都可以對(duì) getInitialState 結(jié)果有影響,使得跟蹤問題變得更加困難。
在感受到這些問題的痛苦之后,React 團(tuán)隊(duì)發(fā)布了“Mixins Considered Harmful”,不鼓勵(lì)繼續(xù)使用這種模式。
高階組件
當(dāng) Javascript 中支持了原生類語法后,React 團(tuán)隊(duì)就在 v15.5 中棄用了 createClass API,支持原生類。
在這個(gè)轉(zhuǎn)變過程中,我們?nèi)匀话凑疹惡蜕芷诘乃悸穪硭伎迹虼藳]有進(jìn)行重大的心智模型轉(zhuǎn)變?,F(xiàn)在可以擴(kuò)展包含生命周期方法的 Reacts Component 類:
class MyComponent extends React.Component {
constructor(props) {
// 在組件掛載到 DOM 之前運(yùn)行
// super 指的是父 Component 的構(gòu)造函數(shù)
super(props)
}
componentWillMount() {}
componentDidMount(){}
componentWillUnmount() {}
componentWillUpdate() {}
shouldComponentUpdate() {}
componentWillReceiveProps() {}
getSnapshotBeforeUpdate() {}
componentDidUpdate() {}
render() {}
}
考慮到 mixin 的缺陷,我們?cè)撊绾我赃@種編寫 React 組件的新方式來共享邏輯和副作用呢?
這時(shí)候,高階組件 (HOC) 就出現(xiàn)了,它的名字來源于高階函數(shù)的函數(shù)式編程概念。它成為了替代 Mixin 的一種流行方式,并出現(xiàn)在像 Redux 這樣的庫的 API 中,例如它的 connect 函數(shù),用于將組件連接到 Redux 存儲(chǔ)。除此之外,還有 React Router 的 withRouter。
// 一個(gè)創(chuàng)建增強(qiáng)組件的函數(shù),有一些額外的狀態(tài)、行為或 props
const EnhancedComponent = myHoc(MyComponent);
// HOC 的簡化示例
function myHoc(Component) {
return class extends React.Component {
componentDidMount() {
console.log('do stuff')
}
render() {
// 使用一些注入的 props 渲染原始組件
return <Component {...this.props} extraProps={42} />
}
}
}
高階組件對(duì)于在多個(gè)組件之間共享通用行為非常有用。它們使包裝的組件保持解耦和通用性,以便可以重用。然而,HOC 遇到了與 mixin 類似的問題:
- 名稱沖突:因?yàn)?HOC 需要轉(zhuǎn)發(fā)和傳播 ...this.props 到包裝的組件中,所以嵌套的 HOC 相互覆蓋可能會(huì)發(fā)生沖突。
- 難以靜態(tài)類型檢查:當(dāng)多個(gè)嵌套的 HOC 將新的 props 注入到包裝的組件中時(shí),正確的 props 輸入很難保證。
- 數(shù)據(jù)流模糊:對(duì)于 mixins,問題是“這個(gè)狀態(tài)從哪里來?”;對(duì)于 HOC,問題是“這些 props 從哪里來?”。因?yàn)樗鼈兪窃谀K級(jí)別靜態(tài)組合的,所以很難跟蹤數(shù)據(jù)流。
除了這些陷阱之外,過度使用 HOC 還導(dǎo)致了深度嵌套和復(fù)雜的組件層次結(jié)構(gòu)以及難以調(diào)試的性能問題。
Render props
render prop 模式作為 HOC 的替代品出現(xiàn),這種模式由開源 API 如 React-Motion 和 downshift 以及構(gòu)建 React Router 的開發(fā)人員推廣普及。
<Motion style={{ x: 10 }}>
{interpolatingStyle => <div style={interpolatingStyle} />}
</Motion>
主要思想就是將一個(gè)函數(shù)作為 props 傳遞給組件。然后組件會(huì)在內(nèi)部調(diào)用該函數(shù),并傳遞數(shù)據(jù)和方法,將控制反轉(zhuǎn)回函數(shù)以繼續(xù)渲染它們想要的內(nèi)容。
與 HOC 不同,組合發(fā)生在 JSX 內(nèi)部的運(yùn)行時(shí),而不是靜態(tài)模塊范圍內(nèi)。它們沒有名稱沖突,因?yàn)楹苊鞔_知道是從哪里來的,也更容易進(jìn)行靜態(tài)類型檢查。
但是,當(dāng)用作數(shù)據(jù)提供者時(shí),它們可能會(huì)導(dǎo)致深度嵌套,創(chuàng)建一個(gè)虛假的組件層次結(jié)構(gòu):
<UserProvider>
{user => (
<UserPreferences user={user}>
{userPreferences => (
<Project user={user}>
{project => (
<IssueTracker project={project}>
{issues => (
<Notification user={user}>
{notifications => (
<TimeTracker user={user}>
{timeData => (
<TeamMembers project={project}>
{teamMembers => (
<RenderThangs renderItem={item => (
// ...
)}/>
)}
</TeamMembers>
)}
</TimeTracker>
)}
</Notification>
)}
</IssueTracker>
)}
</Project>
)}
</UserPreferences>
)}
</UserProvider>
這時(shí),通常會(huì)將管理狀態(tài)的組件與渲染 UI 的組件分開來處理。隨著 Hooks 的出現(xiàn),“容器”和“展示性”組件模式已經(jīng)不再流行。但值得一提的是,這種模式在服務(wù)逇組件中有所復(fù)興。
目前,render props 仍然是創(chuàng)建可組合組件 API 的有效模式。
Hooks
Hooks 在 React 16.8 版本中成為了官方的重用邏輯的方式,鞏固了將函數(shù)組件作為編寫組件的推薦方式。
Hooks 讓在組件中重用和組合邏輯變得更加簡單明了。相比于類組件,在其中封裝并共享邏輯會(huì)更加棘手,因?yàn)樗鼈兛赡芊稚⒃诟鞣N生命周期方法中的不同部分。
深度嵌套的結(jié)構(gòu)可以被簡化和扁平化。搭配 TypeScript,Hook 也很容易進(jìn)行類型化。
function Example() {
const user = useUser();
const userPreferences = useUserPreferences(user);
const project = useProject(user);
const issues = useIssueTracker(project);
const notifications = useNotification(user);
const timeData = useTimeTracker(user);
const teamMembers = useTeamMembers(project);
return (
<div>
{/* 渲染內(nèi)容 */}
</div>
);
}
權(quán)衡利弊
使用 Hooks 帶來了很多好處,它們解決了類中的一些問題,但也需要付出一定的代價(jià),下面來深入了解一下。
類 vs 函數(shù)
從組件消費(fèi)者的角度來看,類組件到函數(shù)組件的轉(zhuǎn)變并沒有改變渲染 JSX 的方式。不過兩種方式的思想是不同的:
- 類與有狀態(tài)類的面向?qū)ο缶幊?/strong>有著緊密聯(lián)系。
- 函數(shù)則與函數(shù)式編程以及純函數(shù)等概念有關(guān)聯(lián)。
React 中的組件概念,以及使用 JavaScript 實(shí)現(xiàn)它的方式,以及我們?cè)噲D使用現(xiàn)有術(shù)語來解釋它,都增加了學(xué)習(xí) React 的開發(fā)人員建立準(zhǔn)確思維模型的困難度。對(duì)理解的漏洞會(huì)導(dǎo)致代碼出現(xiàn) bug。在這個(gè)過渡階段中,一些常見的問題包括設(shè)置狀態(tài)或獲取數(shù)據(jù)時(shí)的無限循環(huán),以及讀取過時(shí)的 props 和 state。指令式響應(yīng)事件和生命周期常常引入了不必要的狀態(tài)副作用,我們可能并不需要它們。
開發(fā)者體驗(yàn)
在使用類組件時(shí),有一套不同的術(shù)語,如 componenDid、componentWill、shouldComponent和將方法綁定到實(shí)例中。函數(shù)和 Hooks 通過移除外部類簡化了這一點(diǎn),使我們能夠?qū)W⒂阡秩竞瘮?shù)。每次渲染都會(huì)重新創(chuàng)建所有內(nèi)容,因此需要能夠在渲染周期之間保留一些內(nèi)容。useCallback 和 useMemo 這樣的 API 被引入就方便定義哪些內(nèi)容應(yīng)該在重新渲染之間保留下來。
在 Hooks 中需要明確管理依賴數(shù)組,再加上 hooks API 的語法復(fù)雜,對(duì)一些人來說富有挑戰(zhàn)性。對(duì)其他人來說,hooks 大大簡化了他們對(duì) React 的思維模型和代碼的理解。
實(shí)驗(yàn)性 React forget 旨在通過預(yù)編譯 React 組件來改善開發(fā)者體驗(yàn),從而消除手動(dòng)記憶和管理依賴項(xiàng)數(shù)組,強(qiáng)調(diào)將事情明確化或嘗試在幕后處理事情之間的權(quán)衡。
將狀態(tài)和邏輯耦合到 React 中
許多狀態(tài)管理庫,如 Redux 或 MobX 將 React 應(yīng)用的狀態(tài)和視圖分開處理。這與 React 最初作為MVC 中的“視圖”標(biāo)語保持一致。隨著時(shí)間的推移,從全局的單塊式存儲(chǔ)向更多的位置遷移,特別是使用 render props 的“一切皆為組件”的想法,這也隨著轉(zhuǎn)向 hooks 得到了鞏固。
React 演進(jìn)背后的原則
我們可以從這些模式的演變中學(xué)到什么呢?哪些啟發(fā)式可以指導(dǎo)我們做出有價(jià)值的權(quán)衡?
API 的用戶體驗(yàn)
框架和庫必須同時(shí)考慮開發(fā)者體驗(yàn)和最終用戶體驗(yàn)。為開發(fā)者體驗(yàn)而犧牲用戶體驗(yàn)是一種錯(cuò)誤的做法,但有時(shí)候一個(gè)會(huì)優(yōu)先于另一個(gè)。
例如,CSS in JS庫 styled-components,在處理大量動(dòng)態(tài)樣式時(shí)使用起來非常棒,但它們可能以最終用戶體驗(yàn)為代價(jià),我們需要對(duì)此進(jìn)行權(quán)衡。
我們可以將 React 18 和 RSC 中的并發(fā)特性視為追求更好的最終用戶體驗(yàn)的創(chuàng)新。這些就意味著更新用來實(shí)現(xiàn)組件的 API 和模式。函數(shù)的“snapshotting”屬性(閉包)使得編寫在并發(fā)模式下正常工作的代碼變得更加容易,服務(wù)端的異步函數(shù)是表達(dá)服務(wù)端組件的好方法。
API 優(yōu)于實(shí)現(xiàn)
上面討論的 API 和模式都是從實(shí)現(xiàn)組件內(nèi)部的角度出發(fā)的。雖然實(shí)現(xiàn)細(xì)節(jié)已經(jīng)從 createClass 發(fā)展到了 ES6 類,再到有狀態(tài)函數(shù)。但“組件”這個(gè)更高級(jí)別的API概念,它可以是有狀態(tài)的并具有 effect,已經(jīng)在整個(gè)演進(jìn)過程中保持了穩(wěn)定性:
return (
<ImplementedWithMixins>
<ComponentUsingHOCs>
<ThisUsesHooks>
<ServerComponentWoah />
</ThisUsesHooks>
</ComponentUsingHOCs>
</ImplementedWithMixins>
)
專注于正確的原語
在React中,組件模型讓我們可以用聲明式的方式來編寫代碼,并且可以方便地在本地進(jìn)行處理。這使得代碼更加易于移植,可以更輕松地刪除、移動(dòng)、復(fù)制和粘貼代碼,而不會(huì)意外破壞其中的任何隱藏的連接。遵循這個(gè)模型的架構(gòu)和模式可以提供更好的可組合性,通常需要保持局部化,讓組件捕獲相關(guān)的關(guān)注點(diǎn),并接受由此帶來的權(quán)衡。與這個(gè)模型不符的抽象化會(huì)使數(shù)據(jù)流變得模糊,并使跟蹤和調(diào)試變得難以理解和處理,從而增加了隱含的耦合。一個(gè)例子就是從類到 hooks 的轉(zhuǎn)換,將分布在多個(gè)生命周期事件中的邏輯打包成可組合的函數(shù),可以直接放置在組件中的相應(yīng)位置。
小結(jié)
考慮 React 的一個(gè)好方法是將其視為一個(gè)庫,它提供了一組可在其上構(gòu)建的低級(jí)原語。React非常靈活,可以按照自己的方式來設(shè)計(jì)架構(gòu),這既是一種福音,也可能帶來一些問題。這也解釋了為什么像 Remix 和 Next 這樣的高級(jí)應(yīng)用框架如此受歡迎,它們會(huì)在React基礎(chǔ)之上添加更強(qiáng)烈的設(shè)計(jì)意圖和抽象化。
React 的擴(kuò)展心智模型
隨著 React 將其范圍擴(kuò)展到客戶端之外,它提供了允許開發(fā)人員構(gòu)建全棧應(yīng)用的原語。在前端編寫后端代碼開辟了一系列新的模式和權(quán)衡。與之前的轉(zhuǎn)變相比,這些轉(zhuǎn)變更多的是對(duì)現(xiàn)有心智模型的擴(kuò)展,而不是需要忘記之前的范式轉(zhuǎn)變。
在混合模型中,客戶端和服務(wù)端組件都對(duì)整體計(jì)算架構(gòu)有所貢獻(xiàn)。在服務(wù)端做更多的事情有助于提高 web 體驗(yàn),它允許卸載計(jì)算密集型任務(wù)并避免通過網(wǎng)絡(luò)發(fā)送臃腫的包。但是,如果我們需要比完整的服務(wù)端往返延遲少得多的快速交互,則客戶端驅(qū)動(dòng)的方法會(huì)更好。React 就是從該模型的僅客戶端部分演變而來的,但可以想象 React 首先從服務(wù)器開始,然后再添加客戶端部分。
了解全棧 React
混合客戶端和服務(wù)端需要知道邊界在模塊依賴圖中的位置。這樣就能夠更好地理解代碼在何時(shí)、何地以及如何運(yùn)行。
為此,我們開始看到一種新的React模式,即指令(或類似于“use strict”、“use asm”或React Native中的“worklet”的編譯指示),它們可以改變其后代碼的含義。
理解“use client”
將此代碼放置在導(dǎo)入代碼之前的文件頂部,可以表明以下的代碼是“客戶端代碼”,標(biāo)志著與僅在服務(wù)端上運(yùn)行的代碼進(jìn)行區(qū)分。其中導(dǎo)入的其他模塊(及其依賴項(xiàng))被認(rèn)為是客戶端包,通過網(wǎng)絡(luò)傳輸。
使用“use client”組件也可以在服務(wù)端運(yùn)行。例如,作為生成初始 HTML 或作為靜態(tài)網(wǎng)站生成過程的一部分。
“use server”指令
Action 函數(shù)是客戶端調(diào)用在服務(wù)端存在的函數(shù)的方式。可以將“use server”放置在服務(wù)器組件的 Action 函數(shù)頂部,以告訴編譯器應(yīng)該在服務(wù)端保留它。
// 在服務(wù)器組件內(nèi)部
// 允許客戶端引用和調(diào)用這個(gè)函數(shù)
// 不發(fā)送給客戶端
// server (RSC) -> client (RPC) -> server (Action)
async function update(formData: FormData) {
'use server'
await db.post.update({
content: formData.get('content'),
})
}
在 Next.js 中,如果一個(gè)文件頂部有“use server”,它告訴打包工具所有導(dǎo)出都是服務(wù)端 Action 函數(shù),這確保函數(shù)不會(huì)包含在客戶端捆綁包中。
當(dāng)后端和前端共享同一個(gè)模塊依賴圖時(shí),有可能會(huì)意外地發(fā)送一堆不想要的客戶端代碼,或者更糟糕的是,意外將敏感數(shù)據(jù)導(dǎo)入到客戶端捆綁包中。為了確保這種情況不會(huì)發(fā)生,還有“server-only”包作為標(biāo)記邊界的一種方式,以確保其后的代碼僅在服務(wù)端組件上使用。這些實(shí)驗(yàn)性的指令和模式也正在其他框架中進(jìn)行探索,超越了 React,并使用類似于server$
的語法來標(biāo)記這種區(qū)別。
全棧組合
在這個(gè)轉(zhuǎn)變中,組件的抽象被提升到一個(gè)更高的層次,包括服務(wù)端和客戶端元素。這使得可以重用和組合整個(gè)全棧功能垂直切片的可能性。
// 可以想象可共享的全棧組件
// 封裝了服務(wù)端和客戶端的細(xì)節(jié)
<Suspense fallback={<LoadingSkelly />}>
<AIPoweredRecommendationThing
apiKey={proccess.env.AI_KEY}
promptContext={getPromptContext(user)}
/>
</Suspense>
這種強(qiáng)大的能力是建立在 React 之上的元框架中使用的高級(jí)打包工具、編譯器和路由器的基礎(chǔ)上的,因此付出的代價(jià)來自于其底層的復(fù)雜性。同時(shí),作為前端開發(fā)者,我們需要擴(kuò)展自己的思維模型,以理解將后端代碼與前端代碼寫在同一個(gè)模塊依賴圖中所帶來的影響。
總結(jié)
本文探討了很多內(nèi)容,從mixin到服務(wù)端組件,探索了 React 的演變和每種范例的權(quán)衡。理解這些變化及其基礎(chǔ)原則是構(gòu)建一個(gè)清晰的 React 思維模型的好方法。準(zhǔn)確的思維模型使我們能夠高效地構(gòu)建,并快速定位錯(cuò)誤和性能瓶頸。