函數式程式設計(Functional Programming)是一種程式設計範式,其以函數作為構成程式的主要單位,將所有問題抽象成帶有動作的函數並組合呼叫
思維上比較接近數學上套用各種公式來解決問題,能夠將一個複雜的問題分拆解析成更好理解與管理的處理流程
JavaScript 是以原型(Prototype)為基礎的多範式程式語言,也就是說物件導向與函數式都只是 JavaScript 支援的其中一種範式而已
在現代化的前端技術中被廣泛採用,React 生態圈中的相關技術大多都深受函數式程式設計的思想影響
一個程式語言是否支援函數式程式設計範式有一個必要的先決條件,就是該語言中的函數是否為一等公民(First-Class)
所謂一等公民,即函數在語言中可以被當作值來運算,可以被當作其他函數的參數或回傳值來進行傳遞、加工與延遲呼叫
JavaScript 中的函數為一等公民,因此基本上可以支援函數式程式設計
const printSomething = function() {
console.log('something');
}
setTimeout(printSomething, 2000);
$('#btn').click(function() {
console.log('clicked');
});
[1, 2, 3].map(num => num + 1);
// => [2, 3, 4]
let num = 1;
const f1 = () => {
num += 1;
}
f1();
console.log(num); // 2
let num = 1;
const f2 = () => num + 1;
f2();
console.log(num); // 1
num = f2(num);
console.log(num); // 2
let num = 1;
const f3 = (n) => n + 1;
f3(num);
console.log(num); // 1
num = f3(num);
console.log(num); // 2
影響外部資料
依賴外部資料
Pure
const squareNumber = memorize(x => x * x);
squareNumber(4);
// => 16
squareNumber(4); // 從緩存中取得當參數為 4 的結果
// => 16
squareNumber(5);
// => 25
squareNumber(5); // 從緩存中取得當參數為 5 的結果
// => 25
const decorate = (f) => (x) => f(x) - 3;
const addTen = (x) => x + 10;
const addSeven = decorate(addTen);
addSeven(5);
// => 12
// 原函數
const add = function(x, y) {
return x + y
};
// Currying 之後
add = function(x) {
return function(y) {
return x + y;
};
};
// ES6 Arrow Function 的寫法
add = x => y => x + y
const increment = add(1);
const addTen = add(10);
increment(2);
// => 3
addTen(2);
// => 12
import { compose } from 'redux'
compose(f1, f2, f3)(x);
// 等同於
f1(f2(f3(x)));
const f = x => 2 * x + 1
const g = x => x ** 2
const h = compose(g, f);
h(4); // 81
= g(f(x))
= (2x + 1) ^ 2
import { compose } from 'redux'
const map = func => array => array.map(func);
const filter = func => array => array.filter(func);
const increaseAll = map(x => x + 1);
const takeEven = filter(x => x % 2 == 0);
const increaseAllAndTakeEven = compose(takeEven, increaseAll);
increaseAllAndTakeEven([1, 2, 3, 4, 5]); // [2, 4, 6];
function Macbook(data) {
this.brand = 'Apple';
this.model = data.model;
this.cpu = data.cpu;
this.ram = data.ram;
this.ssd = data.ssd;
}
Macbook.models = {
2014: ['2014 Mid'],
2015: ['2015 Early'],
2016: [
'2016 Late Without TouchBar',
'2016 Late With TouchBar'
]
};
Macbook.getModelsByYear = function(year) {
return Macbook.models[year];
};
Macbook.prototype.reboot = function () {
console.log('do reboot...');
};
var mbpr = new Macbook({
model: '2016 Late Without TouchBar',
cpu: 'i7-6660U',
ram: '16G',
ssd: '512G'
});
console.log(mbpr);
/*
Macbook {
brand: "Apple",
model: "2016 Late Without TouchBar",
cpu: "i7-6660U",
ram: "16G",
ssd: "512G"
} */
mbpr.reboot();
// "do reboot..."
console.log(Macbook.getModelsByYear(2016));
/* [
"2016 Late Without TouchBar",
"2016 Late With TouchBar"
] */
class Macbook {
brand = 'Apple';
constructor({model, cpu, ram, ssd}) {
this.model = model;
this.cpu = cpu;
this.ram = ram;
this.ssd = ssd;
}
reboot() {
console.log('do reboot...');
}
static models = {
2014: ['2014 Mid'],
2015: ['2015 Early'],
2016: [
'2016 Late Without TouchBar',
'2016 Late With TouchBar'
]
};
static getModelsByYear(year) {
return Macbook.models[year];
}
}
class MacbookPro extends Macbook{
constructor(data) {
super(data);
}
isPro = true;
}
const mbpr = new MacbookPro({
model: '2016 Late Without TouchBar',
cpu: 'i7-6660U',
ram: '16G',
ssd: '512G'
});
console.log(mbpr);
/*
MacbookPro {
brand: "Apple",
model: "2016 Late Without TouchBar",
cpu: "i7-6660U",
isPro: true,
ram: "16G",
ssd: "512G"
} */
前端程式非常難以推理
前端的全域變數與副作用操作太多
DOM 操作
事件處理
localStorage、Cookie
前端是非同步的
AJAX
函數式程式設計強調將運算過程封裝成片段的純函數,利用高階函數的特性,能夠彈性的組合與修飾邏輯片段
將副作用隔離,來讓主要的資料運算流程保持 Pure,能夠使前端程式碼的可推測性大幅提升
React 將副作用隔離到 Virtual DOM 的封裝
事件處理、DOM 操作
Redux 將副作用隔離到 Action,若這個副作用操作流程會被重複使用,可抽象化成 Middleware
React 的工作分為兩個部分:
Reconciler(react):將你定義的 UI 組合出一個虛擬的畫面結構
Renderer(react-dom):以這個虛擬結構作為依據,產生出對應的實際 DOM
<!DOCTYPE html>
<html>
<body>
<div id="root"></div>
</body>
<script src="/dist/bundle.js"></script>
</html>
import React from 'react';
import ReactDOM from 'react-dom'
const reactElement = (
React.createElement('div', { className: 'box' },
React.createElement('button', { onClick: () => alert('clicked') }, 'click me')
)
);
ReactDOM.render(reactElement, document.getElementById('root'));
Virtual DOM 是一份純資料的 Tree 物件結構,映射對應到實際的 DOM
使用 React.createElement 函數來產生 Tree 節點(React Element)
Virtual DOM 為自定義組件提供了中介的虛擬層,讓開發者能以聲明式的方式定義 UI 的顯示邏輯與行為
我們透過定義組件來表達「UI 什麼情況該如何呈現」,而「要用什麼手段來達到這個畫面改變(如何產生和操作 DOM)」 ,React 則會自動幫你完成 (react-dom 這個 Renderer 的工作)
React.createElement('div', null,
React.createElement(AlertButton, { text: 'HelloJS' }),
React.createElement(AlertButton, { text: 'React' }),
React.createElement(AlertButton, { text: 'Basic' })
)
UI 開發最大的兩個問題與挑戰就是「可重用性」與「反應變化」
然而 Web 中建構 UI 的媒介 – DOM,並沒有直接滿足以下需求的能力:
自定義資料抽象化
複雜情形的組合與重用
綁定資料來源以自動反應顯示結果與變化
因此 React 建立了一個虛擬結構層,來間接實現這些對於 UI 開發來說相當重要的能力
我們對於 Virtual DOM 這個虛擬結構層以組件的形式定義想要的 UI 呈現結構,而 Renderer 則會幫我們將其自動轉換成對應的實際 DOM 結果
One-way Data Flow(單向資料流)
只有因為資料改變,才能導致 UI 的顯示結果自動跟著改變
這個因果關係永遠不能逆向
UI 只能被動的隨資料而反應變化
UI 不能反過來主動直接修改資料或是修改 UI 自己的顯示結果
React 如何實現單向資料流:當 UI 的來源資料有變化時
不需要關心整份資料中具體是變化了哪些部分
先把 UI 畫面全部洗掉,然後再依據完整的最新資料全部重新產生 UI 畫面,通常可以保證顯示結果一定是正確的
然而每次都重繪全部的實體 DOM 顯然在效能考量之下是不可行的,但是重繪 Virtual DOM 則成本相對降低許多,因此 React 實作了一套 Reconciliation 演算法來實現這個概念與流程
當畫面需要改變時,根據最新的資料重繪出新的 Virtual DOM Tree,
並與改變前的舊 Virtual DOM Tree 進行全面式的比較與計算,
其中被發現新舊差異的地方,才真的會在實際的 DOM 上發生操作改變
JSX 是 React 在使用的一種特殊 JavaScript 語法糖
能夠讓你以可讀性較高的語法來定義 React UI 結構
語法長得很像 HTML,但本質上完全不是 HTML
瀏覽器看不懂,需要翻譯成原本的 React.createElement 語法才能正常的在瀏覽器上執行
<div className="box">
<button onClick={() => alert('clicked')}>click me</button>
</div>
React.createElement("div", {"className": "box"},
React.createElement("button", {"onClick": () => alert("clicked")}, "click me")
)
Babel Compile
JSX 是 React.createElement 函數的語法糖,用來建立 Virtual DOM 節點結構
支援原生 HTML DOM 有的標籤以及自訂的 Component Class 標籤
嚴格標籤閉合
與 HTML 重要的語法差異
class → className
所有 property 名稱改以駝峰式命名,EX:onclick → onClick
若想要傳遞的 Props 的值是固定字串的話,可以直接使用雙引號,其他情況則需要使用大括號來包住
<div>
<h2 className="title">Title</h2>
<NumberItem number={100}/>
<br/>
</div>
使用 { } 語法來填入 JavaScript 表達式(一個值),其中可直接當作顯示內容印出的型別有:
React Element:當作子節點插入
String:直接印出
Number:轉成字串後直接印出
Array:攤平成多個表達式後印出(如果 item 的值也是這些可印的型別)
Boolean、Null、Undefined:什麼都不印,直接忽略
可以在組件之間用 Props 傳遞,但不能當顯示內容印出的型別有:
Object
Function
<div>
{(a > 100) && (
<AlertButton text="HelloJS"/>
)}
<AlertButton text="React"/>
<AlertButton text="Basic"/>
</div>
<div>
{(a > 100) ? (
<AlertButton text="HelloJS"/>
): (
<AlertButton text="React"/>
)}
<AlertButton text="Basic"/>
</div>
<div>
{numbers.map((number, key) => (
<button key={key}>{number}</button>
))}
</div>
class Button extends React.PureComponent {
render() {
return (
<button>{this.props.text}</button>
)
}
}
// 每次 render,style 都是新的 object
<List style={{ color: 'silver'}} />
// 每次 render,itmes 都是新的 array
<IAmPure items={items.map(item => item + 1)} />
// 每次 render,onClick 都是新的 function
<List onClick={e => console.log(e)} />
class Input extends React.PureComponent {
handleChange(e) {
console.log(e.target.value);
}
render() {
return (
<input onChange={this.handleChange.bind(this)}/>
)
}
}
class Input extends React.PureComponent {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
console.log(e.target.value);
}
render() {
return (
<input onChange={this.handleChange}/>
)
}
}
class Input extends React.PureComponent {
handleChange = (e) => {
console.log(e.target.value);
}
render() {
return (
<input onChange={this.handleChange}/>
)
}
}
class App extends React.Component {
render() {
return (
<Test/>
)
}
}
class Test extends React.Component {
static defaultProps = {
num: 100
}
state = {
active: true
}
constructor(props) {
// 此處仍不能使用 this
super(props);
// 呼叫 super 方法並傳入 props 之後,
// 便可以使用 this.props
console.log(this.props); // { num: 100 }
console.log(this.state); // { active: true }
}
render() {
return (
<div>text</div>
)
}
}
class App extends React.Component {
state = {
data: 0
}
componentWillMount() {
this.setState({
data: 1
})
}
render() {
return (
<div>{this.state.data}</div>
)
}
}
class App extends React.Component {
state = {
data: 0
}
componentWillMount() {
this.setState({
data: 1
})
}
render() {
return (
<div>{this.state.data}</div>
)
}
}
class App extends React.Component {
state = {
data: ''
}
componentDidMount = async() => {
const { data } = await this.fetchAPI();
this.setState({ data });
}
fetchAPI = async() => {
const response = await fetch('https://api.myjson.com/bins/jtkux');
return await response.json();
}
render() {
return (
<div>{this.state.data}</div>
)
}
}
class Button extends React.Component {
componentWillReceiveProps(nextProps) {
console.log(this.props); // old props
console.log(nextProps); // new props
}
render() {
return (
<button>{this.props.text}</button>
)
}
}
class Button extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return this.props.text != nextProps.text;
}
render() {
return (
<button>{this.props.text}</button>
)
}
}
class Button extends React.Component {
componentWillUpdate(nextProps, nextState) {
}
render() {
return (
<button>{this.props.text}</button>
)
}
}
class NewsList extends React.Component {
state = {
newsList: []
}
componentDidMount() {
this.loadNewsListPage(1);
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.page != this.props.page) {
this.loadNewsListPage(this.props.page);
}
}
async loadNewsListPage(page) {
const response = await fetch(`/api/news/list/${page}`);
this.setState({
newsList: await response.json()
});
}
render() {
return (
<div>
{this.state.newsList.map(item => {
<NewsListItem {...item} key={item.id}/>
})}
</div>
)
}
}
class App extends React.Component {
componentDidMount() {
window.addEventListener('scroll', this.handleWindowScrolling);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleWindowScrolling);
}
handleWindowScrolling() {
console.log('scrolling')
}
render() {
return (
<div/>
)
}
}
import React from 'react'
import PureRenderMixin from 'react-addons-pure-render-mixin'
React.createClass({
mixins: [PureRenderMixin],
render: function() {
return (
<div>foo</div>
)
}
});
class App extends React.PureComponent {
render() {
console.log(this.props);
return (
<div>App</div>
)
}
}
const addName = (WrappedComponent) => {
return class extends React.PureComponent {
name = 'Zet';
render() {
return (
<WrappedComponent
{...this.props}
name={this.name}
/>
)
}
}
}
export default addName(App);
class App extends React.PureComponent {
render() {
console.log(this.props);
return (
<div>App</div>
)
}
}
const addName = (WrappedComponent) => {
return class extends React.PureComponent {
name = 'Zet';
render() {
return (
<WrappedComponent
{...this.props}
name={this.name}
/>
)
}
}
}
export default addName(App);
class App extends React.PureComponent {
render() {
console.log(this.props);
return (
<div>App</div>
)
}
}
const addName = (WrappedComponent) => {
return class extends React.PureComponent {
name = 'Zet';
render() {
return (
<WrappedComponent
{...this.props}
name={this.name}
/>
)
}
}
}
export default addName(App);
class App extends React.PureComponent {
render() {
console.log(this.props);
return (
<div>App</div>
)
}
}
const addBorder = (WrappedComponent) => {
return class extends React.PureComponent {
styles = {
border: '1px solid #DDD'
}
render() {
return (
<div style={styles}>
<WrappedComponent
{...this.props}
name={this.name}
/>
</div>
)
}
}
}
export default addBorder(App);
組件
Mixin
Mixin
組件
Mixin
高階組件
class App extends React.PureComponent {
render() {
return (
<div>
<Button>Button</Button>
</div>
)
}
}
const bgColor = '#009688';
const Button = styled.button`
color: #FFF;
background-color: ${bgColor};
padding: 8px;
border: 1px solid #DDD;
border-radius: 10px;
`
class App extends React.PureComponent {
handleClick = () => {
alert('clicked!');
}
render() {
return (
<div>
<Button onClick={this.handleClick}>Click me</Button>
</div>
)
}
}
const Button = styled.button`
color: #FFF;
background-color: #009688;
padding: 8px;
border: 1px solid #DDD;
border-radius: 10px;
`
const Button = styled.button`
color: #FFF;
background-color: #009688;
padding: 8px;
border: 1px solid #DDD;
border-radius: 10px;
&:hover {
background-color: green;
}
`
class App extends React.PureComponent {
state = {
active: true
}
handleClick = () => {
this.setState({
active: !this.state.active
})
}
render() {
return (
<div>
<Button
onClick={this.handleClick}
active={this.state.active}
>
Click me
</Button>
</div>
)
}
}
const Button = styled.button`
color: #FFF;
background-color: #009688;
padding: 8px;
border: 1px solid #DDD;
border-radius: 10px;
opacity: ${props => props.active ? '1' : '0.5'};
`
const Button = styled.button`
color: #FFF;
background-color: #009688;
padding: 8px;
border: 1px solid #DDD;
border-radius: 10px;
opacity: ${props => props.active ? '1' : '0.5'};
&:hover {
background-color: green;
}
`
const RedTextButton = styled(Button)`
color: red;
`
class ButtonText extends React.PureComponent {
render() {
return (
<span
className={this.props.className}
>
{this.props.text}
</span>
)
}
}
const BlueButtonText = styled(ButtonText)`
color: blue;
`
View
( React )
Store
Action
Reducer
pass
by
dispatch
畫面需要改變
產生 action
return
newState
資料變更
Server
const initialState = { num: 0, name: '' };
export default (state = initialState, action) => {
switch (action.type) {
case 'CHANGE_NAME':
return {
...state,
name: action.payload.name
}
case 'INCREMENT':
return {
...state,
num: state.num + 1
}
default:
return state;
}
}
dispatch
Redux
Thunk
Middleware
Reducer
action is a function
=> pass dispatch and call it
action is an object
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
const fooAction = (num) => (dispatch) => {
dispatch({ type: 'ADD_NUM', payload: { num } });
dispatch({ type: 'ADD_NUM', payload: { num: num + 1 } });
dispatch({ type: 'ADD_NUM', payload: { num: num + 2 } });
}
// in react component
this.props.dispatch(fooAction(10));
當你呼叫一個非同步 API,有兩個關鍵的時間點:你開始呼叫的的時候,以及當你收到回應 (或是失敗) 的時候。
所以一個請求的行為應該可以分為下列四種狀態:
尚未開始請求(status: null)
請求中,還沒得到結果(status: request)
得到結果並成功(status: success)
得到結果但失敗 (status: failure)
你可以將上述中會發生的三種情況(除掉預設本來就是還沒請請求的狀態),設計成對應的三種 Action
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
const initState = {
status: null,
data: [],
error: null
}
function postReducer(state = initState, action) {
switch (action.type) {
case FETCH_POSTS_REQUEST:
return {
...state,
status: 'request'
};
case FETCH_POSTS_SUCCESS:
return {
...state,
status: 'success',
data: action.response
}
case FETCH_POSTS_FAILURE:
return {
...state,
status: 'failure',
error: action.error
}
default:
return state;
}
}
使用Redux Thunk 來讓 dispatch 方法可以接收一個函數,並將 AJAX 的非同步副作用集中於此處理
function fetchPosts() {
return async (dispatch) => {
dispatch({
type: FETCH_POSTS_REQUEST
});
try {
const httpResponse = await fetch('/posts');
if (httpResponse.status != 200) {
throw new Error(`${httpResponse.status(httpResponse.statusText)}`);
}
dispatch({
type: FETCH_POSTS_SUCCESS,
response: await httpResponse.json()
});
} catch (error) {
console.error(error);
dispatch({
type: FETCH_POSTS_FAILURE,
error: error.message
});
}
}
}