1.要實現redux,先搞清楚context
React.js 的 context 就是這麼一個東西,某個組件只要往自己的 context 裏面放了某些狀態,這個組件之下的所有子組件都直接訪問這個狀態而不需要通過中間組件的傳遞。一個組件的 context 只有它的子組件能夠訪問,它的父組件是不能訪問到的
//現在我們修改 Index,讓它往自己的 context 裏面放一個 themeColor
class Index extends Component {
static childContextTypes = {//要給組件設置 context,那麼 childContextTypes 是必寫的
themeColor: PropTypes.string
}
constructor () {
super()
this.state = { themeColor: 'red' }
}
getChildContext () {
return { themeColor: this.state.themeColor }
}
render () {
return (
<div>
<Header />
<Main />
</div>
)
}
}
//子組件直接獲取context裏面的東西
//子組件要獲取 context 裏面的內容的話,就必須寫 contextTypes 來聲明和驗證你需要獲取的狀態的類型,
//它也是必寫的
class Title extends Component {
static contextTypes = {
themeColor: PropTypes.string
}
render () {
return (
<h1 style={{ color: this.context.themeColor }}>React.js</h1>
)
}
}
2.實現共享狀態優化
context 打破了組件和組件之間通過 props 傳遞數據的規範,極大地增強了組件之間的耦合性。而且,就如全局變量一樣,context 裏面的數據能被隨意接觸就能被隨意修改,每個組件都能夠改 context 裏面的內容會導致程序的運行不可預料,這時我們就需要規範對共享狀態的修改
1.假設使用的是一個共享狀態 appState,每個人都可以修改它
2.所有對共享狀態的操作都是不可預料的(某個模塊 appState.title = null 你一點意見都沒有),出現問題的時候 debug 起來就非常困難
3.如果所有對數據的操作必須通過 dispatch 函數。它接受一個參數 action,這個 action 是一個普通的 JavaScript 對象,裏面必須包含一個 type 字段來聲明你到底想幹什麼,那就好操作了
4.我們可以把appState 和 dispatch抽離出來結合到一起形成store,構建一個函數 createStore,用來專門生產這種 state 和 dispatch 的集合
/*createStore 接受兩個參數,一個是表示應用程序狀態的 state;另外一個是 stateChanger,
它來描述應用程序狀態會根據 action 發生什麼變化*/
function createStore (state, stateChanger) {
const getState = () => state
const dispatch = (action) => stateChanger(state, action)
return { getState, dispatch }
}
renderApp(store.getState()) // 首次渲染頁面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 》' }) // 修改標題文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標題顏色
renderApp(store.getState()) // 把新的數據渲染到頁面上
3.實現Redux數據刷新優化
1.細上面代碼中更改數據後需要手動調用renderapp刷新,這裏我們可以用觀察者模式優化刷新
function createStore (state, stateChanger) {
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
stateChanger(state, action)
listeners.forEach((listener) => listener())//dispatch後調用我傳入subscribe的刷新方式
}
return { getState, dispatch, subscribe }
}
//可以用同一個APPstate去渲染不同的頁面
const store = createStore(appState, stateChanger)
store.subscribe(() => renderApp(store.getState()))
store.subscribe(() => renderApp2(store.getState()))
store.subscribe(() => renderApp3(store.getState()))
2.避免重複渲染優化 這裏直接引用鬍子大叔的優化
function createStore (state, stateChanger) {
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
state = stateChanger(state, action) // 覆蓋原對象
listeners.forEach((listener) => listener())
}
return { getState, dispatch, subscribe }
}
function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 沒有傳入,所以加了默認參數 oldAppState = {}
if (newAppState === oldAppState) return // 數據沒有變化就不渲染了
console.log('render app...')
renderTitle(newAppState.title, oldAppState.title)
renderContent(newAppState.content, oldAppState.content)
}
function renderTitle (newTitle, oldTitle = {}) {
if (newTitle === oldTitle) return // 數據沒有變化就不渲染了
console.log('render title...')
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = newTitle.text
titleDOM.style.color = newTitle.color
}
function renderContent (newContent, oldContent = {}) {
if (newContent === oldContent) return // 數據沒有變化就不渲染了
console.log('render content...')
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = newContent.text
contentDOM.style.color = newContent.color
}
let appState = {
title: {
text: 'React.js 小書',
color: 'red',
},
content: {
text: 'React.js 小書內容',
color: 'blue'
}
}
function stateChanger (state, action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
return { // 構建新的對象並且返回
...state,
title: {
...state.title,
text: action.text
}
}
case 'UPDATE_TITLE_COLOR':
return { // 構建新的對象並且返回
...state,
title: {
...state.title,
color: action.color
}
}
default:
return state // 沒有修改,返回原來的對象
}
}
const store = createStore(appState, stateChanger)
let oldState = store.getState() // 緩存舊的 state
store.subscribe(() => {
const newState = store.getState() // 數據可能變化,獲取新的 state
renderApp(newState, oldState) // 把新舊的 state 傳進去渲染
oldState = newState // 渲染完以後,新的 newState 變成了舊的 oldState,等待下一次數據變化重新渲染
})
renderApp(store.getState()) // 首次渲染頁面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小書》' }) // 修改標題文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標題顏色
4.Reducer
其實 appState 和 stateChanger 可以合併到一起去
1將appstate放入statechanger
function stateChanger (state, action) {
if (!state) {
return {
title: {
text: 'React.js 小書',
color: 'red',
},
content: {
text: 'React.js 小書內容',
color: 'blue'
}
}
}
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
return {
...state,
title: {
...state.title,
text: action.text
}
}
case 'UPDATE_TITLE_COLOR':
return {
...state,
title: {
...state.title,
color: action.color
}
}
default:
return state
}
}
2.creactstore的參數就會被優化爲一個
function createStore (stateChanger) {
let state = null
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
state = stateChanger(state, action)
listeners.forEach((listener) => listener())
}
dispatch({}) // 初始化 state
return { getState, dispatch, subscribe }
}
3.最後我們規定createStore參數的名字爲reducer,且reducer是一個純函數
reducer 是不允許有副作用的。你不能在裏面操作 DOM,也不能發 Ajax 請求,更不能直接修改 state,它要做的僅僅是 —— 初始化和計算新的 state
// 定一個 reducer
function reducer (state, action) {
/* 初始化 state 和 switch case */
}
// 生成 store
const store = createStore(reducer)
// 監聽數據變化重新渲染頁面
store.subscribe(() => renderApp(store.getState()))
// 首次渲染頁面
renderApp(store.getState())
// 後面可以隨意 dispatch 了,頁面自動更新
store.dispatch(...)
5.React-redux中的store和context
React.js 的 context 中提出,我們可用把共享狀態放到父組件的 context 上,這個父組件下所有的組件都可以從 context 中直接獲取到狀態而不需要一層層地進行傳遞了,但組件對其的改動會讓context不可預料。 store 的數據不是誰都能修改,而是約定只能通過 dispatch 來進行修改,這樣的話每個組件既可以去 context 裏面獲取 store 從而獲取狀態,又不用擔心它們亂改數據了,所以將store和context結合起來
1.構建自己的React-redux
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import Header from './Header'
import Content from './Content'
import './index.css'
function createStore (reducer) {
let state = null
const listeners = [
const subscribe = (listener) => listeners.push(listener)
const getState = () => state//這是函數表達式 調用它時state已經初始化了
const dispatch = (action) => {
state = reducer(state, action)
listeners.forEach((listener) => listener())
}
dispatch({}) // 初始化 state
return { getState, dispatch, subscribe }
}
const themeReducer = (state, action) => {
if (!state) return {
themeColor: 'red'
}
switch (action.type) {
case 'CHANGE_COLOR':
return { ...state, themeColor: action.themeColor }
default:
return state
}
}
const store = createStore(themeReducer)
class Index extends Component {
static childContextTypes = {
store: PropTypes.object
}
getChildContext () {
return { store }//將store放入context
}
render () {
return (
<div>
<Header />
<Content />
</div>
)
}
}
2.子組件獲取context中的配置
class Header extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor () {
super()
this.state = { themeColor: '' }
}
componentWillMount () {
this._updateThemeColor()
}
_updateThemeColor () {
const { store } = this.context//解構賦值取出來
const state = store.getState()
this.setState({ themeColor: state.themeColor })//放到state中來用
}
render () {
return (
<h1 style={{ color: this.state.themeColor }}>React.js 小書</h1>
)
}
}
3.用dispatch去改變配置刷新頁面
//首先配置監聽函數的刷新模式
componentWillMount () {
const { store } = this.context
this._updateThemeColor()//獲取默認數據加載
store.subscribe(() => this._updateThemeColor())//dispatch數據更改後加載
}
//觸發事件
handleSwitchColor (color) {
const { store } = this.context
store.dispatch({
type: 'CHANGE_COLOR',
themeColor: color
})
}
6.React-redux與組件拆分開,讓組件無污染可複用性強
可以把一些可複用的邏輯放在高階組件當中,高階組件包裝的新組件和原來組件之間通過 props 傳遞信息,減少代碼的重複程度,我們需要高階組件幫助我們從 context 取數據,然後用高階組件把它們包裝一層,高階組件和 context 打交道,把裏面數據取出來通過 props 傳給 Dumb 組件
1.這個高階組件起名字叫 connect,因爲它把 Dumb 組件和 context 連接
2.每個傳進去的組件需要 store 裏面的數據都不一樣的,所以除了給高階組件傳入 Dumb 組件以外,還需要告訴高級組件我們需要什麼數據
import React, { Component } from 'react'
import PropTypes from 'prop-types'
//connect 現在是接受一個參數 mapStateToProps,然後返回一個函數,這個返回的函數纔是高階組件
export const connect = (mapStateToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
render () {
const { store } = this.context
let stateProps = mapStateToProps(store.getState())
// {...stateProps} 意思是把這個對象裏面的屬性全部通過 `props` 方式傳遞進去
return <WrappedComponent {...stateProps} />
}
}
return Connect
}
----------
//mapStateToProps爲傳入數據的方式
const mapStateToProps = (state) => {
return {
themeColor: state.themeColor
}
}
Header = connect(mapStateToProps)(Header)
//這裏的mapStateToprops在connect裏面執行並把獲取的數據放到header的props中
3.除了傳遞數據我們還需要高階組件來 dispatch
const mapDispatchToProps = (dispatch) => {
return {
onSwitchColor: (color) => {
dispatch({ type: 'CHANGE_COLOR', themeColor: color })
}
}
}
4.結合起來構建Connect
export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor () {
super()
this.state = {
allProps: {}
}
}
componentWillMount () {
const { store } = this.context
this._updateProps()
store.subscribe(() => this._updateProps())
}
_updateProps () {
const { store } = this.context
let stateProps = mapStateToProps
? mapStateToProps(store.getState(), this.props)
: {} // 防止 mapStateToProps 沒有傳入
let dispatchProps = mapDispatchToProps
? mapDispatchToProps(store.dispatch, this.props)
: {} // 防止 mapDispatchToProps 沒有傳入
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
render () {
return <WrappedComponent {...this.state.allProps} />
}
}
return Connect
}
5.剝離出index.js
class Index extends Component {
static childContextTypes = {
store: PropTypes.object
}
getChildContext () {
return { store }
}//這些都是污染需要剝離
render () {
return (
<div>
<Header />
<Content />
</div>
)
}
}
6.Provider
//將index中污染部分放入Provider,再用成爲index的父組件
export class Provider extends Component {
static propTypes = {
store: PropTypes.object,
children: PropTypes.any
}
static childContextTypes = {
store: PropTypes.object
}
getChildContext () {
return {
store: this.props.store
}
}
render () {
return (
<div>{this.props.children}</div>
)
}
}
...
// 頭部引入 Provider
import { Provider } from './react-redux'
...
// 刪除 Index 裏面所有關於 context 的代碼
class Index extends Component {
render () {
return (
<div>
<Header />
<Content />
</div>
)
}
}
// 把 Provider 作爲組件樹的根節點
ReactDOM.render(
<Provider store={store}>
<Index />
</Provider>,
document.getElementById('root')
)