實現myRedux(二)

實現MyRedux系列

  1. 實現MyRedux(一)
  2. 實現MyRedux(二)
  3. 實現MyRedux(三)

現在我們繼續實現屬於我們自己的redux

ps:沒有看過第一篇的童鞋建議先看 第一篇再看這篇


純函數(Pure Function)

https://blog.csdn.net/qq_40766509/article/details/104405353#comments

簡單說,一個函數的返回結果只依賴於他的參數,並且執行過程中沒有副作用,就是純函數

從上面看,純函數需要兩個條件:

  • 函數的返回結果只依賴於它的參數
  • 函數的執行過程沒有副作用

現在分別看一下兩個條件

  1. 函數的返回結果只依賴於它的參數

    const a = 1
    const foo = (b) => a + b
    foo(2) // => 3
    

    foo就不是一個純函數,因爲他的返回值要依賴外部變量a,我們不知道a的情況下,不能保證foo(2)的返回值是3,如果a變化,那返回值是不可預料的。

    const a = 1
    const foo = (x, b) => x + b
    foo(1, 2) // => 3
    

    修改一下foo,現在函數的返回結果只依賴於他的參數,所以是純函數

  2. 函數執行過程沒有副作用

再修改一下foo

const a = 1
const foo = (obj, b) => {
  return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 3
counter.x // => 1

把原來的x變成了一個對象,可以往裏面傳一個對象進行計算,過程中不會對傳入的對象進行更改,counter不會發生變化,所以是純函數,但是如果我們稍微修改它一下:

const a = 1
const foo = (obj, b) => {
  obj.x = 2
  return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 4
counter.x // => 2

foo內部添加了一句obj.x = 2,計算前counter.x是1,但是計算以後的counter.x是2。函數內的執行對外部產生了影響,也叫副作用,所以不是純函數

只要函數執行產生了外部可以觀察到的變化,就是副作用,像調用DOM API修改頁面,發送AJAX請求,甚至console.log也是副作用


優化共享結構的對象

其實之前的部分已經可以算是簡單實現了一個redux,但是如果細心就會發現,之前的版本中有很嚴重的性能問題,如果我們試着在每個渲染函數開頭輸出一些日誌看一下:

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
}

執行一次初始化渲染和兩次更新

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' }) // 修改標題顏色

在控制塔瞄一眼唄:
在這裏插入圖片描述

看得出來一組三個render函數輸出了三次,第一次肯定是首次渲染,後邊兩次分別是兩次store.dispatch導致的。可以看出每次更新數據都要重新渲染整個App,但是我們兩次更新都只是更新了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 來保存舊的應用狀態,在需要重新渲染的地方把新舊數據傳進去:

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

即使修改了state.title.text,但是state還是原來那個state,一切都是以前的一切,這些引用指向的還是于謙的對象,只是內容改變了而已,就像下邊這個語句:

let obj = {
	x: 0
}
let obj2 = obj
obj2.x = 1
obj !== obj2 // false ,兩個引用指向同一個對象,怎麼會不相等?

想讓兩者成爲兩個完全不同的對象,就得使用ES6的語法

const obj = {a:1}
const obj2 = {...obj}

放到例子中就是

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

用一個圖來表示對象結構

在這裏插入圖片描述

appStatenewAppState是兩個不同的對象,因爲淺複製所以兩個對象的content指向同一對象,但是title被一個新的對象覆蓋,所以指向不同,同樣的可修改appState.title.color

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

在這裏插入圖片描述

這樣每次我們修改某些數據的時候,都不會碰原來的數據,每次都是copy的一個新對象。現在我們可以修改stateChanger中的代碼,讓他產生上述共享結構的對象

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 // 沒有修改,返回原來的對象
  }
}
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 }
}

現在再看一眼控制檯:

在這裏插入圖片描述

這樣我們就能避免不需要的渲染了!過兩天再往下實現……


在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章