[譯]從零開始Redux(一)初體驗(yàn)

前言

參加了公司大拿組織的前端學(xué)習(xí)會,本來也是抱著學(xué)習(xí)的心態(tài),React這塊自己也就會寫寫一般的組件和界面,也沒做過什么深入的研究。但是年后排到我分享了,也不能只聽不說吧。正好之前看了一點(diǎn)redux作者寫的系列教學(xué)視頻,感覺說的非常到位,就把它翻譯過來供大家閱讀。我個人認(rèn)為Redux中的很多思想其實(shí)在后端中體現(xiàn)更多,我也比較好理解,包括狀態(tài)轉(zhuǎn)移,不可變性對象,純函數(shù)等等。原文在此,整個視頻長度大約在121分鐘。

概述

在提供給用戶良好體驗(yàn)這件事上,狀態(tài)管理起到了關(guān)鍵作用,并且它也是現(xiàn)代前端工程中比較難處理的一個點(diǎn)。Redux提供了一個穩(wěn)定成熟的解決方案來讓你在React應(yīng)用中管理狀態(tài)。通過一系列精密的模式,Redux能夠?qū)⒛愕膽?yīng)用從一堆復(fù)雜晦澀的狀態(tài)中解脫出來,使之能夠優(yōu)美得組織并淺顯易懂。
Redux的設(shè)計原理并不新奇,它呈現(xiàn)給你了一個易于使用的工具,不僅提升了你的應(yīng)用質(zhì)量,也讓你更深入的理解現(xiàn)代JS項(xiàng)目應(yīng)該如何構(gòu)建。

教程

基本原則

首先我們來熟悉Redux的幾個原則:

  • 無論你的應(yīng)用是復(fù)雜還是簡單,你的狀態(tài)都是由一個不可變的js對象 (state or state tree) 來存儲并表示
  • 每當(dāng)你要做狀態(tài)的修改,你需要下發(fā)一個行為(action)來表明你需要做的修改,行為里面有一個屬性是必須有的,就是它的類型(type),用來唯一標(biāo)明這個行為的意義
  • 針對一個狀態(tài)和一個行為,通過一個純函數(shù)(pure function)來產(chǎn)生新的js對象來表示行為發(fā)生后的狀態(tài)

在一切開始之前,我們先來討論下純函數(shù)和有副作用的函數(shù):

// pure function 純函數(shù)
function square(x) {
  return x * x;
}
function squareAll(items) {
  return items.map(square)
}

// Impure function 非純函數(shù),有副作用
function square(x) {
  updateXInDatabase(x);
  return x * x;
}
function squareAll(items) {
  for (let i = 0; i< items.length; i++ ) {
    items[i] = square(items[i])
  }
}

上面四個函數(shù)做的事情是類似的,都是將一個輸入的x平方并返回,但是第三個函數(shù)不能為純函數(shù)的點(diǎn)就在于它還做了一個數(shù)據(jù)庫操作,而第四個函數(shù)的問題是他修改了本身入?yún)tems的值,這個修改和數(shù)據(jù)庫操作也就是我們說的副作用。從定義上講,一個純函數(shù)應(yīng)該這樣:

對于一個入?yún)ⅲ瑹o論什么時候什么情況,都會產(chǎn)生同樣的輸出,并且不會修改原有入?yún)⒌膶傩?/p>

純函數(shù)在代碼推到和邏輯建立上比非純函數(shù)會有比較大的優(yōu)勢,因?yàn)閷τ谝粋€純函數(shù),你只需要考慮入?yún)ⅲ湍芡茖?dǎo)出會得到的結(jié)果,而不需要考慮其他的因素。針對之前提到的Redux兩個原則,Redux中所使用或者你傳遞給Redux的函數(shù)都應(yīng)該是純函數(shù),這是Redux中的一個規(guī)約。
在Redux中,狀態(tài)的轉(zhuǎn)移是用了一個純函數(shù)來完成,這個純函數(shù)的入?yún)⑹钱?dāng)前的狀態(tài)(state)和一個下發(fā)行為(action),出參是一個新的狀態(tài),并且不改變原本狀態(tài)的值,類似于這樣:

function reducer(state, action){
   var newState = undefined;
   /**
   do something to transform
   **/
   return newState;
}

從計數(shù)器開始

我們了解了Redux的一個基本原則之后,我們來嘗試一下自己寫下第一個例子,以一個計數(shù)器為例:

const counter = (state = 0, action) => {
  switch(action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

expect(
  counter(0, { type: 'INCREMENT'})
).toEqual(1);

expect(
  counter(1, { type: 'INCREMENT'})
).toEqual(2);

expect(
  counter(2, { type: 'DECREMENT'})
).toEqual(1);

expect(
  counter(undefined, {})
).toEqual(0);

上面這個例子中,我們通過expect這個庫來完成單元測試,然后這里面需要注意的是,我們函數(shù)里面針對每個行為都創(chuàng)建了一個新的狀態(tài),并且我們針對缺省的行為和狀態(tài)有一個默認(rèn)的初態(tài)和行為,這個非常關(guān)鍵。通過上面一段代碼,我們構(gòu)建了一個簡易的reducer函數(shù),這個函數(shù)通過一個狀態(tài)和行為產(chǎn)生一個新的狀態(tài),并保證自己是純函數(shù)。

實(shí)戰(zhàn)第一發(fā)

在了解了我們的reducer之后,我們來使用redux進(jìn)行我們實(shí)戰(zhàn)的第一發(fā),原文直接在頁面上寫的,翻譯過程中我直接放到react工程中了。首先我們編寫一個組件放置我們的reducer方法:

// in CounterReducer.js
export default function counter(state = 0, action) {
    console.log('enter counter action' + action.type);
    switch(action.type) {
      case 'INCREMENT':
        return state + 1;
      case 'DECREMENT':
        return state - 1;
      default:
        return state;
    }
};

然后我們在我們的計數(shù)器組件中這么寫:

import React, { Component } from 'react';
import './App.css';
import {createStore} from 'redux'
import counter from './CounterReducer'


class Counter extends Component {
  constructor(props) {
    super(props);
    this.store = createStore(counter);
    this.state = {
      count: this.store.getState()
    };
    this.store.subscribe(() => this.setState({count:this.store.getState()}));
  }

  render() {
    return (
      <div>
        <p>
          {this.state.count}
        </p>
        <div>
          <button onClick={() => this.store.dispatch({type: 'INCREMENT'})}>+</button>
          <button onClick={() => this.store.dispatch({type: 'DECREMENT'})}>-</button>
        </div>
      </div>
      
    );
  }
}

export default Counter;

其中我們需要注意的是這一行:

this.store = createStore(counter);

Redux通過createStore 方法根據(jù)你傳入的reducer創(chuàng)建了一個js對象來存儲你應(yīng)用的狀態(tài),在才創(chuàng)建的時候這個狀態(tài)處于你的初態(tài),因?yàn)槟悴]有傳入任何參數(shù),所以reducer支持缺省和默認(rèn)初態(tài)就顯得重要了。然后這個對象支持幾個方法:

  • getState 獲取當(dāng)前的狀態(tài)
  • dispatch 下發(fā)行為來改變狀態(tài)
  • subscribe 注冊一個回調(diào)函數(shù),在每次行為發(fā)生時調(diào)用
    有了這三個函數(shù),我們只需要在頁面的按鈕中加上按鍵回調(diào),下發(fā)指定的行為,就能很好地控制我們的應(yīng)用狀態(tài)和展示,如圖所示:
    計數(shù)器

createStore從無到有

在了解了實(shí)戰(zhàn)之后,我們再來嘗試一步步實(shí)現(xiàn)我們的createStore方法
首先我們來把骨架搭起來:

const createStore = (reducer) => {
    let state;

    const getState = () => state;

    const dispatch = (action) => {

    }

    const subscribe = (listener) => {

    }
    return {getState, dispatch, subscribe};
}

根據(jù)我們之前的描述,createStore方法接受一個reducer函數(shù)作為入?yún)ⅲ⒎祷匾粋€對象來保存狀態(tài),這個對象支持getState、dispatch、subscribe三個函數(shù)。上面這個骨架這幾點(diǎn)都滿足,接下來我們再來看具體的實(shí)現(xiàn)。首先針對subscribe,我們需要有一個數(shù)組來存放我們注冊上去的回調(diào)函數(shù),供我們后續(xù)下發(fā)行為之后使用,另外,我們還可以在subscribe的時候返回一個handler用于取消注冊,因此可以這樣實(shí)現(xiàn):

  let listeners = [];
  const subscribe = (listener) => {
      listeners.push(listener)
      return () => {
          listeners = listeners.filter(l => l !== listener);
      }
  }

然后我們再來看dispatch,可以通過reducer和當(dāng)前狀態(tài)產(chǎn)生一個新的狀態(tài),然后遍歷調(diào)用注冊的回調(diào):

    const dispatch = (action) => {
        state = reducer(state, action)
        listeners.forEach(listener => listener());
    }

然后為了在調(diào)用createStore的時候就能創(chuàng)建一個初態(tài),我們需要顯式調(diào)用一次dispatch,最終完成如下:

const createStore = (reducer) => {
    let state;
    let listeners = [];

    const getState = () => state;

    const dispatch = (action) => {
        state = reducer(state, action)
        listeners.forEach(listener => listener());
    }

    const subscribe = (listener) => {
        listeners.push(listener)
        return () => {
            listeners = listeners.filter(l => l !== listener);
        }
    }
    dispatch({})

    return {getState, dispatch, subscribe};
}

最終我們使用自己實(shí)現(xiàn)的這個createStore來替換了Redux的實(shí)現(xiàn),一切工作正常。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容