React學習筆記(四)

組件通信

父組件與子組件通信

  • 父組件將自己的狀態傳遞給子組件,子組件當做屬性來接收,當父組件更改自己狀態的時候,子組件接收到的屬性就會發生改變

  • 父組件利用ref對子組件做標記,通過調用子組件的方法以更改子組件的狀態,也可以調用子組件的方法..

子組件與父組件通信

  • 父組件將自己的某個方法傳遞給子組件,在方法里可以做任意操作,比如可以更改狀態,子組件通過this.props接收到父組件的方法后調用。

跨組件通信

在react沒有類似vue中的事件總線來解決這個問題,我們只能借助它們共同的父級組件來實現,將非父子關系裝換成多維度的父子關系。react提供了context api來實現跨組件通信, React 16.3之后的contextapi較之前的好用。

實例,使用context實現購物車中的加減功能

// counterContext.js
import React, { Component, createContext } from 'react'
?
const {
  Provider,
  Consumer: CountConsumer
} = createContext()
?
class CountProvider extends Component {
  constructor () {
    super()
    this.state = {
      count: 1
    }
  }
  increaseCount = () => {
    this.setState({
      count: this.state.count + 1
    })
  }
  decreaseCount = () => {
    this.setState({
      count: this.state.count - 1
    })
  }
  render() {
    return (
      <Provider value={{
        count: this.state.count,
        increaseCount: this.increaseCount,
        decreaseCount: this.decreaseCount
      }}
      >
        {this.props.children}
      </Provider>
    )
  }
}
?
export {
  CountProvider,
  CountConsumer
}



// 定義CountButton組件
const CountButton = (props) => {
  return (
    <CountConsumer>
      // consumer的children必須是一個方法
      {
        ({ increaseCount, decreaseCount }) => {
          const { type } = props
          const handleClick = type === 'increase' ? increaseCount : decreaseCount
          const btnText = type === 'increase' ? '+' : '-'
          return <button onClick={handleClick}>{btnText}</button>
        }
      }
    </CountConsumer>
  )
}



// 定義count組件,用于顯示數量
const Count = (prop) => {
  return (
    <CountConsumer>
      {
        ({ count }) => {
          return <span>{count}</span>
        }
      }
    </CountConsumer>
  )
}




// 組合
class App extends Component {
  render () {
    return (
        <CountProvider>
        <CountButton type='decrease' />
        <Count />
        <CountButton type='increase' />
      </CountProvider>
    )
  }
}

復雜的非父子組件通信在react中很難處理,多組件間的數據共享也不好處理,在實際的工作中我們會使用flux、redux、mobx來實現

HOC(高階組件)

Higher-Order Components就是一個函數,傳給它一個組件,它返回一個新的組件。

const NewComponent =  higherOrderComponent(YourComponent)

比如,我們想要我們的組件通過自動注入一個版權信息。

// withCopyright.js 定義一個高階組件
import React, { Component, Fragment } from 'react'
?
const withCopyright = (WrappedComponent) => {
  return class NewComponent extends Component {
    render() {
      return (
        <Fragment>
          <WrappedComponent />
          <div>&copy;版權所有 千鋒教育 2019 </div>
        </Fragment>
      )
    }
  }
}
export default withCopyright



// 使用方式
import withCopyright from './withCopyright'
?
class App extends Component {
  render () {
    return (
        <div>
        <h1>Awesome React</h1>
        <p>React.js是一個構建用戶界面的庫</p>
      </div>
    )
  }
}
const CopyrightApp = withCopyright(App)

這樣只要我們有需要用到版權信息的組件,都可以直接使用withCopyright這個高階組件包裹即可。

在這里要講解在CRA 中配置裝飾器模式的支持。

狀態管理

傳統MVC框架的缺陷

什么是MVC?

MVC的全名是Model View Controller,是模型(model)-視圖(view)-控制器(controller)的縮寫,是一種軟件設計典范。

V即View視圖是指用戶看到并與之交互的界面。

M即Model模型是管理數據 ,很多業務邏輯都在模型中完成。在MVC的三個部件中,模型擁有最多的處理任務。

