Redux之廢話連篇

0. 前言

猿人們都知道什么React、Redux、React-redux,其實他們根本不是同一個東西,Redux 是一種架構模式(Flux 架構的一種變種),它不關注你到底用什么庫,你可以把它應用到 React 和 Vue,甚至跟 jQuery 結合都沒有問題。而 React-redux 就是把 Redux 這種架構模式和 React.js 結合起來的一個庫,就是 Redux 架構在 React.js 中的體現。至于我為什么會寫這篇文章,是有原因的,因為項目以前沒有用Redux,有很多數據更新的地方不能達到實時的、同步的更新,記得在官網上看到過這樣一句話,曰“如果你不知道自己的項目中要不要用Redux,那就說明不需要用”。而現在我的項目中,就有這種需求,所以。。。不說了,讓我靜一靜。


1.jpg

1. 簡介

引用書中的一段話

Redux是JavaScript的狀態(tài)容器,它提供可預測的狀態(tài)管理。Redux可以運行在不同的環(huán)境中,不論在客戶端、服務器端、還是原生應用都可以運行Redux。

2. 動機

一般我們要想學習一個東西的時候,做一件事的時候都是需要有動機的,為什么要去做?Redux的動機?。?!前端開發(fā)的應用真正變得越來越復雜,隨著各種框架的推出,單頁面應用也層出不窮,這些應用的狀態(tài)(state)也變得復雜起來。狀態(tài)其實就是這個運行的時候需要的各種各樣的動態(tài)數據。
管理這些不斷的變化令人非??鄲?,改變一個model的時候可能會引起其他無法預料的副作用,比如說其他model的變化或者view的變化。state在何時、什么原因發(fā)生了改變都變得無法預測。
Redux正是試圖解決這個問題、讓state的變化可以預測的工具。

3. 三大定律

1. 單一數據源

整個應用的state存儲在一個JavaScript對象中,Redux用一個稱為store的對象來存儲整個state。

//模擬數據存儲
{
  posts : {
    isLoading : false,
    items : [
      {
        id : 1, content : " hello world "
      }
    ]
  }
}

2. state是只讀的

不能在state上面直接修改數據,改變state的唯一方法是觸發(fā)action。action只是一個信息載體,一個普通的JavaScript對象。這樣確保了其他操作都無法修改state數據,整個修改都被集中處理,而且嚴格按順序執(zhí)行

//使用dispatch觸發(fā)store的改變
store.dispatch({
  type : ' GET_INFO ',
  post : {id : 2, content : " hello there "}
})
//使用getState 方法返回當前的state
store.getState()

3. 使用純函數執(zhí)行修改

為了描述action怎樣改變state,需要編寫reducer來規(guī)定修改的規(guī)則。
reducer是純函數,接收先前的state和處理的action,返回新的state。reducer可以根據應用的大小拆分成多個,分別操作state的不同部分。
那么問題就來了,你可能天天都在碼代碼,天天都在用,卻說不出來什么是純函數,那么我來舉個栗子...

簡單來說,一個函數的返回結果只依賴于它的參數,并且在執(zhí)行過程里面沒有副作用,我們就把這個函數叫做純函數。

  • 函數的返回結果只依賴于它的參數。
  • 函數執(zhí)行過程里面沒有副作用。
函數的返回結果只依賴于它的參數
const a = 1
const foo = (b) => a + b
foo(2) // => 3

foo 函數不是一個純函數,因為它返回的結果依賴于外部變量 a,我們在不知道 a 的值的情況下,并不能保證 foo(2) 的返回值是 3。雖然 foo 函數的代碼實現并沒有變化,傳入的參數也沒有變化,但它的返回值卻是不可預料的,現在 foo(2) 是 3,可能過了一會就是 4 了,因為 a 可能發(fā)生了變化變成了 2。

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

現在 foo 的返回結果只依賴于它的參數 x 和 b,foo(1, 2) 永遠是 3。今天是 3,明天也是 3,在服務器跑是 3,在客戶端跑也 3,不管你外部發(fā)生了什么變化,foo(1, 2) 永遠是 3。只要 foo 代碼不改變,你傳入的參數是確定的,那么 foo(1, 2) 的值永遠是可預料的。
這就是純函數的第一個條件:一個函數的返回結果只依賴于它的參數。

函數執(zhí)行過程沒有副作用

一個函數執(zhí)行過程對產生了外部可觀察的變化那么就說這個函數是有副作用的。
我們修改一下 foo:

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

我們把原來的 x 換成了 obj,我現在可以往里面?zhèn)饕粋€對象進行計算,計算的過程里面并不會對傳入的對象進行修改,計算前后的 counter 不會發(fā)生任何變化,計算前是 1,計算后也是 1,它現在是純的。但是我再稍微修改一下它:

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

現在情況發(fā)生了變化,我在 foo 內部加了一句 obj.x = 2,計算前 counter.x 是 1,但是計算以后 counter.x 是 2。foo 函數的執(zhí)行對外部的 counter 產生了影響,它產生了副作用,因為它修改了外部傳進來的對象,現在它是不純的。

但是你在函數內部構建的變量,然后進行數據的修改不是副作用:

const foo = (b) => {
  const obj = { x: 1 }
  obj.x = 2
  return obj.x + b
}

雖然 foo 函數內部修改了 obj,但是 obj 是內部變量,外部程序根本觀察不到,修改 obj 并不會產生外部可觀察的變化,這個函數是沒有副作用的,因此它是一個純函數。

除了修改外部的變量,一個函數在執(zhí)行過程中還有很多方式產生外部可觀察的變化,比如說調用 DOM API 修改頁面,或者你發(fā)送了 Ajax 請求,還有調用 window.reload 刷新瀏覽器,甚至是 console.log 往控制臺打印數據也是副作用。

純函數很嚴格,也就是說你幾乎除了計算數據以外什么都不能干,計算的時候還不能依賴除了函數參數以外的數據。

那么,得知上面的結論可能你看了也不是太明白,準備兩張圖來看看區(qū)別:
1.原來模塊(組件)修改共享數據是直接改的


image.png

2.我們很難把控每一根指向 appState 的箭頭,appState 里面的東西就無法把控。但現在我們必須通過一個“中間人” —— dispatch,所有的數據修改必須通過它,并且你必須用 action 來大聲告訴它要修改什么,只有它允許的才能修改:


image.png

我們再也不用擔心共享數據狀態(tài)的修改的問題,我們只要把控了 dispatch,所有的對 appState 的修改就無所遁形,畢竟只有一根箭頭指向 appState 了。

4. 共享結構對象

其實你從字面的意思就能看出我想表達什么!共享對象,就是有一個模板,我直接引用過來,要想設置自己的屬性,就設置,不想設置,就用原來的,可能就是你理解的深拷貝、淺拷貝,什么是什么拷貝呢,深拷貝就是將對象在堆區(qū)完整的copy一份,再返回棧區(qū)的引用,淺拷貝就是復制一份堆區(qū)的引用地址。
手畫一張圖。。。有點丑

WechatIMG27.jpeg

大家都知道這種 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 的更新上,我們禁止直接修改原來的對象,一旦你要修改某些東西,你就得把修改路徑上的所有對象復制一遍,例如,我們不寫下面的修改代碼:

模板代碼

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

我們新建一個 appState

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

如果我們用一個樹狀的結構來表示對象結構的話:


image.png

appState 和 newAppState 其實是兩個不同的對象,因為對象淺復制的緣故,其實它們里面的屬性 content 指向的是同一個對象;但是因為 title 被一個新的對象覆蓋了,所以它們的 title 屬性指向的對象是不同的。同樣地,修改 appState.title.color:

let newAppState1 = { // 新建一個 newAppState1
  ...newAppState, // 復制 newAppState1 里面的內容
  title: { // 用一個新的對象覆蓋原來的 title 屬性
    ...newAppState.title, // 復制原來 title 對象里面的內容
    color: "blue" // 覆蓋 color 屬性
  }
}
image.png

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

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

修改數據的時候就把修改路徑都復制一遍,但是保持其他內容不變,最后的所有對象具有某些不變共享的結構(例如上面三個對象都共享 content 對象)。大多數情況下我們可以保持 50% 以上的內容具有共享結構,這種操作具有非常優(yōu)良的特性,我們可以用它來優(yōu)化上面的渲染性能。

5. action

action是信息的載體,里面有action的名稱和要傳遞的信息,然后可以被傳遞到store中去,傳遞的方法是利用store的dispatch方法,action是store的唯一信息來源。

6. reducer

createStore 接受一個叫 reducer 的函數作為參數,這個函數規(guī)定是一個純函數,它接受兩個參數,一個是 state,一個是 action。

如果沒有傳入 state 或者 state 是 null,那么它就會返回一個初始化的數據。如果有傳入 state 的話,就會根據 action 來“修改“數據,但其實它沒有、也規(guī)定不能修改 state,而是要通過上節(jié)所說的把修改路徑的對象都復制一遍,然后產生一個新的對象返回。如果它不能識別你的 action,它就不會產生新的數據,而是(在 default 內部)把 state 原封不動地返回。

reducer 是不允許有副作用的。你不能在里面操作 DOM,也不能發(fā) Ajax 請求,更不能直接修改 state,它要做的僅僅是 ——** 初始化和計算新的 state。**

現在我們可以用這個 createStore 來構建不同的 store 了,只要給它傳入符合上述的定義的 reducer 即可:

function themeReducer (state, action) {
  if (!state) return {
    themeName: 'Red Theme',
    themeColor: 'red'
  }
  switch (action.type) {
    case 'UPATE_THEME_NAME':
      return { ...state, themeName: action.themeName }
    case 'UPATE_THEME_COLOR':
      return { ...state, themeColor: action.themeColor }
    default:
      return state
  }
}

const store = createStore(themeReducer)
...

7. store

store就是兩者的粘合劑,它能完成以下這些任務:

  • 保存整個程序的state
  • 可以通過getState()方法訪問state的值
  • 可以通過dispatch()方法執(zhí)行一個action
  • 還可以通過subscribe(listenter)注冊回調,監(jiān)聽state的變化

8. 結束語

廢話就不多說了,在這里再附上一句話“l(fā)earn once write anywhere”,感謝????????????關注,謝謝。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容