手挽手帶你學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;
}
}
function change(){
// 寫一個方法來觸發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,這裏面涉及到了太多高級的東西,大家需要好好消化,不理解的一定要留言提問~~