實作 Redux(四):共享結構的物件提高性能


Posted by YongChenSu on 2020-12-14

log 加爆查看 function 效率問題

  1. 第一次渲染:印出前三個。
  2. 第一次 store.dispatch:印出中間三個。
  3. 第二次 store.dispatch:印出最後三個

發現不用 renderContenet



以下的優化方法可行嗎?

1. 先判斷若傳入相同的數據就不渲染

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
}

2. 再將舊的狀態儲存起來

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,等待下一次重新渲染
}) // 監聽數據變化

結果:這樣的方法行不通,因為改到相同的記憶體位置

查看 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
  }
}

以上改動的如同以下的程式碼

修改相同的記憶體位置的東西

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

了解一下 ES6 解構

const obj = {a: 1, b: 2}
const obj2 = { ...obj } // => { a: 1, b: 2 }
const obj3 = { ...obj, b: 3, c: 4} // => { a: 1, b: 2, c: 3 }

obj、obj2、obj3 的記憶體位置不同

改變記憶體位置

let newAppState = {
  ...appState, // 複製 appState 裡面的內容
  title: { // 用一個新的物件覆蓋原來 title 屬性
    ...appState.title,
    text: '新的 React.js 小書' // 覆蓋 text 屬性
  }
}

content 指的是原本的物件 (舊的記憶體位置),title 則不是

color 也是一樣

console.log('1. ', newAppState !== newAppState1) // ture
console.log('2. ', appState.title !== newAppState.title) // ture
console.log('3. ', appState.content !== appState.content) // false

優化性能

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 // 沒有修改,回傳原來的狀態
  }
}

修改一下 createStore

每次呼叫 stateChanger(state, action) 後覆蓋原來的物件。

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 }
}

只有第一次印出 render content



本節完整程式碼

const appState = {
  title: {
    text: 'React.js 小書',
    color: 'red',
  },
  content: {
    text: 'React.js 小書內容',
    color: 'blue'
  }
}

let newAppState = {
  ...appState, // 複製 appState 裡面的內容
  title: { // 用一個新的物件覆蓋原來 title 屬性
    ...appState.title, // 複製原來 title 裡面的內容
    text: '新的 React.js 小書' // 覆蓋 text 屬性
  }
}

let newAppState1 = {
  ...newAppState, // 複製 newAppState 裡面的內容
  title: { // 用一個新的物件覆蓋原來 title 屬性
    ...newAppState.title, // 複製原來 title 裡面的內容
    text: '新的 React.js 小書' // 覆蓋 color 屬性
  }
}

console.log('1. ', newAppState !== newAppState1) // ture
console.log('2. ', appState.title !== newAppState.title) // ture
console.log('3. ', appState.content !== appState.content) // false

// 新增渲染函數
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
}

// createStore
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 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()) // first render
store.dispatch({type: 'UPDATE_TITLE_TEXT', text: 'React 小書'}) // 修改內容
store.dispatch({type: 'UPDATE_TITLE_COLOR', color: 'blue'}) // 修改標題顏色
// renderApp(store.getState()) // redner again


總結

  1. 在渲染函式中新增判斷式
  2. stateChanger 函式中複製相同內容但改變記憶體位置。
  3. createStore 中將舊的狀態 state 變成新的狀態 state。

藉由判斷是否改變記憶體、是否傳入相同的數據,決定要不要重新渲染。

不必擔心新增記憶體位置會有性能、內存問題,因為成本非常低。而且最多保存兩個物件引用 oldStatenewState,其餘的會被回收掉。

參考資源


#程式導師實驗計畫第四期 #前端 #React #react book #Redux #共享結構的物件







Related Posts

[Week9] - Cookie

[Week9] - Cookie

Java 學習筆記 01 – 開發環境

Java 學習筆記 01 – 開發環境

【譯】Tokio 內部機制:從頭理解 Rust 非同步 I/O 框架

【譯】Tokio 內部機制:從頭理解 Rust 非同步 I/O 框架


Comments