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

陳學家_6174發表於2016-06-30

書籍完整目錄

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

參考連結

相關文章