React手寫redux過程分步講解
提起 Redux 我們想到最多的應(yīng)該就是 React-redux 這個(gè)庫(kù),可是實(shí)際上 Redux 和 React-redux 并不是同一個(gè)東西, Redux 是一種架構(gòu)模式,源于 Flux。 React-redux 是 Redux 思想與 React 結(jié)合的一種具體實(shí)現(xiàn)。
在我們使用 React 的時(shí)候,常常會(huì)遇到組件深層次嵌套且需要值傳遞的情況,如果使用 props 進(jìn)行值的傳遞,顯然是非常痛苦的。為了解決這個(gè)問題,React 為我們提供了原生的 context API,但我們用的最多的解決方案卻是使用 React-redux 這個(gè)基于 context API 封裝的庫(kù)。
本文并不介紹 React-redux 的具體用法,而是通過一個(gè)小例子,來了解下什么是 redux。
好了,現(xiàn)在我們言歸正傳,來實(shí)現(xiàn)我們自己的 redux。
一、最初
首先,我們用 creat-react-app 來創(chuàng)建一個(gè)項(xiàng)目,刪除 src 下冗余部分,只保留 index.js,并修改 index.html 的 DOM 結(jié)構(gòu):
# index.html <div id="root"> <div id="head"></div> <div id="body"></div> </div>
我們?cè)?index.js 中創(chuàng)建一個(gè)對(duì)象,用它來儲(chǔ)存、管理我們整個(gè)應(yīng)用的數(shù)據(jù)狀態(tài),并用渲染函數(shù)把數(shù)據(jù)渲染在頁面:
const appState = { head: { text: '我是頭部', color: 'red' }, body: { text: '我是body', color: 'green' } } function renderHead (state){ const head = document.getElementById('head') head.innerText = state.head.text; head.style.color = state.head.color; } function renderBody (state){ const body = document.getElementById('body') body.innerText = state.body.text; body.style.color = state.body.color; } function renderApp (state){ renderHead(state); renderBody(state); } renderApp(appState);
此時(shí)運(yùn)行代碼,打開頁面,我們可以看到,在 head 中已經(jīng)出現(xiàn)了紅色字體的‘我是頭部’,在 body 中出現(xiàn)了綠色字體的‘我是body’。
如果我們把 head 和 body 看作是 root 中的兩個(gè)組件,那么我們已經(jīng)實(shí)現(xiàn)了一個(gè)全局唯一的 state 。這個(gè) state 是全局共享的,隨處可調(diào)用的。
我們可以修改 head 的渲染函數(shù),來看下效果:
function renderHead (state){ const head = document.getElementById('head') head.innerText = state.head.text + '--' + state.body.text; head.style.color = state.head.color; state.body.text = '我是經(jīng)過 head 修改后的 body'; }
我們看到,在 head 渲染函數(shù)中,我們不僅可以取用 body 屬性的值,還可以改變他的值。這樣就存在一個(gè)嚴(yán)重的問題,因?yàn)?state 是全局共用的,一旦在一個(gè)地方改變了 state 的值,那么,所有用到這個(gè)值的組件都將受到影響,而且這個(gè)改變是不可預(yù)期的,顯然給我們的代碼調(diào)試增加了難度系數(shù),這樣的結(jié)果是我們不愿意看到的!
二、dispatch
現(xiàn)在看來,在我們面前出現(xiàn)了一個(gè)矛盾:我們需要數(shù)據(jù)共享,但共享數(shù)據(jù)被任意的修改又會(huì)造成不可預(yù)期的問題!
為了解決這個(gè)矛盾,我們需要一個(gè)管家,專門來管理共享數(shù)據(jù)的狀態(tài),任何對(duì)共享數(shù)據(jù)的操作都要通過他來完成,這樣,就避免了隨意修改共享數(shù)據(jù)帶來的不可預(yù)期的危害!
我們重新定義一個(gè)函數(shù),用這個(gè)函數(shù)充當(dāng)我們的管家,來對(duì)我們的共享數(shù)據(jù)進(jìn)行管理:
function dispatch(state, action) { switch (action.type) { case 'HEAD_COLOR': state.head.color = action.color break case 'BODY_TEXT': state.body.text = action.text break default: break } }
我們來重新修改head 的渲染函數(shù):
function renderHead (state){ const head = document.getElementById('head') head.innerText = state.head.text + '--' + state.body.text; head.style.color = state.head.color; dispatch(state, { type: 'BODY_TEXT', text: '我是 head 經(jīng)過調(diào)用 dispatch 修改后的 body' }) }
dispatch 函數(shù)接收兩個(gè)參數(shù),一個(gè)是需要修改的 state ,另一個(gè)是修改的值。這時(shí),雖然我們依舊修改了 state ,但是通過 dispatch 函數(shù),我們使這種改變變得可控,因?yàn)槿魏胃淖?state 的行為,我們都可以在 dispatch 中找到改變的源頭。
這樣,我們似乎已經(jīng)解決了之前的矛盾,我們創(chuàng)建了一個(gè)全局的共享數(shù)據(jù),而且嚴(yán)格的把控了任何改變這個(gè)數(shù)據(jù)的行為。
然而,在一個(gè)文件中,我們既要保存 state, 還要維護(hù)管家函數(shù) dispatch,隨著應(yīng)用的越來越復(fù)雜,這個(gè)文件勢(shì)必會(huì)變得冗長(zhǎng)繁雜,難以維護(hù)。
現(xiàn)在,我們把 state 和 dispatch 單獨(dú)抽離出來:
- 用一個(gè)文件單獨(dú)保存 state
- 用另一個(gè)文件單獨(dú)保存 dispatch 中修改 state 的對(duì)照關(guān)系 changeState
- 最后再用一個(gè)文件,把他們結(jié)合起來,生成全局唯一的 store
這樣,不僅使單個(gè)文件變得更加精簡(jiǎn),而且在其他的應(yīng)用中,我們也可以很方便的復(fù)用我們這套方法,只需要傳入不同應(yīng)用的 state 和修改 state 的對(duì)應(yīng)邏輯 stateChange,就可以放心的通過調(diào)用 dispatch 方法,對(duì)數(shù)據(jù)進(jìn)行各種操作了:參考前端手寫面試題詳細(xì)解答
# 改變我們的目錄結(jié)構(gòu),新增 redux 文件夾 + src ++ redux --- state.js // 儲(chǔ)存應(yīng)用數(shù)據(jù)狀態(tài) --- storeChange.js // 維護(hù)一套修改 store 的邏輯,只負(fù)責(zé)計(jì)算,返回新的 store --- createStore.js // 結(jié)合 state 和 stateChange , 創(chuàng)建 store ,方便任何應(yīng)用引用 --index.js ## 修改后的各個(gè)文件 # state.js -- 全局狀態(tài) export const state = { head: { text: '我是頭部', color: 'red' }, body: { text: '我是body', color: 'green' } } # storeChange.js -- 只負(fù)責(zé)計(jì)算,修改 store export const storeChange = (store, action) => { switch (action.type) { case 'HEAD_COLOR': store.head.color = action.color break case 'BODY_TEXT': store.body.text = action.text break default: break } } # createStore.js -- 創(chuàng)建全局 store export const createStore = (state, storeChange) => { const store = state || {}; const dispatch = (action) => storeChange(store, action); return { store, dispatch } } # index.js import { state } from './redux/state.js'; import { storeChange } from './redux/storeChange.js'; import { createStore } from './redux/createStore.js'; const { store, dispatch } = createStore(state, storeChange) function renderHead (state){ const head = document.getElementById('head') head.innerText = state.text; head.style.color = state.color; } function renderBody (state){ const body = document.getElementById('body') body.innerText = state.text; body.style.color = state.color; } function renderApp (store){ renderHead(store.head); renderBody(store.body); } // 首次渲染 renderApp(store);
通過以上的文件拆分,我們看到,不僅使單個(gè)文件更加精簡(jiǎn),文件的職能也更加明確:
- 在 state 中,我們只保存我們的共享數(shù)據(jù)
- 在 storeChange 中,我們來維護(hù)改變 store 的對(duì)應(yīng)邏輯,計(jì)算出新的 store
- 在 createStore 中,我們創(chuàng)建 store
- 在 index.js 中,我們只需要關(guān)心相應(yīng)的業(yè)務(wù)邏輯
三、subscribe
一切似乎都那么美好,可是當(dāng)我們?cè)谑状武秩竞笳{(diào)用 dispatch 修改
store 時(shí),我們發(fā)現(xiàn),雖然數(shù)據(jù)被改變了,可是頁面并沒有刷新,只有在 dispatch 改變數(shù)據(jù)后,重新調(diào)用 renderApp() 才能實(shí)現(xiàn)頁面的刷新。
// 首次渲染 renderApp(store); dispatch({ type: 'BODY_TEXT', text: '我是調(diào)用 dispatch 修改的 body' }) // 修改數(shù)據(jù)后,頁面并沒有自動(dòng)刷新 renderApp(store); // 重新調(diào)用 renderApp 頁面刷新
這樣,顯然并不能達(dá)到我們的預(yù)期,我們并不想在每次改變數(shù)據(jù)后手動(dòng)的刷新頁面,如果能在改變數(shù)據(jù)后,自動(dòng)進(jìn)行頁面的刷新,當(dāng)然再好不過了!
如果直接把 renderApp 寫在 dispatch 里,顯然是不太合適的,這樣我們的 createStore 就失去了通用性。
我們可以在 createStore 中新增一個(gè)收集數(shù)組,把 dispatch 調(diào)用后需要執(zhí)行的方法統(tǒng)一收集起來,然后再循環(huán)執(zhí)行,這樣,就保證了 createStore 的通用性:
# createStore export const createStore = (state, storeChange) => { const listeners = []; const store = state || {}; const subscribe = (listen) => listeners.push(listen); const dispatch = (action) => { storeChange(store, action); listeners.forEach(item => { item(store); }) }; return { store, dispatch, subscribe } } # index.js ··· const { store, dispatch, subscribe } = createStore(state, storeChange) ··· ··· // 添加 listeners subscribe((store) => renderApp(store)); renderApp(store); dispatch({ type: 'BODY_TEXT', text: '我是調(diào)用 dispatch 修改的 body' });
這樣,我們每次調(diào)用 dispatch 時(shí),頁面就會(huì)重新刷新。如果我們不想刷新頁面,只想 alert 一句話,只需要更改添加的 listeners 就好了:
subscribe((store) => alert('頁面刷新了')); renderApp(store); dispatch({ type: 'BODY_TEXT', text: '我是調(diào)用 dispatch 修改的 body' });
這樣我們就保證了 createStore 的通用性。
四、優(yōu)化
到這里,我們似乎已經(jīng)實(shí)現(xiàn)了之前想達(dá)到的效果:我們實(shí)現(xiàn)了一個(gè)全局公用的 store , 而且這個(gè) store 的修改是經(jīng)過嚴(yán)格把控的,并且每次通過 dispatch 修改 store 后,都可以完成頁面的自動(dòng)刷新。
可是,顯然這樣并不足夠,以上的代碼仍有些簡(jiǎn)陋,存在嚴(yán)重的性能問題,
雖然我們只是修改了 body 的文案,可是,在頁面重新渲染時(shí),head 也被再次渲染。那么,我們是不是可以在頁面渲染的時(shí)候,來對(duì)比新舊兩個(gè) store 來感知哪些部分需要重新渲染,哪些部分不必再次渲染呢?
根據(jù)上面的想法,我們?cè)俅蝸硇薷奈覀兊拇a:
# storeChange.js export const storeChange = (store, action) => { switch (action.type) { case 'HEAD_COLOR': return { ...store, head: { ...store.head, color: action.color } } case 'BODY_TEXT': return { ...store, body: { ...store.body, text: action.text } } default: return { ...store } } } # createStore.js export const createStore = (state, storeChange) => { const listeners = []; let store = state || {}; const subscribe = (listen) => listeners.push(listen); const dispatch = (action) => { const newStore = storeChange(store, action); listeners.forEach(item => { item(newStore, store); }) store = newStore; }; return { store, dispatch, subscribe } } # index.js import { state } from './redux/state.js'; import { storeChange } from './redux/storeChange.js'; import { createStore } from './redux/createStore.js'; const { store, dispatch, subscribe } = createStore(state, storeChange); function renderHead (state){ console.log('render head'); const head = document.getElementById('head') head.innerText = state.text; head.style.color = state.color; } function renderBody (state){ console.log('render body'); const body = document.getElementById('body') body.innerText = state.text; body.style.color = state.color; } function renderApp (store, oldStore={}){ if(store === oldStore) return; store.head !== oldStore.head && renderHead(store.head); store.body !== oldStore.body && renderBody(store.body); console.log('render app',store, oldStore); } // 首次渲染 subscribe((store, oldStore) => renderApp(store, oldStore)); renderApp(store); dispatch({ type: 'BODY_TEXT', text: '我是調(diào)用 dispatch 修改的 body' });
以上,我們修改了 storeChange ,讓他不再直接修改原來的 store,而是通過計(jì)算,返回一個(gè)新的 store 。我們又修改了 cearteStore 讓他接收 storeChange 返回的新 store ,在 dispatch 修改數(shù)據(jù)并且頁面刷新后,把新 store 賦值給之前的 store 。而在頁面刷新時(shí),我們來通過比較 newStore 和 oldStore ,感知需要重新渲染的部分,完成一些性能上的優(yōu)化。
最后
我們通過簡(jiǎn)單的代碼例子,簡(jiǎn)單了解下 redux,雖然代碼仍有些簡(jiǎn)陋,可是我們已經(jīng)實(shí)現(xiàn)了 redux 的幾個(gè)核心理念:
- 應(yīng)用中的所有state都以一個(gè)object tree的形式存儲(chǔ)在一個(gè)單一的store中。
- 唯一能改store的方法是觸發(fā)action,action是動(dòng)作行為的抽象。
到此這篇關(guān)于React手寫redux過程分步講解的文章就介紹到這了,更多相關(guān)React redux內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React中useLayoutEffect鉤子使用場(chǎng)景詳解
這篇文章主要為大家介紹了React中useLayoutEffect鉤子使用場(chǎng)景詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12react ant Design手動(dòng)設(shè)置表單的值操作
這篇文章主要介紹了react ant Design手動(dòng)設(shè)置表單的值操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-10-10詳解前端路由實(shí)現(xiàn)與react-router使用姿勢(shì)
本篇文章主要介紹了詳解前端路由和react-router使用姿勢(shì),詳細(xì)的介紹了react-router的用法,有興趣的可以了解一下2017-08-08Electron打包React生成桌面應(yīng)用方法詳解
這篇文章主要介紹了React+Electron快速創(chuàng)建并打包成桌面應(yīng)用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-12-12React中映射一個(gè)嵌套數(shù)組實(shí)現(xiàn)demo
這篇文章主要為大家介紹了React中映射一個(gè)嵌套數(shù)組實(shí)現(xiàn)demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12React和Vue實(shí)現(xiàn)文件下載進(jìn)度條
本文主要介紹了React和Vue實(shí)現(xiàn)文件下載進(jìn)度條,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04