動(dòng)手實(shí)現(xiàn)redux
一、首先渲染數(shù)據(jù)一部分?jǐn)?shù)據(jù)到頁面上
src/index.js
const appState = {
title: {
text: 'React.js',
color: 'red',
},
content: {
text: 'React.js 內(nèi)容',
color: 'blue'
}
}
function renderApp(appState) {
renderTitle(appState.title)
renderContent(appState.content)
}
function renderTitle(title) {
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = title.text
titleDOM.style.color = title.color
}
function renderContent(content) {
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = content.text
contentDOM.style.color = content.color
}
renderApp(appState)
存在的問題:
所有對(duì)共享狀態(tài)的操作都是不可預(yù)料的(某個(gè)模塊 appState.title = null
你一點(diǎn)意見都沒有)芳杏,出現(xiàn)問題的時(shí)候 debug 起來就非常困難矩屁,這就是老生常談的盡量避免全局變量
二辟宗、改進(jìn)通過dispatch來觸發(fā)
let appState = {
title: {
text: 'React.js',
color: 'red',
},
content: {
text: 'React.js 內(nèi)容',
color: 'blue'
}
}
function dispatch (action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
appState.title.text = action.text
break
case 'UPDATE_TITLE_COLOR':
appState.title.color = action.color
break
default:
break
}
}
function renderApp (appState) {
renderTitle(appState.title)
renderContent(appState.content)
}
function renderTitle (title) {
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = title.text
titleDOM.style.color = title.color
}
function renderContent (content) {
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = content.text
contentDOM.style.color = content.color
}
renderApp(appState) // 首次渲染頁面
dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js》' }) // 修改標(biāo)題文本
dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標(biāo)題顏色
renderApp(appState) // 把新的數(shù)據(jù)渲染到頁面上
appState
里面的東西就無法把控。但現(xiàn)在我們必須通過一個(gè)“中間人” —— dispatch
档插,所有的數(shù)據(jù)修改必須通過它慢蜓,并且你必須用 action
來大聲告訴它要修改什么,只有它允許的才能修改郭膛。我們?cè)僖膊挥脫?dān)心共享數(shù)據(jù)狀態(tài)的修改的問題晨抡,我們只要把控了 dispatch
,所有的對(duì) appState
的修改就無所遁形则剃,畢竟只有一根箭頭指向 appState
了
三耘柱、 抽離dispatch
出來,讓它變得更加通用
現(xiàn)在我們把它們集中到一個(gè)地方棍现,給這個(gè)地方起個(gè)名字叫做 store
调煎,然后構(gòu)建一個(gè)函數(shù) createStore
,用來專門生產(chǎn)這種 state
和 dispatch
的集合己肮,這樣別的 App 也可以用這種模式了:
function createStore (state, stateChanger) {
const getState = () => state
const dispatch = (action) => stateChanger(state, action)
return { getState, dispatch }
}
createStore
接受兩個(gè)參數(shù)士袄,一個(gè)是表示應(yīng)用程序狀態(tài)的 state
;另外一個(gè)是 stateChanger
谎僻,它來描述應(yīng)用程序狀態(tài)會(huì)根據(jù) action 發(fā)生什么變化娄柳,其實(shí)就是相當(dāng)于 dispatch
代碼里面的內(nèi)容
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())
}
return { getState, dispatch, subscribe }
}
function renderApp (appState) {
renderTitle(appState.title)
renderContent(appState.content)
}
function renderTitle (title) {
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = title.text
titleDOM.style.color = title.color
}
function renderContent (content) {
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = content.text
contentDOM.style.color = content.color
}
let appState = {
title: {
text: 'React.js',
color: 'red',
},
content: {
text: 'React.js 內(nèi)容',
color: 'blue'
}
}
function stateChanger (state, action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
state.title.text = action.text
break
case 'UPDATE_TITLE_COLOR':
state.title.color = action.color
break
default:
break
}
}
const store = createStore(appState, stateChanger)
store.subscribe(() => renderApp(store.getState())) // 監(jiān)聽數(shù)據(jù)變化
renderApp(store.getState()) // 首次渲染頁面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js》' }) // 修改標(biāo)題文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標(biāo)題顏色
四、優(yōu)化性能
現(xiàn)在存在的問題
我們分別在renderApp
,renderTitle
,renderContent
中加入console.log('render xxx…')
function renderApp (appState) {
console.log('render app...')
...
}
function renderTitle (title) {
console.log('render title...')
...
}
function renderContent (content) {
console.log('render content...')
...
}
render app...
index.js:10 render title...
index.js:17 render content...
index.js:4 render app...
index.js:10 render title...
index.js:17 render content...
index.js:4 render app...
index.js:10 render title...
index.js:17 render content...
前三個(gè)毫無疑問是第一次渲染打印出來的艘绍。中間三個(gè)是第一次 store.dispatch
導(dǎo)致的赤拒,最后三個(gè)是第二次 store.dispatch
導(dǎo)致的∮站希可以看到問題就是挎挖,每當(dāng)更新數(shù)據(jù)就重新渲染整個(gè) App,但其實(shí)我們兩次更新都沒有動(dòng)到 appState
里面的 content
字段的對(duì)象航夺,而動(dòng)的是 title
字段蕉朵。其實(shí)并不需要重新 renderContent
,它是一個(gè)多余的更新操作阳掐,現(xiàn)在我們需要優(yōu)化它墓造。
這里提出的解決方案是,在每個(gè)渲染函數(shù)執(zhí)行渲染操作之前先做個(gè)判斷锚烦,判斷傳入的新數(shù)據(jù)和舊的數(shù)據(jù)是不是相同觅闽,相同的話就不渲染了。
// 防止 oldAppState 沒有傳入涮俄,所以加了默認(rèn)參數(shù) oldAppState = {}
function renderApp (newAppState, oldAppState = {}) {
if (newAppState === oldAppState) return // 數(shù)據(jù)沒有變化就不渲染了
...
}
function renderTitle (newTitle, oldTitle = {}) {
if (newTitle === oldTitle) return // 數(shù)據(jù)沒有變化就不渲染了
...
}
function renderContent (newContent, oldContent = {}) {
if (newContent === oldContent) return // 數(shù)據(jù)沒有變化就不渲染了
...
}
然后我們用一個(gè) oldState
變量保存舊的應(yīng)用狀態(tài)蛉拙,在需要重新渲染的時(shí)候把新舊數(shù)據(jù)傳進(jìn)入去:
const store = createStore(appState, stateChanger)
let oldState = store.getState() // 緩存舊的 state
store.subscribe(() => {
const newState = store.getState() // 數(shù)據(jù)可能變化,獲取新的 state
renderApp(newState, oldState) // 把新舊的 state 傳進(jìn)去渲染
oldState = newState // 渲染完以后彻亲,新的 newState 變成了舊的 oldState孕锄,等待下一次數(shù)據(jù)變化重新渲染
})
做到這里其實(shí)還未達(dá)到效果吮廉,看看我們的 stateChanger
:
function stateChanger (state, action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
state.title.text = action.text
break
case 'UPDATE_TITLE_COLOR':
state.title.color = action.color
break
default:
break
}
}
即使你修改了 state.title.text
,但是 state
還是原來那個(gè) state
畸肆,state.title
還是原來的 state.title
宦芦,這些引用指向的還是原來的對(duì)象,只是對(duì)象內(nèi)的內(nèi)容發(fā)生了改變轴脐。所以即使在每個(gè)渲染函數(shù)開頭加了那個(gè)判斷也沒什么用调卑。
但是,我們接下來就要讓這種事情變成可能大咱。
共享結(jié)構(gòu)對(duì)象
const obj = { a: 1, b: 2}
const obj2 = { ...obj } // => { a: 1, b: 2 }
const obj2 = { ...obj }
其實(shí)就是新建一個(gè)對(duì)象 obj2
恬涧,然后把 obj
所有的屬性都復(fù)制到 obj2
里面,相當(dāng)于對(duì)象的淺復(fù)制碴巾。上面的 obj
里面的內(nèi)容和 obj2
是完全一樣的溯捆,但是卻是兩個(gè)不同的對(duì)象。
我們可以把這種特性應(yīng)用在 state
的更新上厦瓢,我們禁止直接修改原來的對(duì)象提揍,一旦你要修改某些東西,你就得把修改路徑上的所有對(duì)象復(fù)制一遍煮仇,例如劳跃,我們修改下面的代碼
appState.title.text = '《React.js》'
//修改為
let newAppState = {
...appState,
title: {
...appState.title,
text: '《React.js》'
}
}
所以這時(shí)候我們修改我們的stateChanger
function stateChanger (state, action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
return { // 構(gòu)建新的對(duì)象并且返回
...state,
title: {
...state.title,
text: action.text
}
}
case 'UPDATE_TITLE_COLOR':
return { // 構(gòu)建新的對(duì)象并且返回
...state,
title: {
...state.title,
color: action.color
}
}
default:
return state // 沒有修改,返回原來的對(duì)象
}
}
因?yàn)?stateChanger
不會(huì)修改原來對(duì)象了欺抗,而是返回對(duì)象,所以我們需要修改一下 createStore
强重。讓它用每次 stateChanger(state, action)
的調(diào)用結(jié)果覆蓋原來的 state
:
function createStore (state, stateChanger) {
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
state = stateChanger(state, action) // 覆蓋原對(duì)象
listeners.forEach((listener) => listener())
}
return { getState, dispatch, subscribe }
}
完整代碼為
function createStore (state, stateChanger) {
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
state = stateChanger(state, action) // 覆蓋原對(duì)象
listeners.forEach((listener) => listener())
}
return { getState, dispatch, subscribe }
}
function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 沒有傳入绞呈,所以加了默認(rèn)參數(shù) oldAppState = {}
if (newAppState === oldAppState) return // 數(shù)據(jù)沒有變化就不渲染了
console.log('render app...')
renderTitle(newAppState.title, oldAppState.title)
renderContent(newAppState.content, oldAppState.content)
}
function renderTitle (newTitle, oldTitle = {}) {
if (newTitle === oldTitle) return // 數(shù)據(jù)沒有變化就不渲染了
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 // 數(shù)據(jù)沒有變化就不渲染了
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 內(nèi)容',
color: 'blue'
}
}
function stateChanger (state, action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
return { // 構(gòu)建新的對(duì)象并且返回
...state,
title: {
...state.title,
text: action.text
}
}
case 'UPDATE_TITLE_COLOR':
return { // 構(gòu)建新的對(duì)象并且返回
...state,
title: {
...state.title,
color: action.color
}
}
default:
return state // 沒有修改,返回原來的對(duì)象
}
}
const store = createStore(appState, stateChanger)
let oldState = store.getState() // 緩存舊的 state
store.subscribe(() => {
const newState = store.getState() // 數(shù)據(jù)可能變化间景,獲取新的 state
renderApp(newState, oldState) // 把新舊的 state 傳進(jìn)去渲染
oldState = newState // 渲染完以后佃声,新的 newState 變成了舊的 oldState,等待下一次數(shù)據(jù)變化重新渲染
})
renderApp(store.getState()) // 首次渲染頁面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js》' }) // 修改標(biāo)題文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標(biāo)題顏色