原文鏈接:http://www.reibang.com/p/582346a54a3d
1. 前言
在 React 中涵卵,一切皆是組件绍妨,因此理解組件的工作流與核心尤為重要茫死。
我們有多種創(chuàng)建組件的方式(不僅 Component)屯烦,很多時(shí)候選擇使用哪種組件的創(chuàng)建方式是值得深入考究的舔株;同時(shí)對(duì)于 React 中有太多的組件概念洁灵,無狀態(tài)組件饱岸、高階組件… 常常也是讓新手一頭霧水掺出,因此本文也嘗試解釋分析不同的組件概念。
2. 組件的創(chuàng)建方式
2.1 Component
這是 React 中最常見與最通用的組件創(chuàng)建方式:
class Container extends React.Component {
construcor (props) {
super(props);
this.state = {};
}
render () {
return (
<div className="container">{ this.props.children }</div>
);
}
}
使用了 es6 中類的繼承方法汤锨,當(dāng)然它也有 es5 的寫法(createClass):
var Container = React.createClass({
getInitialState: function() {
return {};
},
render () {
return (
<div className="container">{ this.props.children }</div>
);
}
});
兩種方法都是一樣的返回一個(gè) Container
的組件類,這是我們通常創(chuàng)建組件的方式铐维,因此不做更多闡述了嫁蛇。
2.2 PureComponent
首先我們來理解下 React 組件執(zhí)行重渲染(re-render)更新的時(shí)機(jī)琳疏,一般當(dāng)一個(gè)組件的 props (屬性)或者 state (狀態(tài))發(fā)生改變的時(shí)候新荤,也就是父組件傳遞進(jìn)來的 props 發(fā)生變化或者使用 this.setState
函數(shù)時(shí)篱瞎,組件會(huì)進(jìn)行重新渲染(re-render)严衬;
而在接受到新的 props 或者 state 到組件更新之間會(huì)執(zhí)行其生命周期中的一個(gè)函數(shù) shouldComponentUpdate
粱挡,當(dāng)該函數(shù)返回 true
時(shí)才會(huì)進(jìn)行重渲染竖慧,如果返回false
則不會(huì)進(jìn)行重渲染灌危,在這里 shouldComponentUpdate
默認(rèn)返回 true
;
因此當(dāng)組件遇到性能瓶頸的時(shí)候可以在 shouldComponentUpdate
中進(jìn)行邏輯判斷,來自定義組件是否需要重渲染蔓挖。
PureComponent 是在 react v15.3.0 中新加的一個(gè)組件拷获,從 React 源碼中可以看到它是繼承了 Component 組件:
/**
* Base class helpers for the updating state of a component.
*/
function ReactPureComponent(props, context, updater) {
// Duplicated from ReactComponent.
this.props = props;
this.context = context;
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
function ComponentDummy() {}
ComponentDummy.prototype = ReactComponent.prototype;
var pureComponentPrototype = (ReactPureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = ReactPureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, ReactComponent.prototype);
pureComponentPrototype.isPureReactComponent = true;
同時(shí)在shouldComponentUpdate
函數(shù)中有一段這樣的邏輯:
if (type.prototype && type.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
因此 PureReactComponent 組件和 ReactComponent 組件的區(qū)別就是它在 shouldComponentUpdate 中會(huì)默認(rèn)判斷新舊屬性和狀態(tài)是否相等,如果沒有改變則返回 false
左冬,因此它得以減少組件的重渲染。
當(dāng)然,這里對(duì)新舊屬性和狀態(tài)的比較都為類的淺比較躁垛。
優(yōu)點(diǎn):
- 在 shouldComponentUpdate 生命周期做了優(yōu)化會(huì)自動(dòng) shadow diff 組件的 state 和 props擂达,結(jié)合 immutable 數(shù)據(jù)就可以很好地去做更新判斷;
- 隔離了父組件與子組件的狀態(tài)變化俭令;
缺點(diǎn):
- shouldComponentUpdate 中的 shadow diff 同樣消耗性能后德;
- 需要確保組件渲染僅取決于 props 與 state 瓢湃;
2.3 函數(shù)式組件
在 React 中還可以以函數(shù)來定義一個(gè)組件棍掐,稱之為函數(shù)式組件:
const Button = ({ children, ...props }) => (
<button {...props}>{children}</button>
);
函數(shù)式組件又稱為無狀態(tài)(stateless)組件,它不存在自身的狀態(tài)悲酷,并且沒有普通組件中的各種生命周期方法套菜,同時(shí)其函數(shù)式的寫法決定了其渲染只由屬性決定;
優(yōu)點(diǎn):
- 簡化代碼顿肺、專注于 render戏溺;
- 組件不需要被實(shí)例化渣蜗,無生命周期,提升性能旷祸;
- 輸出(渲染)只取決于輸入(屬性)耕拷,無副作用;
- 視圖和數(shù)據(jù)的解耦分離托享;
缺點(diǎn):
- 無法使用 ref骚烧;
- 無生命周期方法;
- 無法控制組件的重渲染嫌吠,因?yàn)闊o法使用 shouldComponentUpdate 方法止潘,當(dāng)組件接受到新的屬性時(shí)則會(huì)重渲染;
3. Component 與 PureComponent 的選擇
先看一個(gè)例子:
class UserAvatar extends React.Component {
render() {
console.log('UserAvatar re-render');
return (
<div>
< img src={this.props.imageUrl} />
</div>
);
}
}
class Container extends React.Component {
construcor (props) {
super(props);
this.state = {
name: '',
avatar: "http://xxx.xxx/xxxx",
};
}
render () {
const { name, avatar } = this.state;
console.log('Container re-render');
return (
<div className="container">
<UserAvatar imageUrl={} />
<div>{name}</div>
<button onClick={() => this.setState({ name: 'n' })}>CLICK</button>
</div>
);
}
}
例子中 Container 組件為 UserAvatar 組件的父組件辫诅,當(dāng) Container 組件的 state 中的 name 發(fā)生改變的時(shí)候凭戴,Container 執(zhí)行重渲染,而 UserAvatar 也執(zhí)行了重渲染炕矮,即使它的屬性沒有發(fā)生改變么夫;
雖然在 React 中實(shí)現(xiàn)的是 diff 比較,實(shí)際的 dom 并不一定會(huì)被更新肤视,但是 diff 的比較也是非常消耗性能的档痪;
當(dāng)把 UserAvatar 改成繼承 PureComponent 之后,那么它將會(huì)在 shouldComponentUpdate 進(jìn)行淺比較邢滑,如果屬性沒有改變腐螟,則不會(huì)進(jìn)行重渲染。
因此相比于 Component 困后,PureComponent 有性能上的更大提升:
- 減少了組件無意義的重渲染(當(dāng) state 和 props 沒有發(fā)生變化時(shí))乐纸,當(dāng)結(jié)合 immutable 數(shù)據(jù)時(shí)其優(yōu)更為明顯;
- 隔離了父組件與子組件的狀態(tài)變化摇予;
當(dāng)我們開始使用 PureComponent 組件汽绢,并不需要做更多的事情,所做的僅僅是將 Component 替換成 PureComponent侧戴;
那么既然 PureComponent 相比 Component 對(duì)性能和渲染上做了更多的優(yōu)化處理宁昭,那么我們是否應(yīng)該在所有地方都使用 PureComponent 替換 Component 嗎?
答案當(dāng)然是否定的酗宋,如果是這樣那么 React 官方早應(yīng)該使用 PureComponent 作為其默認(rèn)組件了积仗。
原因如下:
- 我們應(yīng)該避免過早優(yōu)化,當(dāng)在應(yīng)用出現(xiàn)性能瓶頸的時(shí)候才需要去排查與解決這部分的渲染本缠;
- 在 shouldComponentUpdate 中先置進(jìn)行新舊屬性與狀態(tài)的淺比較同樣是對(duì)于性能上的消耗斥扛,而其帶來的優(yōu)化效果與性能消耗還需結(jié)合實(shí)際情況進(jìn)行抉擇;
- PureComponent 在 shouldComponentUpdate 所做的也僅是淺層的對(duì)象比較丹锹,在屬性/狀態(tài)層級(jí)結(jié)構(gòu)較深較復(fù)雜的情況下容易出現(xiàn)深層 bug稀颁,當(dāng)然如果引入了 immutable 數(shù)據(jù)那么這里的風(fēng)險(xiǎn)將會(huì)大大減小楣黍;
- 在我們真正遇到性能瓶頸時(shí)匾灶,很多時(shí)候的處理并不僅僅是比較屬性/狀態(tài)是否改變,因此 PureComponent 在這種情況下優(yōu)勢也不大租漂。
4. 選擇函數(shù)式組件
那么何時(shí)使用函數(shù)式組件呢阶女?
- 對(duì)于函數(shù)式組件,由于它不需要處理復(fù)雜的生命周期函數(shù)哩治,因此它在性能上也有一定優(yōu)勢秃踩。
- 當(dāng)一個(gè)展示性組件的渲染僅僅依賴于其屬性,使用函數(shù)式組件可以保證它的“純”业筏,并且對(duì)組件本身無任何副作用憔杨;
- 由于它無法控制它的重渲染(無 shouldComponentUpdate 生命周期),因此我們希望該組件的屬性數(shù)據(jù)相對(duì)較少蒜胖,同時(shí)組件本身結(jié)構(gòu)相對(duì)簡單消别;
- 函數(shù)式組件能夠更好地與業(yè)務(wù)抽離,并且易于測試台谢。
因此對(duì)于簡單的通用性組件寻狂,比如自定義的 <Button />、<Input /> 等組件選擇函數(shù)式組件是再好不過的了朋沮。
5. 純組件與無狀態(tài)組件
那么什么樣的組件才算是 “純” 的呢蛇券?
React 中將 PureComponent 定義為純組件,假如一個(gè)組件只和 props 和 state 有關(guān)系樊拓,給定相同的 props 和 state 就會(huì)渲染出相同的結(jié)果纠亚,且不受副作用影響,那么這個(gè)組件就叫做純組件骑脱,或者說純組件只依賴于組件的 props 和 state 菜枷。
那么使用 PureComponent 的組件就一定是所謂的 “純” 組件了嗎?
事實(shí)上叁丧,在 PureComponent 中仍然會(huì)在生命周期中對(duì)其產(chǎn)生副作用啤誊,比如你可以在 componentDidMount 發(fā)送 ajax 請(qǐng)求,或者通過計(jì)算 dom 去改變某個(gè) div 的高度拥娄。
因此組件的 “純” 取決于你實(shí)際代碼中是如何實(shí)現(xiàn)的蚊锹。
?
而對(duì)于無狀態(tài)組件它僅由其屬性決定,且它通過函數(shù)式定義因此不存在副作用稚瘾,無狀態(tài)組件也是一種 “純” 組件牡昆。
6. Smart 組件與 Dumb 組件
當(dāng)應(yīng)用的視圖層與數(shù)據(jù)層解耦的情況下,比如結(jié)合 Redux 或者 Mobx 等狀態(tài)管理庫,那么在一個(gè)應(yīng)用中組件又分為 Smart 組件與 Dumb 組件丢烘。
6.1 Smart 組件
Smart 組件又稱為 容器組件柱宦,它負(fù)責(zé)處理應(yīng)用的數(shù)據(jù)以及業(yè)務(wù)邏輯,?同時(shí)將狀態(tài)數(shù)據(jù)與操作函數(shù)作為屬性傳遞給子組件播瞳;
一般而言它僅維護(hù)很少的 DOM掸刊,其所有的 DOM 也僅是作為布局等作用。
6.2 Dumb 組件
Dumb 組件又稱為 木偶組件赢乓,它負(fù)責(zé)展示作用以及響應(yīng)用戶交互忧侧,它一般是無狀態(tài)的(在如 Modal 等類組件中可能會(huì)維護(hù)少量自身狀態(tài));
一般而言 Dumb 組件會(huì)拆分為一個(gè)個(gè)可復(fù)用牌芋、功能單一的組件蚓炬;
因此 Dumb 組件使用函數(shù)式組件定義,當(dāng)其需要對(duì)重渲染進(jìn)行優(yōu)化時(shí)則可以使用 PureComponent躺屁。
所以 Smart 組件更多關(guān)注與數(shù)據(jù)以及業(yè)務(wù)邏輯肯夏,而 Dumb 組件與數(shù)據(jù)和業(yè)務(wù)解耦,主要復(fù)雜 UI 層面的展示與交互楼咳。
7. 高階組件
高階組件(higher-order-components)是react中對(duì)組件邏輯進(jìn)行重用的高級(jí)技術(shù)熄捍。但高階組件本身并不是React API。它只是一種模式母怜,這種模式是由react自身的組合性質(zhì)必然產(chǎn)生的余耽。
這里需要明確一點(diǎn):高階組件并不是一個(gè)組件類,它是一個(gè)函數(shù)苹熏,接收一個(gè)組件并返回一個(gè)新組件碟贾。
const newComponent = higherOrderComponent(oldComponent);
高階組件(HOC)是一種修飾者模式,它對(duì)原有組件進(jìn)行改造并生成新的組件轨域,對(duì)組件代碼進(jìn)行的復(fù)用袱耽。
?
下面例子?來自 https://discountry.github.io/react/docs/higher-order-components.html;
假設(shè)有一個(gè) CommentList 組件:
class CommentList extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
// "DataSource" is some global data source
comments: DataSource.getComments()
};
}
componentDidMount() {
// Subscribe to changes
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
// Clean up listener
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
// Update component state whenever the data source changes
this.setState({
comments: DataSource.getComments()
});
}
render() {
return (
<div>
{this.state.comments.map((comment) => (
<Comment comment={comment} key={comment.id} />
))}
</div>
);
}
}
之后你又有一個(gè) BlogPost 組件:
class BlogPost extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
blogPost: DataSource.getBlogPost(props.id)
};
}
componentDidMount() {
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
blogPost: DataSource.getBlogPost(this.props.id)
});
}
render() {
return <TextBlock text={this.state.blogPost} />;
}
}
CommentList 和 BlogPost 組件并不相同 —— 他們調(diào)用了 DataSource 的不同方法獲取數(shù)據(jù)干发,并且他們渲染的輸出結(jié)果也不相同朱巨。但是,他們的大部分實(shí)現(xiàn)邏輯是一樣的:
- 掛載組件時(shí)候監(jiān)聽數(shù)據(jù)變化枉长;
- 數(shù)據(jù)變化是改變組件狀態(tài)冀续;
- 組件卸載時(shí)移除監(jiān)聽函數(shù);
設(shè)想一下必峰,在一個(gè)大型的應(yīng)用中洪唐,這種從 DataSource 訂閱數(shù)據(jù)并調(diào)用 setState 的模式將會(huì)一次又一次的發(fā)生。我們就可以抽象出一個(gè)模式吼蚁,該模式允許我們在一個(gè)地方定義邏輯并且能對(duì)所有的組件使用凭需,這就是高階組件的精華所在。
我們寫一個(gè)函數(shù),該函數(shù)能夠創(chuàng)建類似 CommonList 和 BlogPost 從 DataSource 數(shù)據(jù)源訂閱數(shù)據(jù)的組件 粒蜈。該函數(shù)接受一個(gè)子組件作為其中的一個(gè)參數(shù)顺献,并從數(shù)據(jù)源訂閱數(shù)據(jù)作為props屬性傳入子組件。我們把這個(gè)函數(shù)取個(gè)名字 withSubscription:
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
});
第一個(gè)參數(shù)是包裹組件(wrapped component)薪伏,第二個(gè)參數(shù)會(huì)從 DataSource和當(dāng)前props 屬性中檢索應(yīng)用需要的數(shù)據(jù)滚澜。
當(dāng) CommentListWithSubscription 和 BlogPostWithSubscription 渲染時(shí), 會(huì)向CommentList 和 BlogPost 傳遞一個(gè) data props屬性粗仓,該 data屬性的數(shù)據(jù)包含了從 DataSource 檢索的最新數(shù)據(jù):
// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... that takes care of the subscription...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
因?yàn)?withSubscription 就是一個(gè)普通函數(shù),你可以添加任意數(shù)量的參數(shù)。例如羞迷,你或許會(huì)想使 data 屬性可配置化号醉,使高階組件和包裹組件進(jìn)一步隔離開÷旖铮或者你想要接收一個(gè)參數(shù)用于配置 shouldComponentUpdate 函數(shù)存捺,或配置數(shù)據(jù)源的參數(shù)。這些都可以實(shí)現(xiàn)曙蒸,因?yàn)楦唠A組件可以完全控制新組件的定義捌治。
在很多第三方庫中都使用到了高階組件,比如 react-router 中的 connect 就是連接了 redux 狀態(tài)與組件的高階組件纽窟,或者 react-router 中的 withRouter 用來給組件注入 ?history 數(shù)據(jù)肖油。
參考
- https://news.ycombinator.com/item?id=14418576
- https://discountry.github.io/react/docs/higher-order-components.html
- https://stackoverflow.com/questions/40703675/react-functional-stateless-component-purecomponent-component-what-are-the-dif
作者:極客教程
鏈接:http://www.reibang.com/p/582346a54a3d
來源:簡書
簡書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處臂港。