手挽手帶你學React:四檔(上)一步一步學會react-redux (自己寫個Redux)
手挽手帶你學React入門四檔,用人話教你react-redux,理解redux架構,以及運用在react中。學完這一章,你就可以開始自己的react專案了。
之前在思否看到過某個大神的redux搭建 忘記了大神的名字 這裡只記得內容了 憑藉記憶和當時的學習路線寫下本文 隔空感謝
本人學習react-redux的時候遇到了很多坎,特別是不理解為什麼這麼用,這是什麼東西,用來做什麼。加上各種名詞讓人無法理解,所以這裡我決定用人話,從原理走起,一步一步教大家使用react-redux。
開始之前
本文開始之前,你需要先了解一些東西,當然我會在文中都一一教給大家。
首先你要會React的基礎(這是廢話)
對高階函式有一定的瞭解
有ES6基礎
滿足這三項我們開始往下看。
React上下文 context
react官網說,context這個東西你可能永遠用不到,但是如果你使用了react-redux那麼你還是無意間就使用到了它了。
那麼它是個什麼東西呢?你可以把它理解為全域性的一個可以傳遞資料的東西,畢竟官方都沒給出對於context的定義。
我們直接來看看它是怎麼樣來讓資料可以全域性使用的
在使用 context之前 你需要先認識這幾個東西
首先需要
import PropTypes from 'prop-types';
prop-types這個東西是一個幫你做型別檢測的 所以我們直接就使用好了
接下來是 childContextTypes 這個屬性 它是一個物件,裡面規定我們要通過context來傳遞給下面的屬性名和型別 它通常在父元件中
然後是 getChildContext(){} 這是個規定好的方法 內部retrun一個物件 用來初始化 context的資料
最後是 contextTypes 這個屬性 它也是一個物件,裡面規定我們要接收context來傳遞給下面的屬性名和型別 它通常在子元件中
好了 瞭解了的話 我們開始寫第一個 context了
// App.js import React,{Component} from 'react' import PropTypes from 'prop-types'//引入 export default class App extends Component { static childContextTypes = {//宣告要通過context傳遞的東西 propA: PropTypes.string, methodA: PropTypes.func } getChildContext () {//初始化context return { propA: 'propA', methodA: () => 'methodA' } } constructor(){ super() this.state={ } } componentWillMount(){ // console.log(hashHistory) } render() { return ( <div> <Children /> </div> ) } } // 為了展示效果定義子元件一 class Children extends Component{ constructor(){ super() this.state={ } } static contextTypes = {//規定要接收的東西 propA: PropTypes.string } render(){ console.log(this.context.methodA)// 因為沒有規定 所以現在是 undefined return( <div> <ChildrenTwo /> <h1>{this.context.propA} </h1> {/* 展示propA */} </div> ) } } // 為了展示效果定義子元件二 ChildrenTwo 是 Children的子元件 但是卻通過context拿到了App元件拿過來的值 (越級傳遞) class ChildrenTwo extends Component{ static contextTypes={ methodA: PropTypes.func } constructor(){ super() this.state={ } } render(){ return( <div> <h1>{this.context.methodA()}</h1> {/* 展示methodA */} </div> ) } }
通俗一點來說 一個元件 通過 getChildContext方法來返回一個物件 這就是我們的context 經過了 childContextTypes 聲明後 它的下層元件都可以通過 contextTypes 宣告。然後通過this.context獲取到內容並且使用了。
好了 context這裡就講完了,大家把它放到你大腦的後臺裡執行著,可能在這裡你一頭霧水,講這個幹毛。好的,我們接下來實現一個redux架構!
從零開始Redux
我們建立一個HTML檔案,就叫redux.html 所有東西我們寫在這一個html裡面。
<!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>Document</title> </head> <body> <div id="myHead"></div> <div id="myBody"></div> <!-- 我們在這裡定義兩個基礎dom --> </body> <script> const state={ myHead:{ color:"red", context:"我是腦袋" }, myBody:{ color:"blue", context:"我是身體" } } // 模擬狀態 // 然後我們宣告三個渲染函式 function renderMyHead(myHead){ var DOM = document.getElementById('myHead') DOM.innerHTML = myHead.context DOM.style.color = myHead.color } function renderMyBody(myBody){ var DOM = document.getElementById('myBody') DOM.innerHTML = myBody.context DOM.style.color = myBody.color } function renderApp(state){ renderMyHead(state.myHead) renderMyBody(state.myBody) } renderApp(state) </script> </html>
上面的程式碼,通過函式渲染把狀態內的東西渲染到了檢視中,但是,這裡的狀態是暴露在外面的,任何一個地方都可以修改這個資料。這樣就不存在穩定性可言了,我們想象一下,如果我們現在規定,你主動修改的state讓程式直接無視掉,只有你通過我給你的方法去修改,我才會認可這個狀態。因此 dispatch就出現了,這是修改資料唯一的地方。
<!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>Document</title> </head> <body> <div id="myHead"></div> <div id="myBody"></div> <!-- 我們在這裡定義兩個基礎dom --> <!-- 定義一個按鈕用來觸發dispatch看效果 --> <button onclick='change()'>呼叫dispatch</button> </body> <script> const state={ myHead:{ color:"red", context:"我是腦袋" }, myBody:{ color:"blue", context:"我是身體" } } // 模擬狀態 // 然後我們宣告三個渲染函式 function renderMyHead(myHead){ var DOM = document.getElementById('myHead') DOM.innerHTML = myHead.context DOM.style.color = myHead.color } function renderMyBody(myBody){ var DOM = document.getElementById('myBody') DOM.innerHTML = myBody.context DOM.style.color = myBody.color } function renderApp(state){ renderMyHead(state.myHead) renderMyBody(state.myBody) } // 我們在這裡宣告一個dispatch函式 function dispatch(action){ switch (action.type){ case 'UPDATE_HEAD_COLOR': state.myHead.color=action.color break; case 'UPDATE_HEAD_CONTEXT': state.myHead.context=action.context break; default: break; } } functionchange(){ // 寫一個方法來觸發dispatch dispatch({type:'UPDATE_HEAD_COLOR',color:'black'}) dispatch({type:'UPDATE_HEAD_CONTEXT',context:'我變了'}) // 更新過後渲染 renderApp(state) } renderApp(state) </script> </html>
現在 你可以通過dispatch來修改state內容了,並且必須要按照它的宣告方式,和修改方式有規律地修改了。
是時候建立一個store了
我們現在有了資料,並且可以修改資料了,我們是不是可以建立我們的倉庫了?它的名字叫做 store ,當然,如果我們手動把這些東西塞進去,那就顯得太low了,使用函式作為一個工廠,幫我們生成這個那是極其舒坦的。
<!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>Document</title> </head> <body> <div id="myHead"></div> <div id="myBody"></div> <!-- 我們在這裡定義兩個基礎dom --> <!-- 定義一個按鈕用來觸發dispatch看效果 --> <button onclick='change()'>呼叫dispatch</button> </body> <script> const state={ myHead:{ color:"red", context:"我是腦袋" }, myBody:{ color:"blue", context:"我是身體" } } // 模擬狀態 // 然後我們宣告三個渲染函式 function renderMyHead(myHead){ var DOM = document.getElementById('myHead') DOM.innerHTML = myHead.context DOM.style.color = myHead.color } function renderMyBody(myBody){ var DOM = document.getElementById('myBody') DOM.innerHTML = myBody.context DOM.style.color = myBody.color } function renderApp(state){ renderMyHead(state.myHead) renderMyBody(state.myBody) } // 我們在這裡宣告一個dispatch函式 function stateChanger(state,action){//封裝過後我們需要告訴它 state來自哪裡 switch (action.type){ case 'UPDATE_HEAD_COLOR': state.myHead.color=action.color break; case 'UPDATE_HEAD_CONTEXT': state.myHead.context=action.context break; default: break; } } function creatStore(state,stateChanger){//這裡我們建立一個函式第一個引數是我們要用的狀態倉 第二個是我們自己做的dispatch const getState = () => state const dispatch = (action)=> stateChanger(state,action)//state就是我們放進來的狀態action是我們呼叫時候傳進來 return{getState,dispatch} } const store = creatStore(state,stateChanger) //這裡我們生成了store renderApp(store.getState())// 渲染 function change(){ store.dispatch({type:'UPDATE_HEAD_COLOR',color:'black'})//改變state數值 store.dispatch({type:'UPDATE_HEAD_CONTEXT',context:'我變了'}) //改變state數值 renderApp(store.getState()) //渲染 } </script> </html>
到這裡我們看到了一點Redux的雛形了,但是我們每次都要手動呼叫渲染,這是不是就非常地不爽。接下來我們要監聽資料變化,讓它自己渲染資料。那麼這個監聽在哪裡呢?沒錯store裡面
設定資料監聽
大家可能想到 我們如果把渲染資料加入到dispatch裡面不就好了嗎?沒錯,不過我們確實要在dispatch裡面做文章。
function creatStore(state,stateChanger){//這裡我們建立一個函式第一個引數是我們要用的狀態倉 第二個是我們自己做的dispatch const getState = () => state const dispatch = (action)=> { stateChanger(state,action) // 這裡我們改變了狀態 然後我們需要重新整理檢視 renderApp(state) }//state就是我們放進來的狀態action是我們呼叫時候傳進來 return{getState,dispatch} } const store = creatStore(state,dispatch) //這裡我們生成了store renderApp(store.getState())// 渲染 store.dispatch({type:'UPDATE_HEAD_COLOR',color:'black'})//改變state數值 store.dispatch({type:'UPDATE_HEAD_CONTEXT',context:'我變了'}) //改變state數值 // 現在我們可以監聽資料變化了
但是這裡我們遇到一個問題,這個creatStore只適用於我們當前的專案啊,不能夠通用啊。這該怎麼辦呢?
其實簡單 我們動態傳入渲染的方法不就好了嗎 於是我們把程式碼改成這樣
function creatStore(state,stateChanger){//這裡我們建立一個函式第一個引數是我們要用的狀態倉 第二個是我們自己做的dispatch const getState = () => state const listenerList = [] const subscribe = (listener) => listenerList.push(listener) const dispatch = (action)=> { stateChanger(state,action) // 這裡我們改變了狀態 然後我們需要重新整理檢視 listenerList.map(item=>item()) }//state就是我們放進來的狀態action是我們呼叫時候傳進來 return{getState,dispatch,subscribe} } const store = creatStore(state,stateChanger) //這裡我們生成了store store.subscribe(()=>renderApp(store.getState())) renderApp(store.getState())// 渲染 store.dispatch({type:'UPDATE_HEAD_COLOR',color:'black'})//改變state數值 store.dispatch({type:'UPDATE_HEAD_CONTEXT',context:'我變了'}) //改變state數值 // 現在我們可以動態加入監聽了
效能優化
寫到這裡 問題又出現了,每次我們改動一個數據 或者資料沒有改動 只要是呼叫了 dispatch 我們就會觸發全部的重新整理 我們加上console.log看一下
// 然後我們宣告三個渲染函式function renderMyHead(myHead){ function renderMyHead(myHead){ console.log("渲染了Head") var DOM = document.getElementById('myHead') DOM.innerHTML = myHead.context DOM.style.color = myHead.color } function renderMyBody(myBody){ console.log("渲染了Body") var DOM = document.getElementById('myBody') DOM.innerHTML = myBody.context DOM.style.color = myBody.color } function renderApp(state){ console.log("渲染了App") renderMyHead(state.myHead) renderMyBody(state.myBody) }
加上這些console以後 你會發現 我們只改變了head 但是 body也被重新渲染了 這就大大浪費了效能啊 我們怎麼辦呢?沒錯 渲染之前檢測一下資料變沒變
不過我們先丟擲一個問題
function renderMyHead(newMyHead,oldMyHead={}){ if(newMyHead==oldMyHead){ return } console.log("渲染了Head") var DOM = document.getElementById('myHead') DOM.innerHTML = newMyHead.context DOM.style.color = newMyHead.color } function renderMyBody(newMyBody,oldMyBody={}){ if(newMyBody===oldMyBody){ return } console.log("渲染了Body") var DOM = document.getElementById('myBody') DOM.innerHTML = newMyBody.context DOM.style.color = newMyBody.color } function renderApp (newState, oldState = {}) { if (newState === oldState) { return } renderMyHead(newState.myHead, oldState.myHead) renderContent(newState.myBody, oldState.myBody) } const store = creatStore(state,dispatch) //這裡我們生成了store let oldState = store.getState() store.subscribe(()=>{ const newState = store.getState() // 資料可能變化,獲取新的 state renderApp(newState,oldState)//把新舊資料傳禁區 oldState = newState //記錄資料 }) renderApp(store.getState())// 渲染 store.dispatch({type:'UPDATE_HEAD_COLOR',color:'black'})//改變state數值 store.dispatch({type:'UPDATE_HEAD_CONTEXT',context:'我變了'}) //改變state數值
好的 到這裡 問題來了,我們寫這個有用嗎?
答案顯然易見 我們做這個等同於
let obj = {cc:1} let oldObj = obj obj.cc = 3 obj===oldObj// true
他們都指向了同一個地址呀 這有什麼作用
所以我們現在要做的就是需要對 stateChanger內部的state返回模式進行改動,我們不再返回值,而是返回物件,當有物件返回的時候,我們的newState肯定就不等於oldState了,說到就做,嘗試一下
<!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>Document</title> </head> <body> <div id="myHead"></div> <div id="myBody"></div> <!-- 我們在這裡定義兩個基礎dom --> </body> <script> const state={ myHead:{ color:"red", context:"我是腦袋" }, myBody:{ color:"blue", context:"我是身體" } } // 模擬狀態 // 然後我們宣告三個渲染函式 function renderMyHead(newMyHead,oldMyHead={}){ if(newMyHead===oldMyHead){//當資料相同的時候 不渲染 return } console.log("渲染了Head") var DOM = document.getElementById('myHead') DOM.innerHTML = newMyHead.context DOM.style.color = newMyHead.color } function renderMyBody(newMyBody,oldMyBody={}){ if(newMyBody===oldMyBody){//當資料相同的時候 不渲染 return } console.log("渲染了Body") var DOM = document.getElementById('myBody') DOM.innerHTML = newMyBody.context DOM.style.color = newMyBody.color } function renderApp(newState,oldState={}){ console.log('來了',newState,oldState) if(newState===oldState){//當資料相同的時候 不渲染 return } console.log("渲染了App") renderMyHead(newState.myHead,oldState.myHead) renderMyBody(newState.myBody,oldState.myBody) } // 我們在這裡宣告一個dispatch函式 function stateChanger(state,action){ switch (action.type){ case 'UPDATE_HEAD_COLOR': return{//這裡我們使用ES6 不再去修改原來的state 而是 返回一個新的state 我們 creatStore裡面的 dispatch方法也要跟著改動 ...state, myHead:{ ...state.myHead, color:action.color } } break; case 'UPDATE_HEAD_CONTEXT': return{ ...state, myHead:{ ...state.myHead, context:action.context } } break; default: return{...state} break; } } function creatStore(state,stateChanger){//這裡我們建立一個函式第一個引數是我們要用的狀態倉 第二個是我們自己做的dispatch const getState = () => state const listenerList = [] const subscribe = (listener) => listenerList.push(listener) const dispatch = (action)=> { state = stateChanger(state,action)//這裡我們直接覆蓋原來是state // 這裡我們改變了狀態 然後我們需要重新整理檢視 listenerList.map(item=>item()) } return{getState,dispatch,subscribe} } const store = creatStore(state,stateChanger) //這裡我們生成了store let oldStore = store.getState()//快取舊資料 store.subscribe(()=>{ let newState = store.getState()//獲得新資料 renderApp(newState,oldStore)//呼叫比較渲染 oldStore = newState//資料快取 }) renderApp(store.getState()) store.dispatch({type:'UPDATE_HEAD_COLOR',color:'black'})//改變state數值 store.dispatch({type:'UPDATE_HEAD_CONTEXT',context:'我變了'}) //改變state數值 // 經過我們一番改進 我們不再去呼叫Body的渲染了 </script> </html>
到這裡我們已經搭建了自己的一個簡單的redux了,我們繼續往react-redux靠近
reducer
我們上面寫 creatStore的時候 傳入了兩個引數 state和 stateChanger 我們是不是可以把這兩個也合併到一起呢?沒問題 合併完了就是我們react-redux的reducer
// 我們就從stateChanger這個函式開始改 function stateChanger(state,action){ // 這裡我們多加一個判斷 是否有state 如果沒有 我們就return一個 if(!state){ return{ myHead:{ color:"red", context:"我是腦袋" }, myBody:{ color:"blue", context:"我是身體" } } } switch (action.type){ case 'UPDATE_HEAD_COLOR': return{//這裡我們使用ES6 不再去修改原來的state 而是 返回一個新的state 我們 creatStore裡面的 dispatch方法也要跟著改動 ...state, myHead:{ ...state.myHead, color:action.color } } break; case 'UPDATE_HEAD_CONTEXT': return{ ...state, myHead:{ ...state.myHead, context:action.context } } break; default: return{...state} break; } } function creatStore(stateChanger){//現在我們不需要傳入state了 只需要傳入stateChanger 就好了 因為我們可以拿到它 let state = null const getState = () => state const listenerList = [] const subscribe = (listener) => listenerList.push(listener) const dispatch = (action)=> { state = stateChanger(state,action)//這裡我們直接覆蓋原來是state // 這裡我們改變了狀態 然後我們需要重新整理檢視 listenerList.map(item=>item()) } dispatch({}) // 這裡初始化 state // 我們一切都宣告完成 只需要呼叫一次 dispatch({}) 因為我們的state是null 所以 執行了state = stateChanger(state,action) 從而得到了我們stateChanger內部設定的state了 return{getState,dispatch,subscribe} } const store = creatStore(stateChanger) //這裡我們生成了store 並且不用傳入state了 只要把我們寫好的 stateChanger放進去就好了 // 這個 stateChanger 官方稱之為 reducer let oldStore = store.getState()//快取舊資料 store.subscribe(()=>{ let newState = store.getState()//獲得新資料 renderApp(newState,oldStore)//呼叫比較渲染 oldStore = newState//資料快取 }) renderApp(store.getState()) store.dispatch({type:'UPDATE_HEAD_COLOR',color:'black'})//改變state數值 store.dispatch({type:'UPDATE_HEAD_CONTEXT',context:'我變了'}) //改變state數值 // 經過我們一番改進 我們不再去呼叫Body的渲染了
到這裡 你會突然發現,自己竟然動手實現了一套redux!我們要和react結合起來 還需要一個過程。
總結
在我們四檔上篇裡面,從零開始搭建了一個自己的redux,這裡面涉及到了太多高階的東西,大家需要好好消化,不理解的一定要留言提問~~