React學習—React漫談

前端閱讀室

事件系統

React基於Virtual DOM實現了一個SyntheticEvent(合成事件)層,咱們定義的處理器會接收一個SyntheticEvent對象的實例,它徹底符合W3C標準,不會存在任何IE的兼容性問題。而且與原生的瀏覽器事件同樣擁有一樣的接口,一樣支持事件的冒泡機制,我門可使用stopPropagation()和preventDefault()來中斷它。若是須要訪問原生事件對象,可使用nativeEvent屬性。css

合成事件的綁定方式

React事件的綁定方式與原生的HTML事件監聽器屬性很類似。html

<button onClick={this.handleClick}>Test</button>

合成事件的實現機制

在React底層,主要對合成事件作了兩件事:事件委派和自動綁定。前端

1.事件委派react

React不會把事件處理函數直接綁定到真實的節點上,而是把全部事件綁定到結構的最外層,使用一個統一的事件監聽器,這個事件監聽器上維持了一個映射來保存全部組件內部的事件監聽和處理函數。當組件掛載或卸載時,只是在這個統一的事件監聽器上插入或刪除一些對象;當事件發生時,首先被這個統一的事件監聽器處理,而後在映射裏找到真正的事件處理函數並調用。(實現原理:對最外層的容器進行綁定,依賴事件的冒泡機制完成委派。)這樣簡化了事件處理和回收機制,效率也有很大提高。webpack

2.自動綁定web

在React組件中,每一個方法的上下文都會指向該組件的實例,即自動綁定this爲當前組件。並且React還會對這種引用進行緩存。在使用ES6 classes或者純函數時,這種自動綁定就不復存在了,咱們須要手動實現this的綁定。
咱們來看幾種綁定方法
bind方法算法

class App extends Component {
  constuctor() {
    super(props)
    this.handleClick = this.handleClick.bind(this)
  }
}

箭頭函數能夠自動綁定此函數的做用域的this數據庫

class App extends Component {
  handleClick= () => {}
}

在React中使用原生事件

class NativeEventDemo extends Component {
  componentDidMount() {
    this.refs.button.addEventListener('click', this.handleClick)
  }
  componentWillUnmout() {
    this.refs.button.removeEventListener('click', this.handleClick)
  }
}

對比React合成事件與JavaScript原生事件

1.事件傳播與阻止事件傳播編程

瀏覽器原生DOM事件的傳播能夠分爲3個階段:事件捕獲階段、目標對象自己的事件處理程序調用、事件冒泡。能夠將e.addEventListener的第三個參數設置爲true時,爲元素e註冊捕獲事件處理程序。事件捕獲在IE9如下沒法使用。事件捕獲在應用程序開發中意義不大,React在合成事件中並無實現事件捕獲,僅僅支持了事件冒泡機制。後端

阻止原生事件傳播須要使用e.stopPropagation,不過對於不支持該方法的瀏覽器(IE9如下)只能使用e.cancelBubble = true來阻止。而在React合成事件中,只須要使用stopPropagation()便可。阻止React事件冒泡的行爲只能用於React合成事件系統中,且沒有辦法阻止原生事件的冒泡。反之,原生事件阻止冒泡,能夠阻止React合成事件的傳播。

2.事件類型

React合成事件的事件類型是JavaScript原生事件類型的一個子集。它僅僅實現了DOM Level3的事件接口,而且統一了瀏覽器的兼容問題。有些事件React沒有實現,或者受某些限制沒辦法去實現,如window的resize事件。

3.事件綁定方式

受到DOM標準影響,瀏覽器綁定原生事件的方式有不少種。React合成事件的綁定方式則簡單不少

<button onClick={this.handleClick}>Test</button>

4.事件對象

在React合成事件系統中,不存在兼容性問題,能夠獲得一個合成事件對象。

表單

在React中,一切數據都是狀態,固然也包括表單數據。接下來咱們講講React是如何處理表單的。

應用表單組件

html表單中的全部組件在React的JSX都有實現,只是它們在用法上有些區別,有些是JSX語法上的,有些則是因爲React對狀態處理上致使的一些區別。

1.文本框

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      inputValue: '',
      textareaValue: ''
    }
  }

  handleInputChange = (e) => {
    this.setState({
      inputValue: e.target.value
    });
  }

  handleTextareaChange = (e) => {
    this.setState({
      textareaValue: e.target.value
    })
  }

  render() {
    const { inputValue, textareaValue } = this.state;
    return (
      <div>
        <p>
          單行輸入框:
          <input type="text" value={inputValue} onChange={this.handleInputChange}/>
        </p>
        <p>
          多行輸入框:
          <textarea type="text" value={textareaValue} onChange={this.handleTextareaChange}/>
        </p>
      </div>
    )
  }

}

在HTML中textarea的值是經過children來表示的,而在react中是用一個value prop來表示表單的值的。

2.單選按鈕與複選框

在HTML中,用類型爲radio的input標籤表示單選按鈕,用類型爲checkbox的input標籤表示複選框。這兩種表單的value值通常是不會改變的,而是經過一個布爾類型的checked prop來表示是否爲選中狀態。在JSX中這些是相同的,不過用法上仍是有些區別。

單選按鈕的示例

import React, { Component } from 'react';

class App extends Component {
  construtor(props) {
    super(props);
    this.state = {
      radioValue: '',
    }
  }