C即Controller控制器是指控制器接受用戶的輸入并調用模型和視圖去完成用戶的需求,控制器本身不輸出任何東西和做任何處理。它只是接收請求并決定調用哪個模型構件去處理請求,然后再確定用哪個視圖來顯示返回的數據。

MVC只是看起來很美

MVC框架的數據流很理想,請求先到Controller, 由Controller調用Model中的數據交給View進行渲染,但是在實際的項目中,又是允許Model和View直接通信的。然后就出現了這樣的結果:

Flux

在2013年,Facebook讓React亮相的同時推出了Flux框架,React的初衷實際上是用來替代jQuery的,Flux實際上就可以用來替代Backbone.jsEmber.js等一系列MVC架構的前端JS框架。

其實FluxReact里的應用就類似于Vue中的Vuex的作用,但是在Vue中,Vue是完整的mvvm框架,而Vuex只是一個全局的插件。

React只是一個MVC中的V(視圖層),只管頁面中的渲染,一旦有數據管理的時候,React本身的能力就不足以支撐復雜組件結構的項目,在傳統的MVC中,就需要用到Model和Controller。Facebook對于當時世面上的MVC框架并不滿意,于是就有了Flux, 但Flux并不是一個MVC框架,他是一種新的思想。

  • View: 視圖層

  • ActionCreator(動作創造者):視圖層發出的消息(比如mouseClick)

  • Dispatcher(派發器):用來接收Actions、執行回調函數

  • Store(數據層):用來存放應用的狀態,一旦發生變動,就提醒Views要更新頁面

Flux的流程:

  1. 組件獲取到store中保存的數據掛載在自己的狀態上

  2. 用戶產生了操作,調用actions的方法

  3. actions接收到了用戶的操作,進行一系列的邏輯代碼、異步操作

  4. 然后actions會創建出對應的action,action帶有標識性的屬性

  5. actions調用dispatcher的dispatch方法將action傳遞給dispatcher

  6. dispatcher接收到action并根據標識信息判斷之后,調用store的更改數據的方法

  7. store的方法被調用后,更改狀態,并觸發自己的某一個事件

  8. store更改狀態后事件被觸發,該事件的處理程序會通知view去獲取最新的數據

Redux

React 只是 DOM 的一個抽象層,并不是 Web 應用的完整解決方案。有兩個方面,它沒涉及。

  • 代碼結構

  • 組件之間的通信

2013年 Facebook 提出了 Flux 架構的思想,引發了很多的實現。2015年,Redux 出現,將 Flux 與函數式編程結合一起,很短時間內就成為了最熱門的前端架構。

如果你不知道是否需要 Redux,那就是不需要它

只有遇到 React 實在解決不了的問題,你才需要 Redux

簡單說,如果你的UI層非常簡單,沒有很多互動,Redux 就是不必要的,用了反而增加復雜性。

  • 用戶的使用方式非常簡單

  • 用戶之間沒有協作

  • 不需要與服務器大量交互,也沒有使用 WebSocket

  • 視圖層(View)只從單一來源獲取數據

需要使用Redux的項目:

  • 用戶的使用方式復雜

  • 不同身份的用戶有不同的使用方式(比如普通用戶和管理員)

  • 多個用戶之間可以協作

  • 與服務器大量交互,或者使用了WebSocket

  • View要從多個來源獲取數據

從組件層面考慮,什么樣子的需要Redux:

  • 某個組件的狀態,需要共享

  • 某個狀態需要在任何地方都可以拿到

  • 一個組件需要改變全局狀態

  • 一個組件需要改變另一個組件的狀態

Redux的設計思想:

  1. Web 應用是一個狀態機,視圖與狀態是一一對應的。

  2. 所有的狀態,保存在一個對象里面(唯一數據源)。

注意:flux、redux都不是必須和react搭配使用的,因為flux和redux是完整的架構,在學習react的時候,只是將react的組件作為redux中的視圖層去使用了。

Redux的使用的三大原則:

  • Single Source of Truth(唯一的數據源)

  • State is read-only(狀態是只讀的)

  • Changes are made with pure function(數據的改變必須通過純函數完成)

自己實現Redux

