動手實現 Redux(四):共享結構的對象提升性能

接下來兩節某些地方可能會稍微有一點點抽象,可是我會盡量用簡單的方式進行講解。若是你以爲理解起來有點困難,能夠把這幾節多讀多理解幾遍,其實咱們一路走來都是符合「邏輯」的,都是發現問題、思考問題、優化代碼的過程。因此最好可以用心留意、思考咱們每個提出來的問題。html

細心的朋友能夠發現,其實咱們以前的例子當中是有比較嚴重的性能問題的。咱們在每一個渲染函數的開頭打一些 Log 看看:緩存

function renderApp (appState) {
  console.log('render app...')
  renderTitle(appState.title)
  renderContent(appState.content)
}

function renderTitle (title) {
  console.log('render title...')
  const titleDOM = document.getElementById('title')
  titleDOM.innerHTML = title.text
  titleDOM.style.color = title.color
}

function renderContent (content) {
  console.log('render content...')
  const contentDOM = document.getElementById('content')
  contentDOM.innerHTML = content.text
  contentDOM.style.color = content.color
}

依舊執行一次初始化渲染,和兩次更新,這裏代碼保持不變:app

const store = createStore(appState, stateChanger)
store.subscribe(() => renderApp(store.getState())) // 監聽數據變化

renderApp(store.getState()) // 首次渲染頁面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小書》' }) // 修改標題文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標題顏色

能夠在控制檯看到:函數

前三個毫無疑問是第一次渲染打印出來的。中間三個是第一次 store.dispatch 致使的,最後三個是第二次 store.dispatch 致使的。能夠看到問題就是,每當更新數據就從新渲染整個 App,但其實咱們兩次更新都沒有動到 appState 裏面的 content字段的對象,而動的是 title 字段。其實並不須要從新 renderContent,它是一個多餘的更新操做,如今咱們須要優化它。性能

這裏提出的解決方案是,在每一個渲染函數執行渲染操做以前先作個判斷,判斷傳入的新數據和舊的數據是否是相同,相同的話就不渲染了。優化

function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 沒有傳入,因此加了默認參數 oldAppState = {}
  if (newAppState === oldAppState) return // 數據沒有變化就不渲染了
  console.log('render app...')
  renderTitle(newAppState.title, oldAppState.title)
  renderContent(newAppState.content, oldAppState.content)
}

function renderTitle (newTitle, oldTitle = {}) {
  if (newTitle === oldTitle) return // 數據沒有變化就不渲染了
  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 // 數據沒有變化就不渲染了
  console.log('render content...')
  const contentDOM = document.getElementById('content')
  contentDOM.innerHTML = newContent.text
  contentDOM.style.color = newContent.color
}

而後咱們用一個 oldState 變量保存舊的應用狀態,在須要從新渲染的時候把新舊數據傳進入去:spa

const store = createStore(appState, stateChanger)
let oldState = store.getState() // 緩存舊的 state
store.subscribe(() => {
  const newState = store.getState() // 數據可能變化,獲取新的 state
  renderApp(newState, oldState) // 把新舊的 state 傳進去渲染
  oldState = newState // 渲染完之後,新的 newState 變成了舊的 oldState,等待下一次數據變化從新渲染
})
...

但願到這裏沒有把你們忽悠到,上面的代碼根本不會達到咱們的效果。看看咱們的 stateChanger3d

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 仍是原來那個 statestate.title仍是原來的 state.title,這些引用指向的仍是原來的對象,只是對象內的內容發生了改變。因此即便你在每一個渲染函數開頭加了那個判斷又什麼用?這就像是下面的代碼那樣自欺欺人:code

let appState = {
  title: {
    text: 'React.js 小書',
    color: 'red',
  },
  content: {
    text: 'React.js 小書內容',
    color: 'blue'
  }
}
const oldState = appState
appState.title.text = '《React.js 小書》'
oldState !== appState // false,其實兩個引用指向的是同一個對象,咱們卻但願它們不一樣。

可是,咱們接下來就要讓這種事情變成可能。htm

共享結構的對象

但願你們都知道這種 ES6 的語法:

const obj = { a: 1, b: 2}
const obj2 = { ...obj } // => { a: 1, b: 2 }

const obj2 = { ...obj } 其實就是新建一個對象 obj2,而後把 obj 全部的屬性都複製到 obj2 裏面,至關於對象的淺複製。上面的 obj 裏面的內容和 obj2 是徹底同樣的,可是倒是兩個不一樣的對象。除了淺複製對象,還能夠覆蓋、拓展對象屬性:

const obj = { a: 1, b: 2}
const obj2 = { ...obj, b: 3, c: 4} // => { a: 1, b: 3, c: 4 },覆蓋了 b,新增了 c

咱們能夠把這種特性應用在 state 的更新上,咱們禁止直接修改原來的對象,一旦你要修改某些東西,你就得把修改路徑上的全部對象複製一遍,例如,咱們不寫下面的修改代碼:

appState.title.text = '《React.js 小書》'

取而代之的是,咱們新建一個 appState,新建 appState.title,新建 appState.title.text

let newAppState = { // 新建一個 newAppState
  ...appState, // 複製 appState 裏面的內容
  title: { // 用一個新的對象覆蓋原來的 title 屬性
    ...appState.title, // 複製原來 title 對象裏面的內容
    text: '《React.js 小書》' // 覆蓋 text 屬性
  }
}