  handleChange = (e) => {
    this.setState(
      radioValue: e.target.value
    )
  }

  render() {
    const { radioValue } = this.state;

    return (
      <div>
        <p>gender:</p>
        <label>
          male:
          <input 
            type="radio"
            value="male"
            checked={radioValue === 'male'}
            onChange={this.handleChange}
          />
        </label>
        <label>
          female:
          <input 
            type="radio"
            value="female"
            checked={radioValue === 'female'}
            onChange={this.handleChange}
          />
        </label>
      </div>
    )
  }
}

複選按鈕的示例

import React, { Component } from 'react';

class App extends Component {
  constructor(props) {
    super(props)
    
    this.state = {
      coffee: []
    }
  }

  handleChange = (e) => {
    const { checked, value } = e.target;
    let { coffee } = this.state;

    if (checked && coffee.indexOf(value) === -1) {
      coffee.push(value)
    } else {
      coffee = coffee.filter(i => i !== value)
    }

    this.setState({
      coffee,
    })
  }

  render() {
    const { coffee } = this.state;
    return (
      <div>
        <p>請選擇你最喜歡的咖啡</p>
        <label>
          <input 
            type="checkbox"
            value="Cappuccino"
            checked={coffee.indexOf('Cappuccino') !== -1}
            onChange={this.handleChange}
          />
          Cappuccino
        </label>
        <br />
        <label>
          <input 
            type="checkbox"
            value="CafeMocha"
            checked={coffee.indexOf('CafeMocha') !== -1}
            onChange={this.handleChange}
          />
          CafeMocha
        </label>
      </div>
    )
  }
}

3.Select組件

在HTML的select元素中,存在單選和多選兩種。在JSX語法中,一樣能夠經過設置select標籤的multiple={true}來實現一個多選下拉列表。

class App extends Component {
  constructor(props) {
    super(props)

    this.state = {
      area: ''
    }
  }

  handleChange = (e) => {
    this.setState({
      area: e.target.value
    })
  }

  render() {
    const { area } = this.state;

    return (
      <select value={area} onChange={this.handleChange}>
        <option value='beijing'>北京</option>
        <option value='shangehai'>上海</option>
      </select>
    )
  }
}

select元素設置multiple={true}的示例

class App extends Component {
  constructor(props) {
    super(props)

    this.state = {
      area: ['beijing', 'shanghai']
    }
  }

  handleChange = (e) => {
    const { options } = e.target;
    const area = Object.keys(options)
      .filter(i => options[i].selected === true)
      .map(i => options[i].value);

    this.setState({
      area,
    })
  }

  render () {
    const { area } = this.state;

    return (
      <select multiple={true} value={area} onChange={this.handleChange}>
        <option value="北京">北京</option>
        <option value="上海">上海</option>
      </select>
    )
  }
}

在HTMl的option組件須要一個selected屬性來表示默認選中的列表項,而React的處理方式是經過爲select組件添加value prop來表示選中的option,在必定程度上統一了接口。

實際上,也能夠寫成這種形式,不過開發體驗就會差不少,React也會拋警告。

<select multiple={true} onChange={this.handleChange}>
  <option value="beijing" selected={area.indexOf('beijing') !== -1}>北京</option>
  <option value="shanghai" selected={area.indexOf('shanghai') !== -1}>上海</option>
</select>

受控組件

每當表單的狀態發生變化,都會被寫入到組件的state中,這種組件在React中被稱爲受控組件。在受控組件中,組件渲染出的狀態與它的value或checked prop相對應。React經過這種方式消除了組件的局部狀態,使得應用的整個狀態更加可控。

非受控組件

若是一個表單組件沒有value prop(或checked prop),就能夠稱之爲非受控組件。相應的你可使用defaultValue和defaultChecked prop來表示組件的默認狀態。

class App extends Compoent {
  constructor(props) {
    super(props)

  }

  handleSubmit = (e) => {
    e.preventDefault();

    const { value } = this.refs.name;
    console.log(value)
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input ref="name" type="text" defaultValue="Hangzhou" />
        <button type="submit">submit</button>
      </form>
    )
  }
}

在React中,非受控組件是一種反模式,它的值不受組件自身的state或props控制。一般,須要爲其添加ref prop來訪問渲染後的底層DOM元素。

對比受控組件和非受控組件

受控組件和非受控組件的最大區別是:非受控組件的狀態並不會受應用狀態的控制,應用中也多了局部組件狀態,而受控組件的值來自於組件的state。

1.性能上的問題

受控組件onChange後,調用setState會從新渲染,確實會有一些性能損耗。

2.是否須要事件綁定

受控組件須要爲每一個組件綁定一個change事件,而且定義一個事件處理器來同步表單值和組件的狀態。

儘管如此,在React仍然提倡使用受控組件,由於它可使得應用的整個狀態更加可控。

表單組件的幾個重要屬性

1.狀態屬性

React的form組件提供了幾個重要的屬性,用於展現組件的狀態。
value: 類型爲text的input組件、textarea組件以及select組件都藉助value prop來展現應用的狀態。
checked: 類型爲radio或checkbox的組件藉助值爲boolean類型的checked prop來展現應用的狀態。
selected: 該屬性可做用於select組件下面的option上,React並不建議使用功這種方式,推薦使用value.

2.事件屬性

