From b2183bedab61f533c717df7c2fde2361da4362ea Mon Sep 17 00:00:00 2001 From: Ronmi Ren Date: Tue, 26 Jul 2016 16:52:38 +0800 Subject: [PATCH] =?UTF-8?q?=E9=81=BF=E5=85=8D=E5=9C=A8=20render=20?= =?UTF-8?q?=E8=A3=A1=E7=94=A2=E7=94=9F=E6=96=B0=E7=9A=84=E7=89=A9=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 參考資料 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 是完全不固定的,所以只能每次動態產生。 --- ui/src/App.tsx | 20 +++++++++++++------ ui/src/components/AuthForm.tsx | 6 ++++-- ui/src/components/CurrencySelector.tsx | 24 +++++++++++++++-------- ui/src/components/OrderForm.tsx | 19 ++++++++++++------ ui/src/types.ts | 27 ++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 22 deletions(-) diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 7931bad..56e4580 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,7 +1,7 @@ /// 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"; @@ -23,6 +23,13 @@ interface State { data?: OrderData[]; } +@Bind( + "submitPincode", + "submitOrder", + "codeSelected", + "handlePinFormatError", + "handleOrderFormatError" +) export default class App extends React.Component { constructor(props?: any, context?: any) { super(props, context); @@ -35,6 +42,7 @@ export default class App extends React.Component { // custom event handlers, returns promise so we can test on it submitPincode(pin: string): Promise { + console.log(this); return new Promise((res, rej) => { this.props.api.Auth(pin).then( () => { @@ -117,8 +125,8 @@ export default class App extends React.Component { return (
+ submitPincode={this.submitPincode} + formatError={this.handlePinFormatError} />
); } @@ -126,11 +134,11 @@ export default class App extends React.Component { return (
+ submitOrder={this.submitOrder} + formatError={this.handleOrderFormatError} />
diff --git a/ui/src/components/AuthForm.tsx b/ui/src/components/AuthForm.tsx index 5cd50d3..14fc391 100644 --- a/ui/src/components/AuthForm.tsx +++ b/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 @@ -9,6 +10,7 @@ interface State { pin: string; } +@Bind("handleSubmit", "handleChange") export default class AuthForm extends React.Component { constructor(props?: Props, context?: any) { super(props, context); @@ -17,12 +19,12 @@ export default class AuthForm extends React.Component { render() { return ( -
+
使用者認證
diff --git a/ui/src/components/CurrencySelector.tsx b/ui/src/components/CurrencySelector.tsx index 6e67b9f..88ef240 100644 --- a/ui/src/components/CurrencySelector.tsx +++ b/ui/src/components/CurrencySelector.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import { T } from "../types"; +import { T, Bind } from "../types"; interface Props { codeSelected: (code: string) => void; @@ -7,7 +7,20 @@ interface Props { defaultValue?: string } +@Bind("handleChange") export default class CurrencySelector extends React.Component { + 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] = ; + } + this.nodes = nodes; + } + handleChange(e: Event) { this.props.codeSelected((e.target as HTMLSelectElement).value); } @@ -25,15 +38,10 @@ export default class CurrencySelector extends React.Component { } render() { - let nodes = [] as JSX.Element[]; - for (let code in T) { - nodes[nodes.length] = ; - } - return ( - {this.renderDefaultLabel()} - {nodes} + {this.nodes} ); } diff --git a/ui/src/components/OrderForm.tsx b/ui/src/components/OrderForm.tsx index cec9115..1300057 100644 --- a/ui/src/components/OrderForm.tsx +++ b/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 { @@ -14,6 +14,13 @@ interface Props { formatError?: () => void; } +@Bind( + "setWhen", + "setLocal", + "setForeign", + "setCode", + "handleSubmit" +) export default class OrderForm extends React.Component { constructor(props?: Props, context?: any) { super(props, context); @@ -40,25 +47,25 @@ export default class OrderForm extends React.Component { render() { return ( - +
新增
diff --git a/ui/src/types.ts b/ui/src/types.ts index 5820b73..4569189 100644 --- a/ui/src/types.ts +++ b/ui/src/types.ts @@ -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; + }; +}