若是咱們用一個樹狀的結構來表示對象結構的話:

appState 和 newAppState 實際上是兩個不一樣的對象,由於對象淺複製的緣故,其實它們裏面的屬性 content 指向的是同一個對象;可是由於 title 被一個新的對象覆蓋了,因此它們的 title 屬性指向的對象是不一樣的。一樣地,修改 appState.title.color

let newAppState1 = { // 新建一個 newAppState1
  ...newAppState, // 複製 newAppState1 裏面的內容
  title: { // 用一個新的對象覆蓋原來的 title 屬性
    ...newAppState.title, // 複製原來 title 對象裏面的內容
    color: "blue" // 覆蓋 color 屬性
  }
}

咱們每次修改某些數據的時候,都不會碰原來的數據,而是把須要修改數據路徑上的對象都 copy 一個出來。這樣有什麼好處?看看咱們的目的達到了:

appState !== newAppState // true,兩個對象引用不一樣,數據變化了,從新渲染
appState.title !== newAppState.title // true,兩個對象引用不一樣,數據變化了,從新渲染
appState.content !== appState.content // false,兩個對象引用相同,數據沒有變化,不須要從新渲染

修改數據的時候就把修改路徑都複製一遍,可是保持其餘內容不變,最後的全部對象具備某些不變共享的結構(例如上面三個對象都共享 content 對象)。大多數狀況下咱們能夠保持 50% 以上的內容具備共享結構,這種操做具備很是優良的特性,咱們能夠用它來優化上面的渲染性能。

優化性能

咱們修改 stateChanger,讓它修改數據的時候,並不會直接修改原來的數據 state,而是產生上述的共享結構的對象:

function stateChanger (state, action) {
  switch (action.type) {
    case 'UPDATE_TITLE_TEXT':
      return { // 構建新的對象而且返回
        ...state,
        title: {
          ...state.title,
          text: action.text
        }
      }
    case 'UPDATE_TITLE_COLOR':
      return { // 構建新的對象而且返回
        ...state,
        title: {
          ...state.title,
          color: action.color
        }
      }
    default:
      return state // 沒有修改,返回原來的對象
  }
}

代碼稍微比原來長了一點,可是是值得的。每次須要修改的時候都會產生新的對象,而且返回。而若是沒有修改(在 default 語句中)則返回原來的 state 對象。

由於 stateChanger 不會修改原來對象了,而是返回對象,因此咱們須要修改一下 createStore。讓它用每次 stateChanger(state, action) 的調用結果覆蓋原來的 state

function createStore (state, stateChanger) {
  const listeners = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
    state = stateChanger(state, action) // 覆蓋原對象
    listeners.forEach((listener) => listener())
  }
  return { getState, dispatch, subscribe }
}

保持上面的渲染函數開頭的對象判斷不變,再看看控制檯:

前三個是首次渲染。後面的 store.dispatch 致使的從新渲染都沒有關於 content 的 Log 了。由於產生共享結構的對象,新舊對象的 content 引用指向的對象是同樣的,因此觸發了 renderContent 函數開頭的:

...
  if (newContent === oldContent) return
...

咱們成功地把沒必要要的頁面渲染優化掉了,問題解決。另外,並不須要擔憂每次修改都新建共享結構對象會有性能、內存問題,由於構建對象的成本很是低,並且咱們最多保存兩個對象引用(oldState 和 newState),其他舊的對象都會被垃圾回收掉。

本節完整代碼:

function createStore (state, stateChanger) {
  const listeners = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
    state = stateChanger(state, action) // 覆蓋原對象
    listeners.forEach((listener) => listener())
  }
  return { getState, dispatch, subscribe }
}

function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 沒有傳入,因此加了默認參數 oldAppState = {}
  if (newAppState === oldAppState) return // 數據沒有變化就不渲染了
  console.log('render app...')
  renderTitle(newAppState.title, oldAppState.title)
  renderContent(newAppState.content, oldAppState.content)
}

function renderTitle (newTitle, oldTitle = {}) {
  if (newTitle === oldTitle) return // 數據沒有變化就不渲染了
  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 // 數據沒有變化就不渲染了
  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 小書內容',
    color: 'blue'
  }
}

function stateChanger (state, action) {
  switch (action.type) {
    case 'UPDATE_TITLE_TEXT':
      return { // 構建新的對象而且返回
        ...state,
        title: {
          ...state.title,
          text: action.text
        }
      }
    case 'UPDATE_TITLE_COLOR':
      return { // 構建新的對象而且返回
        ...state,
        title: {
          ...state.title,
          color: action.color
        }
      }
    default:
      return state // 沒有修改,返回原來的對象
  }
}

const store = createStore(appState, stateChanger)
let oldState = store.getState() // 緩存舊的 state
store.subscribe(() => {
  const newState = store.getState() // 數據可能變化,獲取新的 state
  renderApp(newState, oldState) // 把新舊的 state 傳進去渲染
  oldState = newState // 渲染完之後,新的 newState 變成了舊的 oldState,等待下一次數據變化從新渲染
})

renderApp(store.getState()) // 首次渲染頁面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小書》' }) // 修改標題文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標題顏色

相關文章
相關標籤/搜索