動(dòng)手實(shí)現(xiàn)redux

動(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)這種 statedispatch 的集合己肮,這樣別的 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)題顏色
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末倘要,一起剝皮案震驚了整個(gè)濱河市圾亏,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌封拧,老刑警劉巖志鹃,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異泽西,居然都是意外死亡曹铃,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門捧杉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來陕见,“玉大人秘血,你說我怎么就攤上這事∑捞穑” “怎么了灰粮?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長忍坷。 經(jīng)常有香客問我粘舟,道長,這世上最難降的妖魔是什么承匣? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任蓖乘,我火速辦了婚禮,結(jié)果婚禮上韧骗,老公的妹妹穿的比我還像新娘嘉抒。我一直安慰自己,他們只是感情好袍暴,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布些侍。 她就那樣靜靜地躺著,像睡著了一般政模。 火紅的嫁衣襯著肌膚如雪岗宣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天淋样,我揣著相機(jī)與錄音耗式,去河邊找鬼。 笑死趁猴,一個(gè)胖子當(dāng)著我的面吹牛刊咳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播儡司,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼娱挨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了捕犬?” 一聲冷哼從身側(cè)響起跷坝,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎碉碉,沒想到半個(gè)月后柴钻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡垢粮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年顿颅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡粱腻,死狀恐怖庇配,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绍些,我是刑警寧澤捞慌,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站柬批,受9級(jí)特大地震影響啸澡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜氮帐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一嗅虏、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧上沐,春花似錦皮服、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蕴侧,卻和暖如春择同,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背净宵。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國打工敲才, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人择葡。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓紧武,卻偏偏與公主長得像,于是被迫代替她去往敵國和親刁岸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子脏里,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容