Skip to content

Commit

Permalink
避免在 render 裡產生新的物件
Browse files Browse the repository at this point in the history
參考資料
https://medium.com/@esamatti/react-js-pure-render-performance-anti-pattern-fb88c101332f#.f88zi8dyb

簡單來說,render 會常常被呼叫,所以一直產生新物件的話,你的系統會花很多
時間跑 GC,造成效能低落。bind 基本上是在原來的函式外包再包一層,所以是產
生新的物件。

一般常見的解決方法是在 constructor 裡把 callback 重新綁定

constructor(...) {
    this.handler = this.handler.bind(this);
}

這樣的寫法我覺得太麻煩,語意也不清楚,所以用 decorator 來處理。這裡有一
個要注意的:用 decorator 魔改過的 constructor 要記得設定 name 屬性。我非
常建議讀者自己試著註解掉 types.tsx 的 Bind 函式裡的
Object.defineProperty 之後,再修改測試碼用 wrapper.debug() 看看 render
出來的結果是什麼。

另外在 CurrencySelector.render 裡,原本是每次呼叫的時候動態產生所有的
option 元素,當然也是產生了新物件。既然 T 是固定的,所以在 constructor
裡直接產生就好。

而在 OrderList 裡,tbody 裡的 tr 是完全不固定的,所以只能每次動態產生。
  • Loading branch information
Ronmi committed Jul 26, 2016
1 parent a4ff195 commit b2183be
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 22 deletions.
20 changes: 14 additions & 6 deletions ui/src/App.tsx
@@ -1,7 +1,7 @@
/// <reference path="../typings/globals/es6-shim/index.d.ts" />

import * as React from "react";
import { OrderData, translate } from "./types";
import { OrderData, translate, Bind } from "./types";
import AuthForm from "./components/AuthForm";
import OrderForm from "./components/OrderForm";
import CurrencySelector from "./components/CurrencySelector";
Expand All @@ -23,6 +23,13 @@ interface State {
data?: OrderData[];
}

