精益 React 學習指南 (Lean React)- 4.2 react patterns

書籍完整目錄html

4.2 react patterns

  • 修改 Propsreact

    • Immutable data representationgit

  • 肯定性es6

    • 在 getInitialState 中使用 propsgithub

    • 私有狀態和全局事件redux

    • render 包含 side effectssegmentfault

    • jQuery 修改 DOM設計模式

    • 使用無狀態組件框架

  • 內存管理ide

    • componentWillUnmount 取消訂閱事件

    • 判斷 isMounted

  • 上層設計

    • 使用 container component

    • 使用 Composition 替代 mixins

    • Composability - Presenter Pattern

    • Composability - Decorator Pattern

    • Context 數據傳遞

4.2.1 關於

React 的框架設計是趨於函數式的,其中最主要的兩點也是爲何會選擇 React 的兩點:

  1. 單向性:數據的流動是單向的

  2. 肯定性:React(storeData) = view 相同數據老是渲染出相同的 view

這兩點便是特性也是設計 React 應用的基本原則,圍繞這兩個原則社區裏邊出現了一些 React 設計模式,即有好的設計模式也有應該要避免的反模式,理解這些設計模式可以幫助咱們寫出更優質的 React 應用,本節將圍繞 單向性、肯定性、內存管理、上層設計 來討論這些設計模式。

anti 表示反模式,good 表示好模式

4.2.2 單向性

數據的流動是單向的

修改 Props (anti)

描述: 組件任何地方修改 props 的值

解釋:

React 的數據流動是單向性的,流動的方式是經過 props 傳遞到組件中,而在 Javascript 中對象是經過引用傳遞的,修改 props 等於直接修改了 store 中的數據,致使破壞數據的單向流動特性

使用不可變數據 (good)

描述: store data 使用不可變數據

解釋: Javascript 對象的特性是能夠任意修改,而這個特性很容易破壞數據的單向性,由於人工沒法永遠確保數據沒有被修改過,惟一的作法是使用不可變數據,用代碼邏輯確保數據不能被任意修改,後面會有一個完整的小節介紹不可變數據在 React 中的應用

4.2.3 肯定性

React(storeData) = view 相同數據老是渲染出相同的 view

在 getInitialState 中使用 props (anti)

描述: getInitialState 經過 props 來生成 state 數據

解釋:

官方文檔 https://facebook.github.io/react/tips/props-in-getInitialState-as-anti-pattern.html

在 getInitialState 中經過 props 來計算 state 破壞了肯定性原則,「source of truth」 應該只是來自於一個地方,經過計算 state 事後增長了 truth source。這種作法的另一個壞處是在組件更新的時候,還須要計算從新計算這部分 state。

舉例:

var MessageBox = React.createClass({
  getInitialState: function() {
    return {nameWithQualifier: 'Mr. ' + this.props.name};
  },

  render: function() {
    return <div>{this.state.nameWithQualifier}</div>;
  }
});

ReactDOM.render(<MessageBox name="Rogers"/>, mountNode);

優化方式:

var MessageBox = React.createClass({
  render: function() {
    return <div>{'Mr. ' + this.props.name}</div>;
  }
});

ReactDOM.render(<MessageBox name="Rogers"/>, mountNode);

須要注意的是如下這種作法並不會影響肯定性

var Counter = React.createClass({
  getInitialState: function() {
    // naming it initialX clearly indicates that the only purpose
    // of the passed down prop is to initialize something internally
    return {count: this.props.initialCount};
  },

  handleClick: function() {
    this.setState({count: this.state.count + 1});
  },

  render: function() {
    return <div onClick={this.handleClick}>{this.state.count}</div>;
  }
});

ReactDOM.render(<Counter initialCount={7}/>, mountNode);

私有狀態和全局事件 (anti)

描述: 在組件中定義私有的狀態或者使用全局事件

介紹: 組件中定義了私有狀態和全局事件事後,組件的渲染可能會出現不一致,由於全局事件和私有狀態均可以控制組件的狀態,這樣外部使用組件沒法保證組件的渲染結果,影響了組件的肯定性。另一點是組件應該儘可能保證獨立性,避免和外部的耦合,使用全局事件形成了和外部事件的耦合。

