函數式程式設計(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); // 2let num = 1;
const f2 = () => num + 1;
f2();
console.log(num); // 1
num = f2(num);
console.log(num); // 2let 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 的結果
// => 25const 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);
// => 12import { 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
      });
    }
  }
}