生命周期
定制shouldComponentUpdate
shouldComponentUpdate(){
console.log(this.state.num) // 1 2 3 4 5....每次都正常打印
if(this.state.num%5== 0){
return true
}
return false
}
--------------------------------
shouldComponentUpdate(nextProps,nextState){
console.log(nextProps,nextState)
console.log(this.props,this.state)
if(nextState.num%5== 0){ // 如果下一個狀態%5=0
return true
}
return false
}
SetState
setState有隊列的概念,對狀態的更新是異步的,比如在一個事件函數中更新多次state,render只執行一次。不要不通過setState直接修改state,每次調用setState都會塞到隊列中。render中不要執行setState,因為setState會執行render,這樣操作會導致循環調用。
調用setState之后,它會把當前要修改的狀態存儲在隊列中,最終會調用隊列更新的方法。
高階組件
- 高階組件就是一個函數,傳給它一個組件,它返回一個新的組件。
- 高階組件的作用:就是為了組件之間的代碼復用。組件可能有著某些相同的邏輯,把這些邏輯抽離出來,放到高階組件中進行復用。
- 高階組件包裝的新組件和原來組件之間通過 props 傳遞信息,減少代碼的重復程度。
function heighCom(Com,name){
class NewComponent extends React.Component(){
constructor(props){
super(props)
this.state = {
data:null
}
}
componentDidMount(){
this.setState({
data : name
})
}
render(){
<Com data={this.state.data}></Com>
}
}
return NewComponent
}
context
- 一個組件的 context 只有它的子組件能夠訪問,它的父組件是不能訪問到的;
- 要給父組件設置 context,childContextTypes 是必寫的;
- 子組件要獲取context內容,必須寫contextTypes聲明和驗證你需要獲取的狀態的類型。
class Index extends Component {
//要給組件設置 context,childContextTypes 是必寫的
static childContextTypes = {
themeColor: PropTypes.string
}
// 初始化themeColor狀態
constructor () {
super()
this.state = { themeColor: 'red' }
}
// 設置 context 的過程
getChildContext () {
return { themeColor: this.state.themeColor }
}
}
----------------------------------------------------------
//子組件要獲取context內容,必須寫contextTypes聲明和驗證你需要獲取的狀態的類型
class Title extends Component {
static contextTypes = {
themeColor: PropTypes.string
}
render () {
return (
<h1 style={{ color: this.context.themeColor }}>React.js 小書標題</h1>
)
}
}
context的好處和壞處 : 如果一個組件設置了 context,那么它的子組件都可以直接訪問到里面的內容,它就像這個組件為根的子樹的全局變量。任意深度的子組件都可以通過 contextTypes 來聲明你想要的 context 里面的哪些狀態,然后可以通過 this.context 訪問到那些狀態。
context 打破了組件和組件之間通過 props 傳遞數據的規范,極大地增強了組件之間的耦合性。而且,就如全局變量一樣,context 里面的數據能被隨意接觸就能被隨意修改,每個組件都能夠改 context 里面的內容會導致程序的運行不可預料。
純函數(Pure Function)簡介&&為什么reducer必須是純函數
一個函數的返回結果只依賴于它的參數,并且在執行過程里面沒有副作用,我們就把這個函數叫做純函數。
- 函數的返回結果只依賴于它的參數。
- 函數執行過程里面沒有副作用:一個函數執行過程對產生了外部可觀察的變化那么就說這個函數是有副作用的。
純函數很嚴格,也就是說你幾乎除了計算數據以外什么都不能干,計算的時候還不能依賴除了函數參數以外的數據。
總結:
一個函數的返回結果只依賴于它的參數,并且在執行過程里面沒有副作用,我們就把這個函數叫做純函數。
為什么要煞費苦心地構建純函數?因為純函數非常“靠譜”,執行一個純函數你不用擔心它會干什么壞事,它不會產生不可預料的行為,也不會對外部產生影響。不管何時何地,你給它什么它就會乖乖地吐出什么。如果你的應用程序大多數函數都是由純函數組成,那么你的程序測試、調試起來會非常方便。
為什么reducer必須是純函數
Redux接收一個給定的state(對象),然后通過循環將state的每一部分傳遞給每個對應的reducer。如果有發生任何改變,reducer將返回一個新的對象。如果不發生任何變化,reducer將返回舊的state。
Redux只通過比較新舊兩個對象的存儲位置來比較新舊兩個對象是否相同(譯者注:也就是Javascript對象淺比較)。如果你在reducer內部直接修改舊的state對象的屬性值,那么新的state和舊的state將都指向同一個對象。因此Redux認為沒有任何改變,返回的state將為舊的state。
因為比較兩個Javascript對象所有的屬性是否相同的的唯一方法是對它們進行深比較。但是深比較在真實的應用當中代價昂貴,因為通常js的對象都很大,同時需要比較的次數很多。
因此一個有效的解決方法是作出一個規定:無論何時發生變化時,開發者都要創建一個新的對象,然后將新對象傳遞出去。同時,當沒有任何變化發生時,開發者發送回舊的對象。也就是說,新的對象代表新的state。
必須注意到你只能使用slice(譯者注:此處slice類似數組的slice方法,具體可以使用本文例子中解構賦值等方法進行slice)或者類似的機制去復制舊的值到新的對象里。
現在使用了新的策略之后,你能夠比較兩個對象通過使用!==比較兩個對象的存儲位置而不是比較兩個對象的所有屬性。同時當兩個對象不同的時候,你就能知道新的對象已經改變了舊的state(也就是說,JavaScript對象當中的某些屬性的值發生了變化)。這正是Redux所采取的策略。
這就是為什么Redux需要reducers是純函數的原因!
為什么Redux需要reducers是純函數
redux
- 專注于狀態管理的庫
-
單一狀態,單向數據流
Redux 是一種架構模式(Flux 架構的一種變種),它不關注你到底用什么庫,你可以把它應用到 React 和 Vue,甚至跟 jQuery 結合都沒有問題。而 React-redux 就是把 Redux 這種架構模式和 React.js 結合起來的一個庫,就是 Redux 架構在 React.js 中的體現。
1、抽離出 store
function createStore(state,reducer){
const getState = ()=> state
const dispatch = (action) => reducer(state,action)
return {getState,dispatch}
}
//es5便于理解
function dispatch(action) {
return reducer(state, action);
};
- createStore: 會返回一個對象,這個對象包含兩個方法 getState 和 dispatch。getState 用于獲取 state 數據,其實就是簡單地把 state 參數返回。
- dispatch: 用于修改數據,和以前一樣會接受action ,然后它會把 state 和action 一并傳給 reducer ,那么reducer 就可以根據 action 來修改 state 了。
監控數據變化
上面的代碼有一個問題,我們每次通過 dispatch 修改數據的時候,其實只是數據發生了變化,如果我們不手動調用 renderApp,頁面上的內容是不會發生變化的。但是我們總不能每次 dispatch 的時候都手動調用一下 renderApp,我們肯定希望數據變化的時候程序能夠智能一點地自動重新渲染數據,而不是手動調用。
你說這好辦,往 dispatch里面加 renderApp 就好了,但是這樣 createStore 就不夠通用了。我們希望用一種通用的方式“監聽”數據變化,然后重新渲染頁面,這里要用到觀察者模式。
function createStore(state,reducer){
let listeners = []
let subscribe = (listener)=>{listeners.push(listener)}
let getState = ()=>state
let dispatch = (action)=>{
reducer(action,state)
listeners.forEach( listener => {listener()} )
}
return {getState ,subscribe ,dispatch}
}
我們修改了 dispatch,每次當它被調用的時候,除了會調用 reducer 進行數據的修改,還會遍歷 listeners 數組里面的函數,然后一個個地去調用。相當于我們可以通過 subscribe 傳入數據變化的監聽函數,每當 dispatch 的時候,監聽函數就會被調用,這樣我們就可以在每當數據變化時候進行重新渲染:
const store = createStore(appState, reducer)
store.subscribe(() => renderApp(store.getState()))
renderApp(store.getState()) // 首次渲染頁面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小書》' }) // 修改標題文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標題顏色
// ...后面不管如何 store.dispatch,都不需要重新調用 renderApp
我的小結:
- subscribe : 訂閱,將參數push到處理數組
- getState : 返回state
- dispatch : 發布,遍歷事件處理數組,并執行;調用reducer,傳入state和action
共享結構的對象提高性能
const obj = { a: 1, b: 2}
const obj2 = { ...obj } // => { a: 1, b: 2 }
const obj2 = { ...obj } 其實就是新建一個對象 obj2,然后把 obj 所有的屬性都復制到 obj2 里面,相當于對象的淺復制。上面的 obj 里面的內容和 obj2 是完全一樣的,但是卻是兩個不同的對象。
const obj = { a: 1, b: 2}
// { a: 1, b: 3, c: 4 },覆蓋了 b,新增了 c
const obj2 = { ...obj, b: 3, c: 4}
我們可以把這種特性應用在 state 的更新上,我們禁止直接修改原來的對象,一旦你要修改某些東西,你就得把修改路徑上的所有對象復制一遍,例如,我們不寫下面的修改代碼:
appState.title.text = '《React.js 小書》'
取而代之的是,我們新建一個 appState,新建 appState.title,新建 appState.title.text:
let newAppState = { // 新建一個 newAppState
...appState, // 復制 appState 里面的內容
title: { // 用一個新的對象覆蓋原來的 title 屬性
...appState.title, // 復制原來 title 對象里面的內容
text: '《React.js 小書》' // 覆蓋 text 屬性
}
}
修改數據的時候就把修改路徑都復制一遍,但是保持其他內容不變,最后的所有對象具有某些不變共享的結構(例如上面三個對象都共享 content 對象)。大多數情況下我們可以保持 50% 以上的內容具有共享結構,這種操作具有非常優良的特性,我們可以用它來優化上面的渲染性能。
優化性能
我們修改 reducer,讓它修改數據的時候,并不會直接修改原來的數據 state,而是產生上述的共享結構的對象:
function reducer (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 // 沒有修改,返回原來的對象
}
}
代碼稍微比原來長了一點,但是是值得的。每次需要修改的時候都會產生新的對象,并且返回。而如果沒有修改(在 default 語句中)則返回原來的 state 對象。
因為 reducer 不會修改原來對象了,而是返回對象,所以我們需要修改一下 createStore。讓它用每次 reducer(state, action) 的調用結果覆蓋原來的 state:
function createStore (reducer) {
let state = null
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
//reducer只是返回對象,所以需要用每次 reducer(state, action) 的調用結果覆蓋原來的 state
state = reducer(state, action)
listeners.forEach((listener) => listener())
}
dispatch({}) // 初始化 state
return { getState, dispatch, subscribe }
}
我們成功地把不必要的頁面渲染優化掉了,問題解決。另外,并不需要擔心每次修改都新建共享結構對象會有性能、內存問題,因為構建對象的成本非常低,而且我們最多保存兩個對象引用(oldState 和 newState),其余舊的對象都會被垃圾回收掉。
我的小結:
修改數據時,禁止直接修改原來的對象,一旦你要修改某些東西,你就得把修改路徑上的所有對象復制一遍,但是保持其他內容不變,最后的所有對象具有某些不變共享的結構,多數情況下我們可以保持 50% 以上的內容具有共享結構,這種操作具有非常優良的特性。
Redux只通過比較新舊兩個對象的存儲位置來比較新舊兩個對象是否相同(譯者注:也就是Javascript對象淺比較)。如果你在reducer內部直接修改舊的state對象的屬性值,那么新的state和舊的state將都指向同一個對象。因此Redux認為沒有任何改變,返回的state將為舊的state。
state = reducer(state, action) // 覆蓋原對象
reducer總結:
createStore 接受一個叫 reducer 的函數作為參數,這個函數規定是一個純函數,它接受兩個參數,一個是 state,一個是 action。
如果沒有傳入 state 或者 state 是 null,那么它就會返回一個初始化的數據。如果有傳入 state 的話,就會根據 action 來“修改“數據,但其實它沒有、也規定不能修改 state,而是要通過上節所說的把修改路徑的對象都復制一遍,然后產生一個新的對象返回。如果它不能識別你的 action,它就不會產生新的數據,而是(在 default 內部)把 state 原封不動地返回。
!!!reducer 是不允許有副作用的。你不能在里面操作 DOM,也不能發 Ajax 請求,更不能直接修改 state,它要做的僅僅是 —— 初始化和計算新的 state。
還要注意:為什么reducer必須是純函數?在純函數章節已經做了總結。
redux總結
不知不覺地,到這里大家不僅僅已經掌握了 Redux,而且還自己動手寫了一個 Redux。我們從一個非常原始的代碼開始,不停地在發現問題、解決問題、優化代碼的過程中進行推演,最后把 Redux 模式自己總結出來了。這就是所謂的 Redux 模式,我們再來回顧一下這幾節我們到底干了什么事情。
我們從一個簡單的例子的代碼中發現了共享的狀態如果可以被任意修改的話,那么程序的行為將非常不可預料,所以我們提高了修改數據的門檻:你必須通過 dispatch 執行某些允許的修改操作,而且必須大張旗鼓的在 action 里面聲明。
這種模式挺好用的,我們就把它抽象出來一個 createStore,它可以產生 store,里面包含 getState 和 dispatch 函數,方便我們使用。
后來發現每次修改數據都需要手動重新渲染非常麻煩,我們希望自動重新渲染視圖。所以后來加入了訂閱者模式,可以通過 store.subscribe 訂閱數據修改事件,每次數據更新的時候自動重新渲染視圖。
接下來我們發現了原來的“重新渲染視圖”有比較嚴重的性能問題,我們引入了“共享結構的對象”來幫我們解決問題,這樣就可以在每個渲染函數的開頭進行簡單的判斷避免沒有被修改過的數據重新渲染。
我們優化了 stateChanger 為 reducer,定義了 reducer 只能是純函數,功能就是負責初始 state,和根據 state 和 action 計算具有共享結構的新的 state。
createStore 現在可以直接拿來用了,套路就是:
// 定一個 reducer
function reducer (state, action) {
/* 初始化 state 和 switch case */
}
// 生成 store
const store = createStore(reducer)
// 監聽數據變化重新渲染頁面
store.subscribe(() => renderApp(store.getState()))
// 首次渲染頁面
renderApp(store.getState())
// 后面可以隨意 dispatch 了,頁面自動更新
store.dispatch(...)
Redux和 React.js 一點關系都沒有!!!接下來我們要把 React.js 和 Redux 結合起來,用 Redux 模式幫助管理 React.js 的應用狀態。
react-redux
前端中應用的狀態存在的問題:一個狀態可能被多個組件依賴或者影響,而 React.js 并沒有提供好的解決方案,我們只能把狀態提升到依賴或者影響這個狀態的所有組件的公共父組件上,我們把這種行為叫做狀態提升。但是需求不停變化,共享狀態沒完沒了地提升也不是辦法。
后來我們在 React.js 的 context 中提出,我們可用把共享狀態放到父組件的 context 上,這個父組件下所有的組件都可以從 context 中直接獲取到狀態而不需要一層層地進行傳遞了。但是直接從 context 里面存放、獲取數據增強了組件的耦合性;并且所有組件都可以修改 context 里面的狀態就像誰都可以修改共享狀態一樣,導致程序運行的不可預料。
解決思路:把 context 和 store 結合起來。畢竟 store 的數據不是誰都能修改,而是約定只能通過 dispatch 來進行修改,這樣的話每個組件既可以去 context 里面獲取 store 從而獲取狀態,又不用擔心它們亂改數據了。
到底什么樣的組件才叫復用性強的組件。如果一個組件對外界的依賴過于強,那么這個組件的移植性會很差,就像這些嚴重依賴 context 的組件一樣。
如果一個組件的渲染只依賴于外界傳進去的 props 和自己的 state,而并不依賴于其他的外界的任何數據,也就是說像純函數一樣,給它什么,它就吐出(渲染)什么出來。這種組件的復用性是最強的,別人使用的時候根本不用擔心任何事情,只要看看 PropTypes 它能接受什么參數,然后把參數傳進去控制它就行了。
我們需要高階組件幫助我們從 context 取數據,我們也需要寫 Dumb 組件幫助我們提高組件的復用性。所以我們盡量多地寫 Dumb 組件,然后用高階組件把它們包裝一層,高階組件和 context 打交道,把里面數據取出來通過 props 傳給 Dumb 組件。
實現過程:
1、connect 現在是接受一個參數 mapStateToProps,然后返回一個函數,這個返回的函數才是高階組件。它會接受一個組件作為參數,然后用 Connect 把組件包裝以后再返回。
//connect高階組件,從context取出store,將state傳給mapStateToProps
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
}
2、高階組件用法
const mapStateToProps = (state) => {
return {
themeColor: state.themeColor,
themeName: state.themeName,
fullName: `${state.firstName} ${state.lastName}`
}
}
const mapDispatchToProps = (dispatch) => {
return {
onSwitchColor: (color) => {
dispatch({ type: 'CHANGE_COLOR', themeColor: color })
}
}
}
Header = connect(mapStateToProps)(Header)
我們在 Connect 組件的 constructor 里面初始化了 state.allProps,它是一個對象,用來保存需要傳給被包裝組件的所有的參數。生命周期 componentWillMount 會調用調用 _updateProps 進行初始化,然后通過 store.subscribe 監聽數據變化重新調用 _updateProps。
為了讓 connect 返回新組件和被包裝的組件使用參數保持一致,我們會把所有傳給 Connect 的 props 原封不動地傳給 WrappedComponent。所以在 _updateProps 里面會把 stateProps 和 this.props 合并到 this.state.allProps 里面,再通過 render 方法把所有參數都傳給 WrappedComponent。
mapStateToProps 也發生點變化,它現在可以接受兩個參數了,我們會把傳給 Connect 組件的 props 參數也傳給它,那么它生成的對象配置性就更強了,我們可以根據 store 里面的 state 和外界傳入的 props 生成我們想傳給被包裝組件的參數。
現在已經很不錯了,Header.js 和 Content.js 的代碼都大大減少了,并且這兩個組件 connect 之前都是 Dumb 組件。接下來會繼續重構 ThemeSwitch。
Provider
我們要把 context 相關的代碼從所有業務組件中清除出去,額外構建一個組件來做這種臟活,然后讓這個組件成為組件樹的根節點,那么它的子組件都可以獲取到 context 了。
Provider 做的事情也很簡單,它就是一個容器組件,會把嵌套的內容原封不動作為自己的子組件渲染出來。它還會把外界傳給它的 props.store 放到 context,這樣子組件 connect 的時候都可以獲取到。
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>
)
}
}
總結:
- 到這里大家已經掌握了 React-redux 的基本用法和概念,并且自己動手實現了一個 React-redux,我們回顧一下這幾節都干了什么事情。
- React.js 除了狀態提升以外并沒有更好的辦法幫我們解決組件之間共享狀態的問題,而使用 context 全局變量讓程序不可預測。通過 Redux 的章節,我們知道 store 里面的內容是不可以隨意修改的,而是通過 dispatch 才能變更里面的 state。所以我們嘗試把 store 和 context 結合起來使用,可以兼顧組件之間共享狀態問題和共享狀態可能被任意修改的問題。
- 第一個版本的 store 和 context 結合有諸多缺陷,有大量的重復邏輯和對 context 的依賴性過強。我們嘗試通過構建一個高階組件 connect 函數的方式,把所有的重復邏輯和對 context 的依賴放在里面 connect 函數里面,而其他組件保持 Pure(Dumb) 的狀態,讓 connect 跟 context 打交道,然后通過 props 把參數傳給普通的組件。
- 而每個組件需要的數據和需要觸發的 action 都不一樣,所以調整 connect,讓它可以接受兩個參數 mapStateToProps 和 mapDispatchToProps,分別用于告訴 connect 這個組件需要什么數據和需要觸發什么 action。
- 最后為了把所有關于 context 的代碼完全從我們業務邏輯里面清除掉,我們構建了一個 Provider 組件。Provider 作為所有組件樹的根節點,外界可以通過 props 給它提供 store,它會把 store 放到自己的 context 里面,好讓子組件 connect 的時候都能夠獲取到。
- 這幾節的成果就是 react-redux.js 這個文件里面的兩個內容:connect 函數和 Provider 容器組件。這就是 React-redux 的基本內容,當然它是一個殘疾版本的 React-redux,很多地方需要完善。例如上幾節提到的性能問題,現在不相關的數據變化的時候其實所有組件都會重新渲染的,這個性能優化留給讀者做練習。
- 通過這種方式大家不僅僅知道了 React-redux 的基礎概念和用法,而且還知道這些概念到底是解決什么問題,為什么 React-redux 這么奇怪,為什么要 connect,為什么要 mapStateToProps 和 mapDispatchToProps,什么是 Provider,我們通過解決一個個問題就知道它們到底為什么要這么設計的了。
使用真正的 Redux 和 React-redux
現在 make-react-redux
工程代碼中的 Redux 和 React-redux 都是我們自己寫的,現在讓我們來使用真正的官方版本的 Redux 和 React-redux。
在工程目錄下使用 npm 安裝 Redux 和 React-redux 模塊:
npm install redux react-redux --save
把 src/
目錄下 Header.js
、ThemeSwitch.js
、Content.js
的模塊導入中的:
import { connect } from './react-redux'
改成:
import { connect } from 'react-redux'
也就是本來從本地 ./react-redux
導入的 connect
改成從第三方 react-redux
模塊中導入。
修改 src/index.js
,把前面部分的代碼調整為:
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import Header from './Header'
import Content from './Content'
import './index.css'
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)
...
我們刪除了自己寫的 createStore
,改成使用第三方模塊 redux
的 createStore
;Provider
本來從本地的 ./react-redux
引入,改成從第三方 react-redux
模塊中引入。其余代碼保持不變。
接著刪除 src/react-redux.js
,它的已經用處不大了。最后啟動工程 npm start
:
可以看到我們原來的業務代碼其實都沒有太多的改動,實際上我們實現的 redux
和 react-redux
和官方版本在該場景的用法上是兼容的。接下來的章節我們都會使用官方版本的 redux
和 react-redux
。