在狀態屬性發生變化時,會觸發onChange事件屬性。實際上,受控組件中的change事件與HTML DOM中提供的input事件更爲相似。React支持DOM Level3中定義的全部表單事件。

樣式處理

基本樣式設置

React能夠經過設置className prop給html設置class,也能夠經過style prop給組件設置行內樣式。

使用classnames庫

咱們能夠經過classnames庫來設置html的類名

CSS Modules

CSS模塊化的解決方案有不少,但主要有兩類。

  1. Inline Style。這種方案完全拋棄CSS,使用JavaScript或JSON來寫樣式,能給CSS提供JavaScript一樣強大的模塊化能力。但缺點一樣明顯,它幾乎不能利用CSS自己的特性,好比級聯、媒體查詢等,:hover和:active等僞類處理起來比較複雜。另外,這種方案須要依賴框架實現,其中與React相關的有Radium、jsxstyle和react-style
  2. CSS Modules。依舊使用CSS,但使用JavaScript來管理樣式依賴。CSS Modules能最大化地結合現有CSS生態和JavaScript模塊化能力,其API很是簡潔。發佈時依舊編譯出單獨的JavaScript和CSS文件。如今,webpack css-loader內置CSS Modules功能。

1.CSS模塊化遇到了哪些問題

CSS模塊化重要的是解決好如下兩個問題:CSS樣式的導入與導出。靈活按需導入以便複用代碼,導出時要隱藏內部做用域,以避免形成全局污染。Sass、Less、PostCSS等試圖解決CSS編程能力弱的問題,但並無解決模塊化這個問題。React實際開發須要的CSS相關問題有:

  1. 全局污染:CSS使用全局選擇器機制來設置樣式,優勢是方便重寫樣式。缺點是全部的樣式全局生效,樣式可能被錯誤覆蓋。所以產生了很是醜陋的!important,甚至inline !important和複雜的選擇器權重計數表,提升犯錯機率和使用成本。Web Component標準中的Shadow DOM能完全解決這個問題,但它把樣式完全局部化,形成外部沒法重寫樣式,損失了靈活性。
  2. 命名混亂:因爲全局污染的問題,多人協同開發時爲了不樣式衝突,選擇器愈來愈複雜,容易造成不一樣的命名風格,樣式變多後,命名將更加混亂。
  3. 依賴管理不完全:組件應該相互獨立,引入一個組件時,應該只引入它所須要的CSS樣式。如今的作法是除了引入JavaScript,還要再引入它的CSS,並且Sass/Less很難實現對每一個組件都編譯出單獨的CSS,引入全部模塊的CSS又形成浪費。JavaScript的模塊化已經很是成熟,若是能讓JavaScript來管理CSS依賴是很好的解決辦法,而webpack的css-loader提供了這種能力。
  4. 沒法共享變量:複雜組件要使用JavaScript和CSS來共同處理樣式,就會形成有些變量在JavaScript和CSS中冗餘,而預編譯語言不能提供跨JavaScript和CSS共享變量的這種能力。
  5. 代碼壓縮不完全:對與很是長的類名壓縮無能爲力。

2.CSS Modules模塊化方案

CSS Modules內部經過ICSS來解決樣式導入和導出這兩個問題,分別對應:import和:export兩個新增的僞類。

:import("path/to/dep.css") {
  localAlias: keyFromDep;
}

:export {
  exportedKey: exportedValue;
}

但直接使用這兩個關鍵字編程太煩瑣,項目中不多會直接使用它們,咱們須要的是用JavaScript來管理CSS的能力。結合webpack的css-loader,就能夠在CSS中定義樣式,在JavaScript文件中導出。

啓用CSS Modules

css?modules&localIdentName=[name]__[local]-[hash:base64:5]

加上modules即爲啓用,其中localIdentName是設置生成樣式命名規則

下面咱們看看js是怎麼引入CSS的:

/* button相關的全部樣式 */
.normal {}
import styles from './Button.css'

buttonElm.outerHTML = `<button class=${styles.normal}>Submit</button>`

最終生成的HTML是這樣的

<button class="button--normal-abc5436">Processing...</button>

這樣class的名稱基本就是惟一的。
CSS Modules對CSS中的class名都作了處理,使用對象來保存原class和混淆後class的對應關係。經過這些簡單的處理,CSS Modules實現瞭如下幾點:

  1. 全部樣式都是局部化的,解決了命名衝突和全局污染問題
  2. class名生成規則配置靈活,能夠以此來壓縮class名
  3. 只須要引用組件的JavaScript,就能搞定組件全部的JavaScript和CSS
  4. 依然是CSS,學習成本幾乎爲零

樣式默認局部

使用CSS Modules至關於給每一個class名外加了:local,以此來實現樣式的局部化。若是咱們想切換到全局模式,可使用:global包裹

.normal {
  color: green;
}
/* 與上面等價 */
:local(.normal) {
  color: green;
}
/* 定義全局樣式 */
:global(.btn) {
  color: red;
}
/* 定義多個全局樣式 */
:global {
  .link {
    color: green;
  }
  .box {
    color: yellow;
  }
}

使用composes來組合樣式

對於樣式複用,CSS Modules只提供了惟一的方式來處理——composes組合。

/* components/Button.css */
.base { /* 全部通用的樣式 */ }

.normal {
  composes: base;
  /* normal其餘樣式 */
}

此外,使用composes還能夠組合外部文件中的樣式

