組件通信
父組件與子組件通信
父組件將自己的狀態傳遞給子組件,子組件當做屬性來接收,當父組件更改自己狀態的時候,子組件接收到的屬性就會發生改變
父組件利用
ref
對子組件做標記,通過調用子組件的方法以更改子組件的狀態,也可以調用子組件的方法..
子組件與父組件通信
- 父組件將自己的某個方法傳遞給子組件,在方法里可以做任意操作,比如可以更改狀態,子組件通過
this.props
接收到父組件的方法后調用。
跨組件通信
在react沒有類似vue中的事件總線來解決這個問題,我們只能借助它們共同的父級組件來實現,將非父子關系裝換成多維度的父子關系。react提供了context
api來實現跨組件通信, React 16.3之后的context
api較之前的好用。
實例,使用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>©版權所有 千鋒教育 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.js
,Ember.js
等一系列MVC
架構的前端JS框架。
其實Flux
在React
里的應用就類似于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的流程:
組件獲取到store中保存的數據掛載在自己的狀態上
用戶產生了操作,調用actions的方法
actions接收到了用戶的操作,進行一系列的邏輯代碼、異步操作
然后actions會創建出對應的action,action帶有標識性的屬性
actions調用dispatcher的dispatch方法將action傳遞給dispatcher
dispatcher接收到action并根據標識信息判斷之后,調用store的更改數據的方法
store的方法被調用后,更改狀態,并觸發自己的某一個事件
store更改狀態后事件被觸發,該事件的處理程序會通知view去獲取最新的數據
Redux
React 只是 DOM 的一個抽象層,并不是 Web 應用的完整解決方案。有兩個方面,它沒涉及。
代碼結構
組件之間的通信
2013年 Facebook 提出了 Flux 架構的思想,引發了很多的實現。2015年,Redux 出現,將 Flux 與函數式編程結合一起,很短時間內就成為了最熱門的前端架構。
如果你不知道是否需要 Redux,那就是不需要它
只有遇到 React 實在解決不了的問題,你才需要 Redux
簡單說,如果你的UI層非常簡單,沒有很多互動,Redux 就是不必要的,用了反而增加復雜性。
用戶的使用方式非常簡單
用戶之間沒有協作
不需要與服務器大量交互,也沒有使用 WebSocket
視圖層(View)只從單一來源獲取數據
需要使用Redux的項目:
用戶的使用方式復雜
不同身份的用戶有不同的使用方式(比如普通用戶和管理員)
多個用戶之間可以協作
與服務器大量交互,或者使用了WebSocket
View要從多個來源獲取數據
從組件層面考慮,什么樣子的需要Redux:
某個組件的狀態,需要共享
某個狀態需要在任何地方都可以拿到
一個組件需要改變全局狀態
一個組件需要改變另一個組件的狀態
Redux的設計思想:
Web 應用是一個狀態機,視圖與狀態是一一對應的。
所有的狀態,保存在一個對象里面(唯一數據源)。
注意: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: 用于連接容器組件和展示組件
Provider 根據單一store原則 ,一般只會出現在整個應用程序的最頂層。
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高階組件的使用
管理一個項目路由的方法
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
, 使用包括pushState
,replaceState
方法來進行跳轉。通過注冊監聽window
對象上的popstate
事件來監聽路由的變化,實現歷史記錄的回退。node環境下: 在內存中進行歷史記錄的存儲,對應
createMemoryHistory
。直接在內存里push
和pop
狀態。