react的各種組件

原文鏈接: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):

  1. 在 shouldComponentUpdate 生命周期做了優(yōu)化會(huì)自動(dòng) shadow diff 組件的 state 和 props擂达,結(jié)合 immutable 數(shù)據(jù)就可以很好地去做更新判斷;
  2. 隔離了父組件與子組件的狀態(tài)變化俭令;

缺點(diǎn):

  1. shouldComponentUpdate 中的 shadow diff 同樣消耗性能后德;
  2. 需要確保組件渲染僅取決于 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):

  1. 簡化代碼顿肺、專注于 render戏溺;
  2. 組件不需要被實(shí)例化渣蜗,無生命周期,提升性能旷祸;
  3. 輸出(渲染)只取決于輸入(屬性)耕拷,無副作用;
  4. 視圖和數(shù)據(jù)的解耦分離托享;

缺點(diǎn):

  1. 無法使用 ref骚烧;
  2. 無生命周期方法;
  3. 無法控制組件的重渲染嫌吠,因?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 有性能上的更大提升:

  1. 減少了組件無意義的重渲染(當(dāng) state 和 props 沒有發(fā)生變化時(shí))乐纸,當(dāng)結(jié)合 immutable 數(shù)據(jù)時(shí)其優(yōu)更為明顯;
  2. 隔離了父組件與子組件的狀態(tài)變化摇予;

當(dāng)我們開始使用 PureComponent 組件汽绢,并不需要做更多的事情,所做的僅僅是將 Component 替換成 PureComponent侧戴;

那么既然 PureComponent 相比 Component 對(duì)性能和渲染上做了更多的優(yōu)化處理宁昭,那么我們是否應(yīng)該在所有地方都使用 PureComponent 替換 Component 嗎?
答案當(dāng)然是否定的酗宋,如果是這樣那么 React 官方早應(yīng)該使用 PureComponent 作為其默認(rèn)組件了积仗。
原因如下:

  1. 我們應(yīng)該避免過早優(yōu)化,當(dāng)在應(yīng)用出現(xiàn)性能瓶頸的時(shí)候才需要去排查與解決這部分的渲染本缠;
  2. 在 shouldComponentUpdate 中先置進(jìn)行新舊屬性與狀態(tài)的淺比較同樣是對(duì)于性能上的消耗斥扛,而其帶來的優(yōu)化效果與性能消耗還需結(jié)合實(shí)際情況進(jìn)行抉擇;
  3. PureComponent 在 shouldComponentUpdate 所做的也僅是淺層的對(duì)象比較丹锹,在屬性/狀態(tài)層級(jí)結(jié)構(gòu)較深較復(fù)雜的情況下容易出現(xiàn)深層 bug稀颁,當(dāng)然如果引入了 immutable 數(shù)據(jù)那么這里的風(fēng)險(xiǎn)將會(huì)大大減小楣黍;
  4. 在我們真正遇到性能瓶頸時(shí)匾灶,很多時(shí)候的處理并不僅僅是比較屬性/狀態(tài)是否改變,因此 PureComponent 在這種情況下優(yōu)勢也不大租漂。

4. 選擇函數(shù)式組件

那么何時(shí)使用函數(shù)式組件呢阶女?

  1. 對(duì)于函數(shù)式組件,由于它不需要處理復(fù)雜的生命周期函數(shù)哩治,因此它在性能上也有一定優(yōu)勢秃踩。
  2. 當(dāng)一個(gè)展示性組件的渲染僅僅依賴于其屬性,使用函數(shù)式組件可以保證它的“純”业筏,并且對(duì)組件本身無任何副作用憔杨;
  3. 由于它無法控制它的重渲染(無 shouldComponentUpdate 生命周期),因此我們希望該組件的屬性數(shù)據(jù)相對(duì)較少蒜胖,同時(shí)組件本身結(jié)構(gòu)相對(duì)簡單消别;
  4. 函數(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)邏輯是一樣的:

  1. 掛載組件時(shí)候監(jiān)聽數(shù)據(jù)變化枉长;
  2. 數(shù)據(jù)變化是改變組件狀態(tài)冀续;
  3. 組件卸載時(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ù)肖油。

參考

作者:極客教程
鏈接:http://www.reibang.com/p/582346a54a3d
來源:簡書
簡書著作權(quán)歸作者所有,任何形式的轉(zhuǎn)載都請(qǐng)聯(lián)系作者獲得授權(quán)并注明出處臂港。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末森枪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子审孽,更是在濱河造成了極大的恐慌县袱,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佑力,死亡現(xiàn)場離奇詭異式散,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)打颤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門暴拄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瘸洛,你說我怎么就攤上這事揍移。” “怎么了反肋?”我有些...
    開封第一講書人閱讀 162,823評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵那伐,是天一觀的道長。 經(jīng)常有香客問我,道長罕邀,這世上最難降的妖魔是什么畅形? 我笑而不...
    開封第一講書人閱讀 58,204評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮诉探,結(jié)果婚禮上日熬,老公的妹妹穿的比我還像新娘。我一直安慰自己肾胯,他們只是感情好竖席,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著敬肚,像睡著了一般毕荐。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上艳馒,一...
    開封第一講書人閱讀 51,190評(píng)論 1 299
  • 那天憎亚,我揣著相機(jī)與錄音,去河邊找鬼弄慰。 笑死第美,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的陆爽。 我是一名探鬼主播什往,決...
    沈念sama閱讀 40,078評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼墓陈!你這毒婦竟也來了恶守?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤贡必,失蹤者是張志新(化名)和其女友劉穎兔港,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仔拟,經(jīng)...
    沈念sama閱讀 45,334評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡衫樊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了利花。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片科侈。...
    茶點(diǎn)故事閱讀 39,727評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖炒事,靈堂內(nèi)的尸體忽然破棺而出臀栈,到底是詐尸還是另有隱情,我是刑警寧澤挠乳,帶...
    沈念sama閱讀 35,428評(píng)論 5 343
  • 正文 年R本政府宣布权薯,位于F島的核電站姑躲,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏盟蚣。R本人自食惡果不足惜黍析,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望屎开。 院中可真熱鬧阐枣,春花似錦、人聲如沸奄抽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽如孝。三九已至宪哩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間第晰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評(píng)論 1 269
  • 我被黑心中介騙來泰國打工彬祖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留茁瘦,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,734評(píng)論 2 368
  • 正文 我出身青樓储笑,卻偏偏與公主長得像甜熔,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子突倍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容