/* settings.css */
.primary-color {
  color: #f40;
}

/* component/Button.css */
.base { /* 全部通用樣式 */ }

.primary {
  composes: base;
  composes: $primary-color from './settings.css'
}

對於大多數項目,有了composes後,已經再也不須要預編譯處理器了。可是若是想用的話,因爲composes不是標準的CSS語法,編譯會報錯,此時只能使用預處理本身的語法作樣式複用了。

class命名技巧

CSS Modules的命名規範是從BEM擴展而來的。BEM把樣式名分爲3個級別

  1. Block: 對應模塊名,如Dialog
  2. Element: 對應模塊中的節點名 Confirm Button
  3. Modifier: 對應節點相關的狀態,如disabled和highlight

如dialog__confirm-button--highlight。

實現CSS與JavaScript變量共享

:export關鍵字能夠把CSS中的變量輸出到JavaScript中

$primary-color: #f40;

:export {
  primaryColor: $primary-color;
}
// app.js
import style from 'config.scss'

console.log(style.primaryColor);

CSS Modules使用技巧

建議遵循以下原則

  1. 不使用選擇器,只使用class名來定義樣式
  2. 不層疊多個class,只使用一個class把全部樣式定義好
  3. 全部樣式經過composes組合來實現複用
  4. 不嵌套

常見問題
1.若是在一個style文件使用同名class?
雖然編譯後多是隨機碼,但還是同名的。
2.若是在style文件中使用了id選擇器、僞類和標籤選擇器等呢?
這些選擇器不被轉換,原封不動地出如今編譯後的CSS中。也就是CSS Moudles只會轉換class名相關的樣式

CSS Modules結合歷史遺留項目實踐

1.外部如何覆蓋局部樣式

由於沒法預知最終的class名,不能經過通常選擇器覆蓋樣式。咱們能夠給組件關鍵節點加上data-role屬性,而後經過屬性選擇器來覆蓋樣式。

// dialog.js
return (
  <div className={styles.root} data-role="dialog-root"></div>
);
// dialog.css
[data-role="dialog-root"] {
  // override style
}

2.如何與全局樣式共存

修改webpack配置

module: {
  loaders: [{
    test: /\.scss$/,
    exclude: path.resolve(__dirname, 'src/views'),
    loader: 'style!css?modules&localIdentName=[name]__[local]!sass?sourceMap=true',
  }, {
    test: /\.scss$/,
    include: path.resolve(__dirname, 'src/styles'),
    loader: 'style!css!sass?sourceMap=true'
  }]
}
/* src/app.js */
import './styles/app.scss';
import Component from './view/Component'

/* src/views/Component.js */
import './Component.scss'

CSS Modules結合React實踐

import styles from './dialog.css';

class Dialog extends Component {
  render() {
    return (
      <div className={styles.root}></div>
    )
  }
}

若是不想頻繁地輸入styles.**,可使用react-css-modules

組件間通訊

父組件向子組件通訊

父組件能夠經過props向子組件傳遞須要的信息

子組件向父組件通訊

有兩種方法:1.利用回調函數。2.利用自定義事件機制:這種方法更通用,設計組件時考慮加入事件機制每每能夠達到簡化組件API的目的。

在React中,可使用任意一種方法,在簡單場景下使用自定義事件過於複雜,通常利用回調函數。

跨級組件通訊

當須要讓子組件跨級訪問信息時,若像以前那樣向更高級別的組件層層傳遞props,此時代碼顯得不那麼優雅,甚至有些冗餘。在React中,咱們還可使用context來實現跨級父子組件間的通訊。

class ListItem extends Component {
  static contextTypes = {
    color: PropTypes.string,
  }

  render () {
    return (
      <li style={{ background: this.context.color }}></li>
    )
  }
}
class List extends Component {
  static childContextTypes = {
    color: PropTypes.string,
  }

  getChildContext() {
    return {
      color: 'red'
    }
  }
  render() {

  }
}

React官方並不建議大量使用context,由於當組件結構複雜的時候,咱們很難知道context是從哪傳過來的。使用context比較好的場景是真正意義上的全局信息且不會更改,例如界面主題、用戶信息等。整體的原則是若是咱們真的須要它,那麼建議寫成高階組件來實現。

沒有嵌套關係的組件通訊

沒有嵌套關係的,那隻能經過能夠影響全局的一些機制去考慮。以前講的自定義事件機制不失爲一種上佳的方法。

咱們在處理事件過程當中須要注意,在componentDidMount事件中,若是組件掛載完成,再訂閱事件;當組件卸載的時候,在componentWillUnmount事件中取消事件的訂閱。

對於React使用的場景,EventEmitter只須要單例就能夠了

import { EventEmitter } from 'events';

export default new EventEmitter();
import emitter from './events';

emitter.emit('ItenChange', entry)
class App extends Component {
  componentDidMount() {
    this.itemChange = emitter.on('ItemChange', (data) => {
      console.log(data)
    })
  }
  componentWillUnmount() {
    emitter.removeListener(this.itemChange)
  }
}

通常來講,程序中出現多級傳遞或跨級傳遞,那麼要個從新審視一下是否有更合理的方式。Pub/Sub的模式可能也會帶來邏輯關係的混亂。

跨級通訊每每是反模式的,應該儘可能避免僅僅經過例如Pub/Sub實現的設計思路,加入強依賴與約定來進一步梳理流程是更好的方法。(如使用Redux)