這個部分,可以根據班級情況看是否講解。對于學生使用redux有很大的幫助。不使用react,直接使用原生的html/js來寫一個簡易的的redux

基本的狀態管理及數據渲染

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Redux principle 01</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定義一個計數器的狀態
    const countState = {
      count: 10
    }
?
    // 定一個方法叫changeState,用于處理state的數據,每次都返回一個新的狀態
    const changeState = (action) => {
      switch(action.type) {
        // 處理減
        case 'COUNT_DECREMENT':
          countState.count -= action.number
          break;
        // 處理加        
        case 'COUNT_INCREMENT':
          countState.count += action.number
          break;
        default:
          break;
      }
    }
?
    // 定義一個方法用于渲染計數器的dom
    const renderCount = (state) => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = state.count
    }
  
    // 首次渲染數據
    renderCount(countState)
?
    // 定義一個dispatch的方法,接收到動作之后,自動調用
    const dispatch = (action) => {
      changeState(action)
      renderCount(countState)
    }
?
  </script>
</body>
</html>

創建createStore方法

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Redux principle 02</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定義一個方法,用于集中管理state和dispatch
    const createStore = (state, changeState) => {
      // getState用于獲取狀態
      const getState = () => state
      
      // 定義一個監聽器,用于管理一些方法
      const listeners = []
      const subscribe = (listener) => listeners.push(listener)
?
       // 定義一個dispatch方法,讓每次有action傳入的時候返回render執行之后的結果
      const dispatch = (action) => {
        // 調用changeState來處理數據
        changeState(state, action)
        // 讓監聽器里的所以方法運行
        listeners.forEach(listener => listener())
      }
      return {
        getState,
        dispatch,
        subscribe
      }
    }
    // 定義一個計數器的狀態
    const countState = {
      count: 10
    }
    // 定一個方法叫changeState,用于處理state的數據,每次都返回一個新的狀態
    const changeState = (state, action) => {
      switch(action.type) {
        // 處理減
        case 'COUNT_DECREMENT':
          state.count -= action.number
          break;
        // 處理加        
        case 'COUNT_INCREMENT':
          state.count += action.number
          break;
        default:
          break;
      }
    }
?
    // 創建一個store
    const store = createStore(countState, changeState)
    // 定義一個方法用于渲染計數器的dom
    const renderCount = () => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = store.getState().count
    }
    // 初次渲染數據
    renderCount()
    // 監聽,只要有dispatch,這個方法就會自動運行
    store.subscribe(renderCount)
  </script>
</body>
</html>

讓changeState方法變為一個純函數

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Redux principle 03</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定義一個方法,用于集中管理state和dispatch
    const createStore = (state, changeState) => {
      // getState用于獲取狀態
      const getState = () => state
      
      // 定義一個監聽器,用于管理一些方法
      const listeners = []
      const subscribe = (listener) => listeners.push(listener)
?
      // 定義一個dispatch方法,讓每次有action傳入的時候返回render執行之后的結果
      const dispatch = (action) => {
        // 調用changeState來處理數據
        state = changeState(state, action)
        // 讓監聽器里的所有方法運行
        listeners.forEach(listener => listener())
      }
      return {
        getState,
        dispatch,
        subscribe
      }
    }
    // 定義一個計數器的狀態
    const countState = {
      count: 10
    }
    // 定一個方法叫changeState,用于處理state的數據,每次都返回一個新的狀態
    const changeState = (state, action) => {
      switch(action.type) {
        // 處理減
        case 'COUNT_DECREMENT':
          return {
            ...state,
            count: state.count - action.number
          }
        // 處理加        
        case 'COUNT_INCREMENT':
          return {
            ...state,
            count: state.count + action.number
          }
        default:
          return state
      }
    }
?
    // 創建一個store
    const store = createStore(countState, changeState)
    // 定義一個方法用于渲染計數器的dom
    const renderCount = () => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = store.getState().count
    }
    // 初次渲染數據
    renderCount()
    // 監聽,只要有dispatch,這個方法就會自動運行
    store.subscribe(renderCount)
  </script>
</body>
</html>

