Curry,Decorator 與 HOC
06 Feb 2019從 curry -> decorator -> HOC 的漫漫長路。
Curry
天冷吃咖哩可有效去寒
可惜本文要聊的 curry 不是吃的,更不是打籃球的。
…
curried function 是指函式一次接受一個參數,利用 closure(閉包)特性,將它們存放在另一個函式中並回傳,直到參數蒐集完畢才做計算,目的是將大問題切割成小問題,以便逐步解決。
範例 1
如下,將兩數相乘切分成兩個步驟來傳入參數,首先使用一個函數 multiply 回傳記住第一個傳入了參數 5 的函數,並指定給變數 multiplyTwoNumbers;再來,從 multiplyTwoNumbers 傳入第二個參數 4,並將結果指定給變數 result,印出 result 而得到 20。
const multiply = (x) => {
return (y) => {
return x * y;
};
};
const multiplyTwoNumbers = multiply(5);
const result = multiplyTwoNumbers(4);
console.log(result); // 20
也可以這麼寫
multiply(5)(4); // 20
此時 x 為 5,y 為 4,得到結果 20。
範例 2
改寫上例,將參數蒐集器(arguments collector)與兩數相乘的函式(multiply)獨立出來。
// arguments collector
const curry = (fn) => (...args) => {
return fn.bind(null, ...args);
};
// multiply two numbers
const multiply = (x, y) => {
return x * y;
};
curried 是個 curried function。
const curried = curry(multiply);
curried(3, 4); // (x, y) => { return x * y; }
此時 x 為 3,y 為 4,得到結果 12。
curried(3, 4)(); // 12
curried(3)(4); // 12
…
使用 curried function 的好處是能簡化參數的處理,若無法在第一時間就得到所有需要的參數,就可這樣來批次處理;也因為可批次處理,因此可將程式碼拆解成更細的片段,有助可讀性、彈性、方便重複利用。
備註:currying(柯里化)是指「將一個接受 n 個參數的函式,轉變成 n 個只接受一個參數的函式」的過程,可參考這裡。
…
除了傳數字進去,也可以傳各式各樣的資料結構。下面要提到的就是其中一種應用,利用柯里化的原理將元件傳到函式中,包裝元件而能新增屬性,也就是 decorator。
Decorator 裝飾器
decorator(裝飾器)即是柯里化的應用,如下所示,函式 sayHello 先傳入字串 name,再傳入元件 cmp,即可為元件新增屬性 intro。
範例 3
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
const App = () => {
return (
<div className='App'>
<HelloWorld />
</div>
);
};
const sayHello = (name) => {
return (cmp) => {
cmp.intro = `Hello, I am ${name}`;
console.log(cmp.intro);
return cmp;
};
};
class HelloWorld extends Component {
render() {
return <div>Hello World!</div>;
}
}
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
sayHello('John')(HelloWorld);
執行結果,依序將字串 John
與元件 HelloWorld 帶入函式 sayHello 中,設定元件的屬性 intro,最後印出字串。
Hello, I am John
範例 4
使用多個 decorator 也是可以的,在這裡使用了兩個 decorator-sayHi 與 sayHello 來包裝元件 HelloWorld。
const App = () => {
return (
<div className='App'>
<HelloWorld />
</div>
);
};
const sayHi = (name) => {
return (cmp) => {
cmp.intro = `Hi, I am ${name}`;
console.log(cmp.intro);
return cmp;
};
};
const sayHello = (name) => {
return (cmp) => {
cmp.greeting = `Hello, I am ${name}`;
console.log(cmp.greeting);
return cmp;
};
};
class HelloWorld extends Component {
render() {
return <div>Hello World!</div>;
}
}
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
sayHello('Patty')(sayHi('John')(HelloWorld));
執行結果。
Hi, I am John
Hello, I am Patty
範例 5
可用新式語法 @
來使用 decorator。注意,目前 decorator 尚在 Stage 2 階段,並未正式全面支援,因此若想使用就必須安裝 babel-plugin-transform-decorators-legacy 做轉譯。如下所示,sayHi 是個 decorator,為 HelloWorld 新增 intro 的屬性。
const App = () => {
return (
<div className='App'>
<HelloWorld />
</div>
);
};
const sayHi = (cmp) => {
cmp.intro = 'Hi';
return cmp;
};
@sayHi
class HelloWorld extends Component {
render() {
return <div>Hello World!</div>;
}
}
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
console.log(HelloWorld.intro);
執行結果,將元件 HelloWorld 的屬性 intro 所設定的字串印出來。
Hi;
範例 6
decorator 也可以代入參數喔!如下所示,sayHi 可傳入一個字串當成打招呼時用的名字。
const App = () => {
return (
<div className='App'>
<HelloWorld />
</div>
);
};
const sayHi = (name) => {
return (cmp) => {
cmp.intro = `Hi, I am ${name}`;
console.log(cmp.intro);
return cmp;
};
};
@sayHi('John')
class HelloWorld extends Component {
render() {
return <div>Hello World!</div>;
}
}
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
執行結果,將 John 帶入 sayHi 中,設定給屬性 intro,最後印出來。
Hi, I am John
範例 7
使用多個 decorator。
const App = () => {
return (
<div className='App'>
<HelloWorld />
</div>
);
};
const sayHi = (name) => {
return (cmp) => {
cmp.intro = `Hi, I am ${name}`;
console.log(cmp.intro);
return cmp;
};
};
const sayHello = (name) => {
return (cmp) => {
cmp.greeting = `Hello, I am ${name}`;
console.log(cmp.greeting);
return cmp;
};
};
@sayHello('Patty')
@sayHi('John')
class HelloWorld extends Component {
render() {
return <div>Hello World!</div>;
}
}
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
執行結果。
Hi, I am John
Hello, I am Patty
react-decoration
react-decoration 是一個提供 React.js 元件的方法可用的 decorator 的函式庫。舉個簡單的例子來看吧,假設我們希望能印出傳入方法的參數,就可使用 @log
來做這件事情,可參考官方原始碼和範例文件 。
範例 8
範例如下,點擊按鈕「View log!」時,印出傳入 onClickHandler 的參數。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { log } from 'react-decoration';
const App = () => {
return (
<div className='App'>
<HelloWorld />
</div>
);
};
const sayHi = (name) => {
return (cmp) => {
cmp.intro = `Hi, I am ${name}`;
console.log(cmp.intro);
return cmp;
};
};
const sayHello = (name) => {
return (cmp) => {
cmp.greeting = `Hello, I am ${name}`;
console.log(cmp.greeting);
return cmp;
};
};
class HelloWorld extends Component {
constructor(props) {
super(props);
this.onClickHandler = this.onClickHandler.bind(this);
}
@log
onClickHandler(e) {
// ...
}
render() {
return (
<div>
Hello World!
<button onClick={this.onClickHandler}>View log!</button>
</div>
);
}
}
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
sayHello('Patty')(sayHi('John')(HelloWorld));
點擊按鈕「View log!」後的執行結果,可看到「Calling function “onClickHandler” with params…」印出傳入 onClickHandler 的參數。
Hi, I am John
Hello, I am Patty
Calling function "onClickHandler" with params: Proxy {dispatchConfig: {…}, _targetInst: FiberNode, nativeEvent: MouseEvent, type: "click", target: button, …}
…
似乎 HOC 的撰寫方式與 decorator 有 87 分像!沒錯,HOC 其實就是利用先前提到的 curry、decorator 的觀念,或可說是我們所熟知的 higher-order function,只是把輸入與輸出的參數從函式改為元件而已。因此,在實作上,HOC 即是將元件當成參數傳入函式,再將經過 decorator 包裝後的元件輸出。
HOC
截圖自天瓏網路書店。
「沒事多重構,多重構沒事」,趕快用 HOC 整理程式碼吧。
…
HOC(Higher-Order Component)是指分離元件的表現層與展示層,將表現層製成 pure component(或稱 presentational component),而在 HOC 放置核心邏輯、提取資料的 API、處理資料的各種方法等,也就是 container component,最後將表現層元件當成參數傳給 HOC,輸出包裝後的元件。關於如何實作 HOC 可參考這裡。
這麼做的好處是「關注點分離」,分開 UI 與邏輯兩部份而讓元件更好維護,並且可避免每次都重新打造相似的輪子,而達到省時省力、可重用、更好的模組化的目的。
常用的 react-redux 的 connect 就是一個例子,connect 將 reducer state 包裝成 prop 與 action 傳給個別元件。
import React, { Component } from 'react';
import { connect } from 'react-redux';
class HelloWorld extends Component {
// ...
}
export default connect(mapStateToProps)(HelloWorld);
和上面提到的 decorator 觀念相同,因此可將 HOC 改寫為 decorator 的方式。
import React, { Component } from 'react';
import { connect } from 'react-redux';
@connect(mapStateToProps)
class HelloWorld extends Component {
// ...
}
export default HelloWorld;
同樣的,使用多個 HOC 也是可以的,方法就如同前面提到的 decorator 一樣。
recompose
recompose 提供給 React.js 許多實用的 HOC,舉個簡單的例子來說,如前面提到,我們用了很多個 HOC 來包裝元件,看得眼花撩亂的,因此可用 compose
來做簡化,可參考官網範例。
範例 9
如前面範例 4 所示,我們使用了多個 decorator 來包裝元件 HelloWorld,使其能擁有 sayHi 和 sayHello 的功能。
原本的寫法。
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
sayHello('Patty')(sayHi('John')(HelloWorld));
改進後的寫法,利用 compose
來為多個 HOC 的組合做簡化,看起來清爽多了。
const enhance = compose(sayHello('Patty'), sayHi('John'));
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
enhance(HelloWorld);
完整程式碼。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { compose } from 'recompose';
const App = () => {
return (
<div className='App'>
<HelloWorld />
</div>
);
};
const sayHi = (name) => {
return (cmp) => {
cmp.intro = `Hi, I am ${name}`;
console.log(cmp.intro);
return cmp;
};
};
const sayHello = (name) => {
return (cmp) => {
cmp.greeting = `Hello, I am ${name}`;
console.log(cmp.greeting);
return cmp;
};
};
class HelloWorld extends Component {
render() {
return <div>Hello World!</div>;
}
}
// 利用 `compose` 來為多個 HOC 的組合做簡化
const enhance = compose(sayHello('Patty'), sayHi('John'));
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);
enhance(HelloWorld);
執行結果。
Hi, I am John
Hello, I am Patty
雖然 compose
是用於為多個 HOC 的組合做簡化,但我個人還是比較喜歡用 decorator @
。
@sayHello('Patty')
@sayHi('John')
class HelloWorld extends Component {
render() {
return <div>Hello World!</div>;
}
}
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);