組件間抽象

經常有這樣的場景,有一類功能須要被不一樣的組件公用,此時就涉及抽象的話題。咱們重點討論兩種:mixin和高階組件

封裝mixin方法

const mixin = function(obj, mixins) {
  const newObj = obj;
  newObj.prototype = Object.create(obj.prototype);

  for (let prop in mixins) {
    if (mixins.hasOwnProperty(prop)) {
      newObj.prototype[prop] = mixins[prop]
    }
  }
}

const BigMixin = {
  fly: () => {
    console.log('fly');
  }
}

const Big = function() {
  console.log('new big');
}

consg FlyBig = mixin(Big, BigMixin)

const flyBig = new FlyBig();
flyBig.fly(); // => 'fly'

對於廣義的mixin方法,就是用賦值的方式將mixin對象裏的方法都掛載到原對象上,來實現對對象的混入。

看到上述實現,你可能會聯想到underscore庫中的extend或lodash庫中的assign方法,或者說ES6中的Object.assign()方法。MDN上的解釋是把任意多個源對象所擁有的自身可枚舉屬性複製給目標對象,而後返回目標對象。

在React中使用mixin

React在使用createClass構建組件時提供了mixin屬性,好比官方封裝的PureRenderMixin

import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';

React.createClass({
  mixins: [PureRenderMixin],
  render() {}
})

mixins數組也能夠增長多個mixin,其若mixin方法之間有重合,對於普通方法,在控制檯裏會報一個ReactClassInterface的錯誤。對於生命週期方法,會將各個模塊的生命週期方法疊加在一塊兒順序執行。

mixin爲組件作了兩件事:

  1. 工具方法。若是你想共享一些工具類方法,能夠定義它們,直接在各個組件中使用。
  2. 生命週期繼承,props與state合併,mixin也能夠做用在getInitialState的結果上,做state的合併,而props也是這樣合併的。

ES6 Classes與decorator

ES6 classes形式構建組件,它並不支持mixin。decorator語法糖能夠實現class上的mixin。

core-decorators庫爲開發者提供了一些實用的decorator,其中實現了咱們正想要的@mixin

import { getOwnPropertyDescriptors } from './private/utils';

const { defineProperty } = Object;

function handleClass(target, mixins) {
  if (!mixins.length) {
    // throw error;
  }

  for(let i = 0, l = mixins.length; i < l; i ++) {
    const descs = getOwnPropertyDescriptors(mixins[i])

    for (const key in descs) {
      if (!(key in target.prototype)) {
        defineProperty(target.prototype, key, descs[key])
      }
    }
  }
}

export default function mixin(...mixins) {
  if (typeof mixins[0] === 'function') {
    return handleClass(mixins[0], [])
  } else {
    return target => {
      return handleClass(target, mixins)
    }
  }
}

原理也很簡單,它將每個mixin對象的方法都疊加到target對象的原型上以達到mixin的目的。這樣就能夠用@mixin來作多個重用模塊的疊加了。

const PureRender = {
  shouldComponentUpdate() {}
}

const Theme = {
  setTheme() {}
}

@mixin(PureRender, Theme)
class MyComponent extends Component {
  render() {}
}

mixin的邏輯和最先實現的簡單邏輯很類似,以前直接給對象的prototype屬性賦值,但這裏用了getOwnPropertyDescriptor和defineProperty這兩個方法,有什麼區別呢?

這樣實現的好在於definedProperty這個方法,也就是定義和賦值的區別,定義是對已有的定義,賦值是覆蓋已有的定義。前者並不會覆蓋已有方法,但後者會。本質上與官方的mixin方法都很不同,除了定義方法級別不能覆蓋外,還得加上對生命週期方法的繼承,以及對state的合併。

decorator還有做用在方法上的,它能夠控制方法的自有屬性,也能夠做decorator的工廠方法。

mixin的問題

mixin存在不少問題,已經被官方棄用了,由高階組件替代。

1.破壞的原有組件的封裝

咱們知道mixin方法會混入方法,給原有組件帶來新的特性,好比mixin中有一個renderList方法,給咱們帶來了渲染List的能力,但它也可能帶來新的state和props,這意味着組件有一些"不可見"的狀態須要咱們去維護,但咱們在使用的時候並不清楚。此外renderList中的方法會調用組件中方法,但極可能被其餘mixin截獲,帶來不少不可知。

2.不一樣mixin的命名衝突

3.增長複雜性

咱們設計一個組件,引入PopupMixin的mixin,這樣就給組件引進了PopupMixin生命週期方法。當咱們再引入HoverMixin,將有更多的方法被引進。固然咱們能夠進一步抽象出TooltipMixin,將兩個整合在一塊兒,但咱們發現它們都有compomentDidUpdate方法。過一段時間,你會發現它的邏輯已經複雜到難以理解了。

咱們寫React組件時,首先考慮的每每是單一的功能、簡潔的設計和邏輯。當加入功能的時候,能夠繼續控制組件的輸入和輸出。若是說由於複雜性,咱們不斷加入新的狀態,那麼組件會變得很是難維護。

高階組件

高階函數是函數式編程中的一個基本概念,這種函數接受函數做爲輸入,或是輸出一個函數。
高階組件相似高階函數,它接受React組件做爲輸入,輸出一個新的React組件。