合并state和changeState(最終版)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Redux principle 04</title>
</head>
<body>
  <h1>redux principle</h1>
  <div class="counter">
    <span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
    <span class="count" id="count"></span>
    <span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
  </div>
  <script>
    // 定義一個方法,用于集中管理state和dispatch, changeState改名了,專業的叫法是reducer
    const createStore = (reducer) => {
      // 定義一個初始的state
      let state = null
      // getState用于獲取狀態
      const getState = () => state
      
      // 定義一個監聽器,用于管理一些方法
      const listeners = []
      const subscribe = (listener) => listeners.push(listener)
?
      // 定義一個dispatch方法,讓每次有action傳入的時候返回reducer執行之后的結果
      const dispatch = (action) => {
        // 調用reducer來處理數據
        state = reducer(state, action)
        // 讓監聽器里的所有方法運行
        listeners.forEach(listener => listener())
      }
      //  初始化state
      dispatch({})
      return {
        getState,
        dispatch,
        subscribe
      }
    }
    // 定義一個計數器的狀態
    const countState = {
      count: 10
    }
    // 定一個方法叫changeState,用于處理state的數據,每次都返回一個新的狀態
    const changeState = (state, action) => {
      // 如果state是null, 就返回countState
      if (!state) return countState
      switch(action.type) {
        // 處理減
        case 'COUNT_DECREMENT':
          return {
            ...state,
            count: state.count - action.number
          }
        // 處理加        
        case 'COUNT_INCREMENT':
          return {
            ...state,
            count: state.count + action.number
          }
        default:
          return state
      }
    }
?
    // 創建一個store
    const store = createStore(changeState)
    // 定義一個方法用于渲染計數器的dom
    const renderCount = () => {
      const countDom = document.querySelector('#count')
      countDom.innerHTML = store.getState().count
    }
    // 初次渲染數據
    renderCount()
    // 監聽,只要有dispatch,renderCount就會自動運行
    store.subscribe(renderCount)
  </script>
</body>
</html>

使用Redux框架

Redux的流程:

1.store通過reducer創建了初始狀態

2.view通過store.getState()獲取到了store中保存的state掛載在了自己的狀態上

3.用戶產生了操作,調用了actions 的方法

4.actions的方法被調用,創建了帶有標示性信息的action

5.actions將action通過調用store.dispatch方法發送到了reducer中

6.reducer接收到action并根據標識信息判斷之后返回了新的state

7.store的state被reducer更改為新state的時候,store.subscribe方法里的回調函數會執行,此時就可以通知view去重新獲取state

Reducer必須是一個純函數:

Reducer 函數最重要的特征是,它是一個純函數。也就是說,只要是同樣的輸入,必定得到同樣的輸出。Reducer不是只有Redux里才有,之前學的數組方法reduce, 它的第一個參數就是一個reducer

純函數是函數式編程的概念,必須遵守以下一些約束。

  • 不得改寫參數

  • 不能調用系統 I/O 的API

  • 不能調用Date.now()或者Math.random()等不純的方法,因為每次會得到不一樣的結果

由于 Reducer 是純函數,就可以保證同樣的State,必定得到同樣的 View。但也正因為這一點,Reducer 函數里面不能改變 State,必須返回一個全新的對象,請參考下面的寫法。

// State 是一個對象
function reducer(state = defaultState, action) {
  return Object.assign({}, state, { thingToChange });
  // 或者
  return { ...state, ...newState };
}
?
// State 是一個數組
function reducer(state = defaultState, action) {
  return [...state, newItem];
}

最好把 State 對象設成只讀。要得到新的 State,唯一辦法就是生成一個新對象。這樣的好處是,任何時候,與某個 View 對應的 State 總是一個不變(immutable)的對象。

我們可以通過在createStore中傳入第二個參數來設置默認的state,但是這種形式只適合于只有一個reducer的時候。

劃分reducer:

因為一個應用中只能有一個大的state,這樣的話reducer中的代碼將會特別特別的多,那么就可以使用combineReducers方法將已經分開的reducer合并到一起

注意: 1、分離reducer的時候,每一個reducer維護的狀態都應該不同 2、通過store.getState獲取到的數據也是會按照reducers去劃分的 3、劃分多個reducer的時候,默認狀態只能創建在reducer中,因為劃分reducer的目的,4、就是為了讓每一個reducer都去獨立管理一部分狀態