render 函數包含 side effects (anti)

side effect 解釋: https://en.wikipedia.org/wiki/Side_effect_(computer_science)

描述: render 函數包含一些 side effects 的代碼邏輯,這些邏輯包括如

  1. 修改 state 數據

  2. 修改 props 數據

  3. 修改全局變量

  4. 調用其餘致使 side effect 的函數

解釋: render 函數若是包含了 side effect ,渲染的結果再也不可信,因此確保 render 函數爲純函數

jQuery 修改 DOM (anti)

描述: 使用外部 DOM 框架修改或刪除了 DOM 節點、屬性、樣式
解釋: React 中 DOM 的結構和屬性都是由渲染函數肯定的,若是使用了 Jquery 修改 DOM,那麼可能形成衝突,視圖的修改源頭增長,直接影響組件的肯定性

使用無狀態組件 (good)

描述: 優先使用無狀態組件
解釋: 無狀態組件更符合函數式的特性,若是組件不須要額外的控制,只是渲染結構,那麼應該優先選擇無狀態組件

4.2.4 內存管理

componentWillUnmount 取消訂閱事件 (good)

描述: 若是組件須要註冊訂閱事件,能夠在 componentDidMount 中註冊,且必須在 ComponentWillUnmount 中取消訂閱
解釋: 在組件 unmount 後若是沒有取消訂閱事件,訂閱事件可能仍然擁有組件實例的引用,這樣第一是組件內存沒法釋放,第二是引發沒必要要的錯誤

判斷 isMounted (anti)

描述: 在組件中使用 isMounted 方法判斷組件是否未被註銷
解釋:

React 中在一個組件 ummount 事後使用 setState 會出現warning提示(一般出如今一些事件註冊回調函數中) ,避免 warning 的解決辦法是:

if(this.isMounted()) { // This is bad.
  this.setState({...});
}

但這是個掩耳盜鈴的作法,由於若是出現了錯誤提示就表示在組件 unmount 的時候還有組件的引用,這個時候應該是已經致使了內存溢出。因此解決錯誤的正確方法是在 componentWillUnmount 函數中取消監聽:

class MyComponent extends React.Component {
  componentDidMount() {
    mydatastore.subscribe(this);
  }
  render() {
    ...
  }
  componentWillUnmount() {
    mydatastore.unsubscribe(this);
  }
}

4.2.5 上層設計

使用 container component (good)

描述: 將 React 組件分爲兩類 container 、normal ,container 組件負責獲取狀態數據,而後傳遞給與之對應的 normal component,對應表示兩個組件的名稱對應,舉例:

TodoListContainer => TodoList
FooterContainer => Footer

解釋: 參看 redux 設計中的 container 組件,container 組件是 smart 組件,normal 組件是 dummy 組件,這樣的責任分離讓 normal 組件更加獨立,不須要知道狀態數據。明確的職責分配也增長了應用的肯定性(明確只有 container 組件可以知道狀態數據,且是對應部分的數據)。

使用 Composition 替代 mixins (good)

描述: 使用組件的組合的方式(高階組件)替代 mixins 實現爲組件增長附加功能
解釋:

mixins 的設計主要目的是給組件提供插件機制,大多數狀況使用 mixin 是爲了給組件增長額外的狀態。可是使用 mixins 會帶來一些額外的壞處:

  1. mixins 一般須要依賴組件定義特定的方法,如 getSomeMixinState ,而這個是隱式的約束

  2. 多個 mixins 可能會致使衝突

  3. mixins 一般增長了額外的狀態數據,而 react 的設計應該是要避免過多的內部狀態

  4. mixins 可能會影響 shouldComponentUpdate 的邏輯, mixins 作了不少數據合併的邏輯

另一點是在新版本的 React 中,mixins 將會是廢棄的 feature,在 es6 class 定義組件也不會支持 mixins。

舉個例子,一個訂閱 fluxstore 的 mixin 爲:

function StoreMixin(store) {
  var Mixin = {
    getInitialState() {
      return this.getStateFromStore(this.props);
    },
    componentDidMount() {
      store.addChangeListener(this.handleStoreChanged)
      this.setState(this.getStateFromStore(this.props));
    },
    componentWillUnmount() {
      store.removeChangeListener(this.handleStoreChanged)
    },
    handleStoreChanged() {
      if (this.isMounted()) {
        this.setState(this.getStateFromStore(this.props));
      }
    }
  };
  return Mixin;
}

使用

const TodolistContainer = React.createClass({
  mixins: [StoreMixin(AppStore)],
  getStateFromStore(props) {
    return {
      todos: AppStore.get('todos');
    }
  }
})

轉換爲組件的組合方式爲:

function connectToStores(Component, store, getStateFromStore) {
  const StoreConnection = React.createClass({
    getInitialState() {
      return getStateFromStore(this.props);
    },
    componentDidMount() {
        store.addChangeListener(this.handleStoreChanged)
    },
    componentWillUnmount() {
        store.removeChangeListener(this.handleStoreChanged)
    },
    handleStoreChanged() {
      if (this.isMounted()) {
        this.setState(getStateFromStore(this.props));
      }
    },
    render() {
      return <Component {...this.props} {...this.state} />;
    }
  });
  return StoreConnection;
};

使用方式:

class Todolist extends React.Component {
    render() {
        // ....
    }
}
TodolistContainer = connectToStore(Todolist, AppStore, props => {
    todos: AppStore.get('todos')
})

Presenter Pattern

描述: 利用 children 能夠做爲函數的特性,將數據獲取和數據表現分離成爲兩個不一樣的組件

以下例子:

class DataGetter extends React.Component {
  render() {
    const { children } = this.props
    const data = [ 1,2,3,4,5 ]
    return children(data)
  }
}

class DataPresenter extends React.Component {
  render() {
    return (
      <DataGetter>
        {data =>
          <ul>
            {data.map((datum) => (
              <li key={datum}>{datum}</li>
            ))}
          </ul>
        }
      </DataGetter>
    )
  }
}

const App = React.createClass({
  render() {
    return (
      <DataPresenter />
    )
  }
})

解釋: 將數據獲取和數據展示分離,同時利用組件的 children 能夠做爲函數的特性,讓數據獲取和數據展示均可以做爲組件使用

Decorator Pattern

描述: 父組件經過 cloneElement 方法給子組件添加方法和屬性

cloneElement 方法:

ReactElement cloneElement(
  ReactElement element,
  [object props],
  [children ...]
)

以下例子:

const CleverParent = React.createClass({
  render() {
    const children = React.Children.map(this.props.children, (child) => {
      return React.cloneElement(child, {
        // 新增 onClick 屬性
        onClick: () => alert(JSON.stringify(child.props, 0, 2))
      })
    })
    return <div>{children}</div>
  }
})

const SimpleChild = React.createClass({
  render() {
    return (
      <div onClick={this.props.onClick}>
        {this.props.children}
      </div>
    )
  }
})

const App = React.createClass({
  render() {
    return (
      <CleverParent>
        <SimpleChild>1</SimpleChild>
        <SimpleChild>2</SimpleChild>
      </CleverParent>
    )
  }
})

解釋: 經過這種設計模式,能夠應用到一些自定義的組件設計,提供更簡潔的 API 給第三方使用,如 facebook 的 FixedDataTable 也是應用了這種設計模式

Context 數據傳遞

描述: 經過 Context 可讓全部組件共享相同的上下文,避免數據的逐級傳遞, Context 是大多數 flux 庫共享 store 的基本方法。

使用方法:

/**
 * 初始化定義 Context 的組件
 */
class Chan extends React.Component {
  getChildContext() {
    return {
      environment: "grandma's house"
    }
  }
}

// 設置 context 類型
Chan.childContextTypes = {
  environment: React.PropTypes.string
};

/**
 * 子組件獲取 context 
 */
class ChildChan extends React.Component {
  render() {
    const ev = this.context.environment;
  }
}
/**
 * 須要設置 contextTypes 才能獲取
 */
ChildChan.contextTypes = {
  environment: React.PropTypes.string
};

解釋: 一般狀況下 Context 是爲基礎組件提供的功能,通常狀況應該避免使用,不然濫用 Context 會影響應用的肯定性。

參考連接

相關文章
相關標籤/搜索