博客 / 詳情

返回

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

書籍完整目錄

4.2 react patterns

  • 修改 Props

    • Immutable data representation

  • 確定性

    • 在 getInitialState 中使用 props

    • 私有狀態和全局事件

    • render 包含 side effects

    • jQuery 修改 DOM

    • 使用無狀態組件

  • 內存管理

    • 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 會影響應用的確定性。

參考鏈接

  • https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750#.hvbsii4zd

  • http://www.zhubert.com/blog/2016/02/05/react-composability-patterns/

  • https://medium.com/@learnreact/context-f932a9abab0e#.wn00ktlde

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.