@Bind(
"submitPincode",
"submitOrder",
"codeSelected",
"handlePinFormatError",
"handleOrderFormatError"
)
export default class App extends React.Component<Props, State> {
constructor(props?: any, context?: any) {
super(props, context);
Expand All @@ -35,6 +42,7 @@ export default class App extends React.Component<Props, State> {

// custom event handlers, returns promise so we can test on it
submitPincode(pin: string): Promise<void> {
console.log(this);
return new Promise<void>((res, rej) => {
this.props.api.Auth(pin).then(
() => {
Expand Down Expand Up @@ -117,20 +125,20 @@ export default class App extends React.Component<Props, State> {
return (
<div>
<AuthForm
submitPincode={this.submitPincode.bind(this)}
formatError={this.handlePinFormatError.bind(this)} />
submitPincode={this.submitPincode}
formatError={this.handlePinFormatError} />
</div>
);
}

return (
<div>
<OrderForm
submitOrder={this.submitOrder.bind(this)}
formatError={this.handleOrderFormatError.bind(this)} />
submitOrder={this.submitOrder}
formatError={this.handleOrderFormatError} />
<div className="list-type">
<CurrencySelector
codeSelected={this.codeSelected.bind(this)}
codeSelected={this.codeSelected}
defaultLabel="全部"
defaultValue="ALL" />
</div>
Expand Down
6 changes: 4 additions & 2 deletions ui/src/components/AuthForm.tsx
@@ -1,4 +1,5 @@
import * as React from "react";
import { Bind } from "../types";

interface Props {
submitPincode: (pin: string) => void; // callback
Expand All @@ -9,6 +10,7 @@ interface State {
pin: string;
}

@Bind("handleSubmit", "handleChange")
export default class AuthForm extends React.Component<Props, State> {
constructor(props?: Props, context?: any) {
super(props, context);
Expand All @@ -17,12 +19,12 @@ export default class AuthForm extends React.Component<Props, State> {

render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<form onSubmit={this.handleSubmit}>
<fieldset>
<legend>使用者認證</legend>
<label htmlFor="pin">
<span>PIN</span>
<input name="pin" type="text" onChange={this.handleChange.bind(this)} placeholder="6 碼數字" />
<input name="pin" type="text" onChange={this.handleChange} placeholder="6 碼數字" />
</label>
<button type="submit">送出</button>
</fieldset>
Expand Down
24 changes: 16 additions & 8 deletions ui/src/components/CurrencySelector.tsx
@@ -1,13 +1,26 @@
import * as React from "react";
import { T } from "../types";
import { T, Bind } from "../types";

interface Props {
codeSelected: (code: string) => void;
defaultLabel?: string;
defaultValue?: string
}

@Bind("handleChange")
export default class CurrencySelector extends React.Component<Props, {}> {
private nodes: JSX.Element[];

constructor(props: Props, context?: any) {
super(props, context);

let nodes = [] as JSX.Element[];
for (let code in T) {
nodes[nodes.length] = <option value={code} key={code}>{T[code]}</option>;
}
this.nodes = nodes;
}

handleChange(e: Event) {
this.props.codeSelected((e.target as HTMLSelectElement).value);
}
Expand All @@ -25,15 +38,10 @@ export default class CurrencySelector extends React.Component<Props, {}> {
}

render() {
let nodes = [] as JSX.Element[];
for (let code in T) {
nodes[nodes.length] = <option value={code} key={code}>{T[code]}</option>;
}

return (
<select name="currency" onChange={this.handleChange.bind(this)} value={this.props.defaultValue}>
<select name="currency" onChange={this.handleChange} value={this.props.defaultValue}>
{this.renderDefaultLabel()}
{nodes}
{this.nodes}
</select>
);
}
Expand Down
19 changes: 13 additions & 6 deletions ui/src/components/OrderForm.tsx
@@ -1,5 +1,5 @@
import * as React from "react";
import { OrderData, translate } from "../types";
import { OrderData, translate, Bind } from "../types";
import CurrencySelector from "./CurrencySelector";

interface State {
Expand All @@ -14,6 +14,13 @@ interface Props {
formatError?: () => void;
}

@Bind(
"setWhen",
"setLocal",
"setForeign",
"setCode",
"handleSubmit"
)
export default class OrderForm extends React.Component<Props, State> {
constructor(props?: Props, context?: any) {
super(props, context);
Expand All @@ -40,25 +47,25 @@ export default class OrderForm extends React.Component<Props, State> {

render() {
return (
<form onSubmit={this.handleSubmit.bind(this)}>
<form onSubmit={this.handleSubmit}>
<fieldset>
<legend>新增</legend>
<label htmlFor="date">
<span>交易時間</span>
<input name="when" type="datetime" onChange={this.setWhen.bind(this)} placeholder="年/月/日 時:分:秒"/>
<input name="when" type="datetime" onChange={this.setWhen} placeholder="年/月/日 時:分:秒"/>
</label>
<label htmlFor="local">
<span>成本(買入為負)</span>
<input name="local" type="number" step="0.01" onChange={this.setLocal.bind(this)} />
<input name="local" type="number" step="0.01" onChange={this.setLocal} />
</label>
<div className="foreign">
<label htmlFor="foreign">
<span>金額(買入為正)</span>
<input name="foreign" type="number" step="0.01" onChange={this.setForeign.bind(this)} />
<input name="foreign" type="number" step="0.01" onChange={this.setForeign} />
</label>
<label htmlFor="currency">
<span>幣別</span>
<CurrencySelector codeSelected={this.setCode.bind(this)} defaultLabel="請選擇" />
<CurrencySelector codeSelected={this.setCode} defaultLabel="請選擇" />
</label>
</div>
<button type="submit">送出</button>
Expand Down
27 changes: 27 additions & 0 deletions ui/src/types.ts
Expand Up @@ -43,3 +43,30 @@ export function formatNumber(val: number, size: number): string {

return str.substr(0, l - size) + "." + str.substr(l - size, size);
}

// Bind decorator
export function Bind(...keys: string[]): (c: any) => any {
let make = function(original: any, args: any[]) {
let cons: any = function() {
return original.apply(this, args);
};
cons.prototype = original.prototype;
return new cons();
};

return function(c: any): any {
let name = c.name;
let ret: any = function(...args: any[]): any {
let obj = make(c, args);

for (let key of keys) {
obj[key] = obj[key].bind(obj);
}

return obj;
};

Object.defineProperty(ret, "name", {value: name});
return ret;
};
}

0 comments on commit b2183be

Please sign in to comment.