高階組件讓咱們的代碼更具備複用性、邏輯性與抽象特性,它能夠對render方法做劫持,也能夠控制props和state。

實現高階組件的方法有兩種:

  1. 屬性代理:經過被包裹的React組件來操做props
  2. 反向繼承:繼承於被包裹的React組件

1.屬性代理

const MyContainer = (WrappedComponent) => {
  class extends Component {
    render() {
      return <WrappedComponent {...this.props} />
    }
  }
}

這樣,咱們就能夠經過高階組件來傳遞props,這種方法即爲屬性代理。
這樣組件就能夠一層層地做爲參數被調用,原始組件就具有了高階組件對它的修飾。保持了單個組件封裝性同時還保留了易用性。固然,也能夠用decorator來轉換

@MyContainer
class MyComponent extends Component {
  render() {}
}

export default MyComponent

上述執行生命週期的過程相似於堆棧調用:
didmount -> HOC didmount -> (HOCs didmount) -> (HOCs will unmount) -> HOC will unmount -> unmount
從功能上,高階組件同樣能夠作到像mixin對組件的控制,包括控制props、經過refs使用引用、抽象state和使用其餘元素包裹WrappedComponent.

1.控制props

咱們能夠讀取、增長、編輯或是移除從WrappedComponent傳進來的props,但須要當心刪除與編輯重要的props。咱們應該儘量對高階組件的props做新的命名以防止混淆。

例如,咱們須要增長一個新的prop:

const MyContainer = (WrappedComponent) => {
  class extends Component {
    render() {
      const newProps = {
        text: newText,
      };
      return <WrappedComponent {...this.props} {...newProps} />;
    }
  }
}

2.經過refs使用引用

const MyContainer = (WrappedComponent) => {
  class extends Component {
    proc(wrappedComponentInstance) {
      wrappedComponentInstance.method();
    }

    render() {
      const props = Object.assign({}, this.props, {
        ref: this.proc.bind(this),
      })
      return <WrappedComponent {...props} />
    }
  }
}

這樣就能夠方便地用於讀取或增長實例的props,並調用實例的方法。
3.抽象state
高階組件能夠將原組件抽象爲展現型組件,分離內部狀態

const MyContainer = (WrappedComponent) => {
  class extends Component {
    constructor(props) {
      super(props)
      this.state = {
        name: ''
      }

      this.onNameChange = this.onNameChange.bind(this)
    }

    onNameChange(event) {
      this.setState({
        name: event.target.value
      })
    }

    render() {
      const newProps = {
        name: {
          value: this.state.name,
          onChange: this.onNameChange,
        }
      }
      return <WrappedComponent {...this.props} {...newProps}>
    }
  }
}

這樣就有效地抽象了一樣的state操做。
4.使用其餘元素包裹WrappedComponent
這既能夠是爲了加樣式,也能夠是爲了佈局

const MyContainer = (WrappedComponent) => {
  class extends Component {
    render() {
      return (
        <div style={{ display: 'block' }}>
          <WrappedComponent {...this.props} />
        </div>
      )
    }
  }
}

反向繼承

const MyContainer = (WrappedComponent) => {
  class extends WrappedComponent{
    render() {
      return super.render()
    }
  }
}

高階組件返回的組件繼承於WrappedComponent,由於被動地繼承了WrappedComponent,全部調用都會反向,這也是這種方法的由來。
由於依賴於繼承的機制,HOC的調用順序和隊列是同樣的
didmount->HOC didmount->(HOCs didmount)->will unmount->HOC will unmount->(HOCs will unmount)

在反向繼承方法中,高階組件可使用WrappedComponent引用,這意味着它可使用WrappedComponent的state、props、生命週期、和render。但它不能保證完整的子組件樹被解析。

它有兩大特色

1.渲染劫持

高階組件能夠控制WrappedComponent的渲染過程。能夠在這個過程當中在任何React元素輸出的結果中讀取;增長、修改。刪除props,或讀取或修改React元素樹,或條件顯示元素樹,又或是用樣式控制包裹元素樹。

若是元素樹中包括了函數類型的React組件,就不能操做組件的子組件。

條件渲染示例

const MyContainer = (WrappedComponent) => {
  class extends WrappedComponent {
    render() {
      if (this.props.loggedIn) {
        return super.render();
      } else {
        return null;
      }
    }
  }
}

對render的輸出結果進行修改

const MyContainer = (WrappedComponent) => {
  class extends WrappedComponent {
    render() {
      const elementsTree = super.render();
      let newProps;
      if (elementsTree && elementsTree.type === 'input') {
        newProps = { value: 'may the force be with you' }
      }
      const props = Object.assign({}, elementsTree.props, newProps);
      const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children);
      return newElementsTree;
    }
  }
}

2.控制state

高階組件能夠讀取、修改或刪除WrappedComponent實例中的state,若是須要的話,也能夠增長state。但這樣作,可能會讓WrappedComponent組件內部狀態變得一團糟。大部分高階組件都應該限制讀取或增長state,尤爲是後者,能夠經過重命名state,以防止混淆

const MyContainer = (WrappedComponent) => {
  class extends WrappedComponent {
    render() {
      return (
        <div>
          <h2>HOC Debugger Component</h2>
          <p>Props</p><pre>{JSON.stringify(this.props, null, 2)}</pre>
          <p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre>
          {super.render()}
        </div>
      )
    }
  }
}