最開始一般基于計數器的例子講解redux的基本使用即可。

關于action/reducer/store的更多概念,請查看官網

Redux異步

通常情況下,action只是一個對象,不能包含異步操作,這導致了很多創建action的邏輯只能寫在組件中,代碼量較多也不便于復用,同時對該部分代碼測試的時候也比較困難,組件的業務邏輯也不清晰,使用中間件了之后,可以通過actionCreator異步編寫action,這樣代碼就會拆分到actionCreator中,可維護性大大提高,可以方便于測試、復用,同時actionCreator還集成了異步操作中不同的action派發機制,減少編碼過程中的代碼量

常見的異步庫:

  • Redux-thunk(就講這個)

  • Redux-saga

  • Redux-effects

  • Redux-side-effects

  • Redux-loop

  • Redux-observable

基于Promise的異步庫:

  • Redux-promise

  • Redux-promises

  • Redux-simple-promise

  • Redux-promise-middleware

容器組件(Smart/Container Components)和展示組件(Dumb/Presentational Components)

使用react-redux

可以先結合context來手動連接react和redux。

react-redux提供兩個核心的api:

  • Provider: 提供store

  • connect: 用于連接容器組件和展示組件

  1. Provider 根據單一store原則 ,一般只會出現在整個應用程序的最頂層。

  2. connect 語法格式為
    connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)(component)
    一般來說只會用到前面兩個,它的作用是:

  • store.getState()的狀態轉化為展示組件的props

  • actionCreators轉化為展示組件props上的方法

特別強調: 官網上的第二個參數為mapDispatchToProps, 實際上就是actionCreators

只要上層中有Provider組件并且提供了store, 那么,子孫級別的任何組件,要想使用store里的狀態,都可以通過connect方法進行連接。如果只是想連接actionCreators,可以第一個參數傳遞為null

React Router

React Router現在的版本是5, 于2019年3月21日搞笑的發布,搞笑的官網鏈接, 本來是要發布4.4的版本的,結果成了5。從4開始,使用方式相對于之前版本的思想有所不同。之前版本的思想是傳統的思想:路由應該統一在一處渲染, Router 4之后是這樣的思想:一切皆組件

React Router包含了四個包:

主要使用react-router-dom

使用方式

正常情況下,直接按照官網的demo就理解 路由的使用方式,有幾個點需要特別的強調:

  • Route組件的exact屬性

exact屬性標識是否為嚴格匹配, 為true是表示嚴格匹配,為false時為正常匹配。

  • Route組件的render屬性而不是component屬性

怎么在渲染組件的時候,對組件傳遞屬性呢?使用component的方式是不能直接在組件上添加屬性的。所以,React Router的Route組件提供了另一種渲染組件的方式render, 這個常用于頁面組件級別的權限管理。

  • 路由的參數傳遞與獲取

  • Switch組件

總是渲染第一個匹配到的組件

  • 處理404與默認頁

  • withRoute高階組件的使用

  • 管理一個項目路由的方法

  • code spliting

  • HashRouter和BrowserRouter的區別,前端路由和后端路由的區別。這個在Vue里應該有講過了。

React Router基本原理

React Router甚至大部分的前端路由都是依賴于history.js的,它是一個獨立的第三方js庫。可以用來兼容在不同瀏覽器、不同環境下對歷史記錄的管理,擁有統一的API。

  • 老瀏覽器的history: 通過hash來存儲在不同狀態下的history信息,對應createHashHistory,通過檢測location.hash的值的變化,使用location.replace方法來實現url跳轉。通過注冊監聽window對象上的hashChange事件來監聽路由的變化,實現歷史記錄的回退。

  • 高版本瀏覽器: 利用HTML5里面的history,對應createBrowserHistory, 使用包括pushStatereplaceState方法來進行跳轉。通過注冊監聽window對象上的popstate事件來監聽路由的變化,實現歷史記錄的回退。

  • node環境下: 在內存中進行歷史記錄的存儲,對應createMemoryHistory。直接在內存里pushpop狀態。


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