在這個例子中,顯示了WrappedComponent的props和state,方便咱們調試。

組件命名

高階組件失去了原始的diplayName,咱們應該爲高階組件命名

HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`

class HOC extends ... {
  static displayName = `HOC(${getDisplayName(WrappedComponent)})`
}
function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName ||
         WrappedComponent.name || 
         'Component'
}

組件參數

function HOCFactoryFactory(...params) {
  return function HOCFactory(WrappedComponent) {
    return class HOC extends Component {
      render() {
        return <WrappedComponent {...this.props} />
      }
    }
  }
}

組合式組件開發實踐

咱們屢次提到,使用React開發組件時利用props傳遞參數。也就是說,用參數來配置組件時咱們最經常使用的封裝方式。隨着場景發生變化,組件的形態也發生變化,咱們必須不斷增長props去應對變化,此時便會致使props的泛濫,而在拓展中又必須保證組件向下兼容,只增不減,使組件的可維護性下降。

咱們就能夠利用上述高階組件的思想,提出組件組合式開發模式,有效地解決了配置式所存在的一些問題。

1.組件再分離

SelectInput、SearchInput與List三個顆粒度更細的組件能夠組合成功能豐富的組件,而每一個組件能夠是純粹的、木偶式的組件。

2.邏輯再抽象

組件中相同交互邏輯和業務邏輯也應該抽象

// 完成SearchInput與List的交互
const searchDecorator = WrappedComponent => {
  class SearchDecorator extends Component {
    constructor(props) {
      super(props)

      this.handleSearch = this.handleSearch.bind(this)
    }

    handleSearch(keyword) {
      this.setState({
        data: this.props.data,
        keyword,
      })

      this.props.onSearch(keyword)
    }

    render() {
      const { data, keyword } = this.state;
      return (
        <WrappedComponent
          {...this.props}
          data={data}
          keyword={keyword}
          onSearch={this.handleSearch}
        />
      )
    }
  }

  return SearchDecorator;
}

// 完成List數據請求
const asyncSelectDecorator = WrappedComponent => {
  class AsyncSelectDecorator extends Component {
    componentDidMount() {
      const { url, params } = this.props;

      fetch(url, { params }).then(data => {
        this.setState({
          data
        })
      })
    }

    render() {
      return (
        <WrappedComponent
          {...this.props}
          data={this.state.data}
        />
      )
    }
  }

  return AsyncSelectDecorator;
}

最後,咱們用compose將高階組件層層包裹,將頁面和邏輯完美結合在一塊兒

const FinalSelector = compose(asyncSelectDecorator, searchDecorator, selectedItemDecorator)(Selector)

組件性能優化

從React的渲染過程當中,如何防止沒必要要的渲染是最須要去解決的問題。
針對這個問題,React官方提供了一個便捷的方法來解決,那就是PureRender

純函數

純函數由三大原則構成

1.給定相同的輸入,它老是返回相同的輸出

2.過程沒有反作用(咱們不能改變外部狀態)

3.沒有額外的狀態依賴

純函數也很是方便進行方法級別的測試以及重構,可讓程序具備良好的擴展性及適應性。

PureRender

PureRender中的Pure指的就是組件知足純函數的條件,即組件的渲染是被相同的props和state渲染進而獲得相同的結果。

1.PureRender本質

官方在早期爲開發者提供了一個react-addons-pure-render-mixin的插件,其原理爲從新實現了shouldComponentUpdate生命週期方法,讓當前傳入的props和state與以前的做淺比較,若是返回false,那麼組件就不會執行render方法。(若作深度比較,也很耗性能)

PuerRender源碼中,只對新舊props做了淺比較,如下是shallowEqual的示例代碼

function shallowEqual(obj, newObj) {
  if (obj === newObj) {
    return true;
  }

  const objKeys = Object.keys(obj);
  const newObjKeys = Object.keys(newObj);
  if (objKeys.length !== newObjKeysl.length) {
    return false;
  }

  return objKeys.every(key => {
    return newObj[key] === obj[key];
  })
}

3.優化PureRender

若是說props或state中有如下幾種類型的狀況,那麼不管如何,它都會觸發PureRender爲true。

3.1直接爲props設置對象或數組

引用的地址會改變

<Account style={{ color: 'black' }} />

避免這個問題

const defaultStyle = {};
<Account style={{ this.props.style || defaultStyle }} />

3.2設置props方法並經過事件綁定在元素上

class MyInput extends Component {
  constructor(props) {
    super(props)

    this.handleChange = this.handleChange.bind(this)
  }

  handleChange(e) {
    this.props.update(e.target.value)
  }

  render() {
    return <input onChange={this.handleChange} />
  }

}

3.3設置子組件

對於設置了子組件的React組件,在調用shouldComponentUpdate時,均返回true。

class NameItem extends Component {
  render() {
    return (
      <Item>
        <span>Arcthur</span>
      </Item>
    )
  }
}
<Item 
  children={React.createElement('span', {}, 'Arcthur')}
/>

怎麼避免重複渲染覆蓋呢?咱們在NameItem設置PureRender,也就是提到父級來判斷。

Immutable

在傳遞數據時能夠直接使用Immutable Data來進一步提高組件的渲染性能。

JavaScript中的對象通常是可變的,由於使用了引用賦值,新的對象簡單地引用了原始對象,改變新的對象將影響到原始對象。

使用淺拷貝或深拷貝能夠避免修改,但這樣作又形成了內存和CPU的浪費。

1.Immutable Data一旦被建立,就不能再更改數據,對Immutable對象進行修改,添加或刪除操做,都會返回一個新的Immutable對象。Immutable實現的原理是持久化的數據結構,也就是使用舊數據建立新數據,要保存舊數據同時可用且不變,同時爲了不深拷貝把全部節點複製一遍帶來的性能損耗,Immutable使用告終構共享,即若是對象樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其餘節點則進行共享。

Immutable的優勢

1.下降了可變帶來的複雜度。

可變數據耦合了time和value的概念,形成了數據很難被回溯

function touchAndLog (touchFn) {
  let data = { key: '1' }
  touchFn(data);
  console.log(data.key)
}

若data是不可變的,能打印的結果是什麼。

2.節省內存

Immutable使用的結構共享盡可能複用內存。沒有引用的對象會被垃圾回收。

import { map } from 'immutable';

let a = Map({
  select: '1',
  filter: Map({ name: '2' }),
});

let b = a.set('select', 'people');

a === b
a.get('filter') === b.get('filter')// true

3.撤銷/重作,複製/粘貼,甚至時間旅行這些功能都很容易實現。

由於每次數據都是不同的,那麼只要把這些數據放到一個數組裏存儲起來,就能自由回退。

4.併發安全

數據天生是不可變的,後端經常使用的併發鎖就不須要了,然而如今並無用,由於通常JavaScript是單線程運行的。

5.擁抱函數式編程

Immutable自己就是函數式編程中的概念,只要輸入一致,輸出必然一致。

使用Immutable的缺點

容易與原生對象混淆是使用Immutale的過程當中遇到的最大問題。
下面給出了一些辦法

1.使用FlowType或TypeScript靜態類型檢查工具

2.約定變量命名規則,如Immutable類型對象以$$開頭

3.使用Immutable.fromJS而不是Immutable.Map或Immutable.List來建立對象,這樣能夠避免Immutable對象和原生對象間的混用

Immutable.js

兩個Immutable對象可使用===來比較,這樣是直接比較內存地址,其性能最好。可是即便兩個對象的值是同樣的,也會返回false。

Immutable提供了Immutable.is來做"值比較",Immutable比較的是兩個對象的hasCode或valueOf,因爲Immutable內部使用了trie數據結構來存儲,只要兩個對象的hasCode相等,值就是同樣的。這樣的算法避免了深度遍歷比較,所以性能很是好。

Immutable與cursor

這裏的cursor和數據庫中的遊標是徹底不一樣的概念。因爲Immutable數據通常嵌套很是深,因此爲了便於訪問深層數據,cursor提供了能夠直接訪問這個深層數據的引用:

let data = Immutable.fromJS({ a: { b: { c: 1 } } });
let cursor = Cursor.from(data, ['a', 'b'], newData => {
  // 當cursor或其子cursor執行更新時調用
  console.log(newData)
})

// 1
cursor.get('c');
cursor = cursor.update('c', x => x + 1)
// 2
cursor.get('c');

Immutable與PureRender

React作性能優化時最經常使用的就是shouldComponentUpdate,使用深拷貝和深比較是很是昂貴的選擇。而使使用Immutable.js,===和is是高效地判斷數據是否變化的方法。

import { is } from 'immutable'

shouldComponentUpdate(nextProps, nextState) {
  const thisProps = this.props || {};
  const thisState = this.state || {};

  if (Object.keys(thisProps).length !== Object.keys(nextProps).length || Object.keys(thisState).length !== Object.keys(nextState).length)  {
    return true;
  }

  for (const key in nextProps) {
    if (nextProps.hasOwnProperty(key) && !is(thisProps[key], nextProps[key])) {
      return true;
    }
  }

  for (const key in nextState) {
    if (nextState.hasOwnProperty(key) && !is(thisState[key], nextState[key])) {
      return true;
    }
  }
}

Immutable與setState

React建議把this.state看成不可變的,所以修改前須要作一個深拷貝

import _ from 'lodash';

class App extends Component {
  this.state = {
    data: { time: 0 }
  }

  handleAdd() {
    let data = _.cloneDeep(this.state.data);
    data.time = data.time + 1;
    this.setState({ data });
  }
}

使用Immutable後,操做變得很簡單

import { Map } from 'immutable';

class App extends Component {
  this.state = {
    data: Map({ time: 0 })
  }

  handleAdd() {
    this.setState(({ data }) => {
      data: data.update('times', v => v + 1)
    })
  }
}

Immutable能夠給應用帶來極大的性能提高。

key

若是每個組件是一個數組或迭代器的話,那麼必須有一個惟一的key prop。它是用來標識當前項的惟一性的。若是使用index,它至關於一個隨機鍵,不管有沒有相同的項,更新都會渲染。若是key相同,react會拋警告,且只會渲染第一個key。

如有兩個子組件須要渲染,能夠用插件createFragment包裹來解決。

react-addons-perf

react-addons-perf經過Perf.start和Perf.stop兩個API設置開始和結束的狀態來做分析,它會把各組件渲染的各階段的時間統計出來,而後打印出一張表格。

參考

《深刻React技術棧》

前端閱讀室

相關文章
相關標籤/搜索