學習React的一知半解

wq93發表於2019-03-10

① 初探

HMTL的渲染過程

​ 這個結構化文字就是 HTML 文字, HTML 中的每個元素都對應 DOM中某個節點,這樣,因為 HTML 元素的逐級包含關係, DOM 節點自然就構成了一個樹形結構,稱為 DOM 樹 。 ​ 瀏覽器為了渲染 HTML 格式的網頁,會先將 HTML 文字解析以構建 DOM 樹,然後根據 DOM 樹渲染出使用者看到的介面,當要改變介面內容的時候,就去改變 DOM 樹上的節點 。

純函式

React 的理念 ,歸結為一個公式,就像下面這樣 : ​ UI=render(data) ​ 讓我們來看看這個公式表達的含義,使用者看到的介面( UI),應該是一個函式(在這裡叫 render)的執行結果,只接受資料( data)作為引數 。

​ 這個函式是一個純函式,所謂純函式,指的是沒有任何副作用,輸出完全依賴於輸入的函式,兩次函式呼叫如果輸人 ​ 相同,得到的結果也絕對相同 。 如此一來,最終的使用者介面,在 render 函式確定的情況下完全取決於輸入資料 。

React初解

​ react的功能其實很單一,主要負責渲染的功能,現有的框架,比如angular是一個大而全的框架,用了angular幾乎就不需要用其他工具輔助配合.

PS: react感覺類似VScode或sublime需要裝各種外掛來寫程式碼,而angular就像webstorm一樣整合了很多功能

React 是什麼

​ 用指令碼進行DOM操作的代價很昂貴。把DOM和JavaScript各自想象為一個島嶼,它們之間用收費橋樑連線,js每次訪問DOM,都要途徑這座橋,並交納“過橋費”,訪問DOM的次數越多,費用也就越高。

​ 因此,推薦的做法是儘量減少過橋的次數,努力待在ECMAScript島上。因為這個原因react的虛擬dom就顯得難能可貴了,它創造了虛擬dom並且將它們儲存起來,每當狀態發生變化的時候就會創造新的虛擬節點和以前的進行對比,讓變化的部分進行渲染。

整個過程沒有對dom進行獲取和操作,只有一個渲染的過程,所以react說是一個ui框架。

元件的生命週期

image.png

元件在初始化時會觸發5個鉤子函式:

1、getDefaultProps()

​ 設定預設的props,也可以用defaultProps設定元件的預設屬性。

這個函式只在 React.createClass 方法創造的元件類才會用到 。

2、getInitialState()

​ 在使用es6的class語法時是沒有這個鉤子函式的,可以直接在constructor中定義this.state。此時可以訪問this.props。

​ 這個函式只在 React.createClass 方法創造的元件類才會用到 。

3、componentWillMount()

​ 元件初始化時只呼叫,以後元件更新不呼叫,整個生命週期只呼叫一次,此時可以修改state。

4、 render()

​ react最重要的步驟,建立虛擬dom,進行diff演算法,更新dom樹都在此進行。此時就不能更改state了。

​ 通常一個元件要發揮作用,總是要渲染一些東西, render 函式並不做實際的誼染動作,它只是返回一個 JSX 描述的結構,最終由 React 來操作渲染過程。 ​ 當然,某些特殊元件的作用不是渲染介面,或者,元件在某些情況下選擇沒有東西可畫,那就讓 render 函式返回一個 null 或者 false ,等於告訴 React,這個元件這次不需要渲染任何 DOM 元素 。 ​ 需要注意, render 函式應該是一個純函式,完全根據 this.state 和 this.props 來決定返回的結果,而且不要產生任何副作用。在 render 函式中去呼叫 this.setState 毫無疑問是錯誤的,因為一個純函式不應該引起狀態的改變。

5、componentDidMount()

​ Render 函式返回的東西已 經引發了渲染,元件已經被“裝載”到了 DOM 樹上 。 元件渲染之後呼叫,可以通過this.getDOMNode()獲取和操作dom節點,只呼叫一次。


在更新時也會觸發5個鉤子函式:

6、componentWillReceivePorps(nextProps)

元件初始化時不呼叫,元件接受新的props時呼叫。

7、shouldComponentUpdate(nextProps, nextState)

​ React效能優化非常重要的一環。元件接受新的state或者props時呼叫,我們可以設定在此對比前後兩個props和state是否相同,如果相同則返回false阻止更新,因為相同的屬性狀態一定會生成相同的dom樹,這樣就不需要創造新的dom樹和舊的dom樹進行diff演算法對比,節省大量效能,尤其是在dom結構複雜的時候。不過呼叫this.forceUpdate會跳過此步驟。

8、componentWillUpdate(nextProps, nextState)

元件初始化時不呼叫,只有在元件將要更新時才呼叫,此時可以修改state

9、render()

當元件的state或者props發生改變的時候,render函式就會重新執行

10、componentDidUpdate()

元件初始化時不呼叫,元件更新完成後呼叫,此時可以獲取dom節點。

還有一個解除安裝鉤子函式

11、componentWillUnmount()

元件將要解除安裝時呼叫,一些事件監聽和定時器需要在此時清除。

​ 以上可以看出來react總共有10個周期函式(render重複一次),這個10個函式可以滿足我們所有對元件操作的需求,利用的好可以提高開發效率和元件效能。

rendershouldComponentUpdate函式,也是 React 生命週期函式中唯二兩個要求有返回結果的函式。 render 函式的返回結果將用於構造 DOM 物件,而 shouldComponentUpdate函式返回一個布林值,告訴 React 庫這個元件在這次更新過程中是否要繼續 。

V16 生命週期函式用法建議

class ExampleComponent extends React.Component {
  // 用於初始化 state
  constructor() {}
  // 用於替換 `componentWillReceiveProps` ,該函式會在初始化和 `update` 時被呼叫
  // 因為該函式是靜態函式,所以取不到 `this`
  // 如果需要對比 `prevProps` 需要單獨在 `state` 中維護
  static getDerivedStateFromProps(nextProps, prevState) {}
  // 判斷是否需要更新元件,多用於元件效能優化
  shouldComponentUpdate(nextProps, nextState) {}
  // 元件掛載後呼叫
  // 可以在該函式中進行請求或者訂閱
  componentDidMount() {}
  // 用於獲得最新的 DOM 資料
  getSnapshotBeforeUpdate() {}
  // 元件即將銷燬
  // 可以在此處移除訂閱,定時器等等
  componentWillUnmount() {}
  // 元件銷燬後呼叫
  componentDidUnMount() {}
  // 元件更新後呼叫
  componentDidUpdate() {}
  // 渲染元件函式
  render() {}
  // 以下函式不建議使用
  UNSAFE_componentWillMount() {}
  UNSAFE_componentWillUpdate(nextProps, nextState) {}
  UNSAFE_componentWillReceiveProps(nextProps) {}
}
複製程式碼

父子元件的渲染過程

​ 因為 render 函式本身並不往 DOM 樹上渲染或者裝載內容,它只是返回一個 JSX 表示的物件,然後由 React 庫來根據返回物件決定如何渲染 。而 React 庫肯定是要把所有元件返回的結果綜合起來,才能知道該如何產生對應的 DOM修改 。 所以,只有 React 庫呼叫三個 Counter 元件的 render 函式之後,才有可能完成裝載,這時候才會依次呼叫各個元件的 componentDidMount 函式作為裝載過程的收尾 。

React的元件化

​ react的一個元件很明顯的由dom檢視和state資料組成,兩個部分涇渭分明。

​ state是資料中心,它的狀態決定著檢視的狀態。這時候發現似乎和我們一直推崇的MVC開發模式有點區別,沒了Controller控制器,那使用者互動怎麼處理,資料變化誰來管理?

​ 然而這並不是react所要關心的事情,它只負責ui的渲染。與其他框架監聽資料動態改變dom不同,react採用setState來控制檢視的更新。

​ setState會自動呼叫render函式,觸發檢視的重新渲染,如果僅僅只是state資料的變化而沒有呼叫setState,並不會觸發更新。

​ 元件就是擁有獨立功能的檢視模組,許多小的元件組成一個大的元件,整個頁面就是由一個個元件組合而成。它的好處是利於重複利用和維護。

UI = render(data)

​ React 元件扮 演的是 render 函式的角色,應該是一個沒有副作用的純函式。修改 props 的值, 是一個副作用,元件應該避免。

元件類別

概念: 所謂元件,簡單說,指的是能完成某個特定功能的獨立的 、 可重用的程式碼 。

  • 容器元件 只關心邏輯,不負責頁面渲染
  • UI元件 不關心邏輯,只負責頁面渲染
  • 無狀態元件 沒有render()函式,只是一個函式,沒有宣告周期函式,效率更高

image.png

React的 Diff演算法

​ 當元件更新的時候,react會建立一個新的虛擬dom樹並且會和之前儲存的dom樹進行比較,這個比較多過程就用到了diff演算法,所以元件初始化的時候是用不到的

​ react提出了一種假設,相同的節點具有類似的結構,而不同的節點具有不同的結構。在這種假設之上進行逐層的比較,如果發現對應的節點是不同的,那就直接刪除舊的節點以及它所包含的所有子節點然後替換成新的節點。如果是相同的節點,則只進行屬性的更改。

​ 對於列表的diff演算法稍有不同,因為列表通常具有相同的結構,在對列表節點進行刪除,插入,排序的時候,單個節點的整體操作遠比一個個對比一個個替換要好得多,所以在建立列表的時候需要設定key值,這樣react才能分清誰是誰。當然不寫key值也可以,但這樣通常會報出警告,通知我們加上key值以提高react的效能。

image.png

演變過程: JSX > createElement > 虛擬dom (JS物件) > 真實dom

虛擬Dom的對比演算法

不同型別的元素

​ 每當根元素有不同型別,React將解除安裝舊樹並重新構建新樹。從<a><img>或從<Article><Comment>,或從<Button><div>,任何的調整都會導致全部重建。

​ 當樹被解除安裝,舊的DOM節點將被銷燬。元件例項會呼叫componentWillUnmount()。當構建一棵新樹,新的DOM節點被插入到DOM中。元件例項將依次呼叫componentWillMount()componentDidMount()。任何與舊樹有關的狀態都將丟棄。

​ 這個根節點下所有的元件都將會被解除安裝,同時他們的狀態將被銷燬。

相同型別的DOM元素

​ 當比較兩個相同型別的React DOM元素時,React則會觀察二者的屬性,保持相同的底層DOM節點,並僅更新變化的屬性。

相同型別的元件元素

​ 當元件更新時,例項仍保持一致,以讓狀態能夠在渲染之間保留。React通過更新底層元件例項的props來產生新元素,並在底層例項上依次呼叫componentWillReceiveProps()componentWillUpdate() 方法。

​ 接下來,render()方法被呼叫,同時對比演算法會遞迴處理之前的結果和新的結果。

React diff演算法流程圖

1545621734055

key的作用

​ React DOM 首先會比較元素內容先後的不同,而在渲染過程中只會更新改變了的部分。

​ key的重要性: 提高對比的效率

​ Keys可以在DOM中的某些元素被增加或刪除的時候幫助React識別哪些元素髮生了變化。因此你應當給陣列中的每一個元素賦予一個確定的標識。

​ 用陣列下標作為 key,看起來 key 值是唯一的,但是卻不是穩定不變的,隨著 todos陣列值的不同,同樣一個 Todoltem 例項在不同的更新過程中在陣列中的下標完全可能不同,把下標當做 key 就讓 React 徹底亂套了 。 ​ 需要注意,雖然 key 是一個 prop ,但是接受 key 的元件並不能讀取到 key 的值,因為 key 和 ref 是 React 保留的兩個特殊 prop ,並沒有預期讓元件直接訪問 。

為什麼使用setState修改資料?

​ 直接修改this.state的值,雖然事實上改變了元件的內部狀態,但只是野蠻地修改了state ,卻沒有驅動元件進行重新渲染,既然元件沒有重新渲染,當然不會反應 this.state值的變化;

​ 而 this.setState()函式所做的事情,首先是改變 this.state 的值,然後驅動元件經歷更新過程,這樣才有機會讓 this.state 裡新的值出現在介面上 。

setState 是非同步函式?

setState() 排隊更改元件的 state ,並通過更新 state來告訴 React,該元件及其子元件需要重新渲染。這是用於 響應事件處理程式 和 伺服器響應 更新使用者介面的主要方法。

​ 記住 setState() 作為一個請求,而不是立即命令來更新元件。為了更好的感知效能,React 可能會延遲它,然後合併多個setState()更新多個元件。React不保證state 更新就立即應用(重新渲染)。

​ React 可以將多個setState() 呼叫合併成一個呼叫來提高效能。

​ 因為 this.propsthis.state 可能是非同步更新的,你不應該依靠它們的值來計算下一個狀態。setState() 並不總是立即更新元件。它可能會 批量延遲到後面更新。這使得在呼叫 setState() 之後立即讀取 this.state 存在一個潛在的陷阱。 而使用 componentDidUpdate 或 setState 回撥(setState(updater, callback)),在應用更新後,都將被保證觸發。

舉個例子:

​ 例如,此程式碼可能無法更新計數器:

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});
複製程式碼

​ 要修復它,請使用第二種形式的 setState() 來接受一個函式而不是一個物件。 該函式將接收先前的狀態作為第一個引數,將此次更新被應用時的props做為第二個引數:

// Correct
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));
複製程式碼

setState()總是會導致重新渲染,除非 shouldComponentUpdate()返回 false 。如果可變物件被使用,並且條件渲染邏輯不能在shouldComponentUpdate() 中實現,只有當新state與先前 state 不同時呼叫 setState()才能避免不必要的重新渲染。

不能在render()裡面寫this.setState()會導致迴圈修改

React元件寫法

image.png

ES6的class類可以看作是建構函式的一個語法糖,可以把它當成建構函式來看,extends實現了類之間的繼承 —— 定義一個類Main 繼承React.Component所有的屬性和方法,元件的生命週期函式就是從這來的。

constructor是構造器,在例項化物件時呼叫,super呼叫了父類的constructor創造了父類的例項物件this,然後用子類的建構函式進行修改。

super(props)

​ 如果在建構函式中沒有呼叫super(props),那麼元件例項被構造之後,類例項的所有成員函式就無法通過 this.props 訪問到父元件傳遞過來的 props 值。很明顯,給 this.props 賦值是 React.Component 建構函式的工作之一 。

shouldCompnentUpdate生命週期

​ 在通用的 shouldCompnentUpdate 函式中做“淺層比較”,是一個被普遍接受的做法;如果需要做“深層比較”,那就是某個特定元件的行為,需要開發者自己根據元件情況去編寫 。

PureComponent

image.png

React15.3 中新加了一個類PureComponent,前身是 PureRenderMixin ,和 Component 基本一樣,只不過會在 render之前幫元件自動執行一次shallowEqual(淺比較),來決定是否更新元件,淺比較類似於淺複製,只會比較第一層。使用 PureComponent 相當於省去了寫 shouldComponentUpdate 函式,當元件更新時,如果元件的 propsstate

  1. 引用和第一層資料都沒發生改變, render 方法就不會觸發,這是我們需要達到的效果。
  2. 雖然第一層資料沒變,但引用變了,就會造成虛擬 DOM 計算的浪費。
  3. 第一層資料改變,但引用沒變,會造成不渲染,所以需要很小心的運算元據。

so. 為了效能,React只做了淺對比,於是就有了immutable.js

immutable.js

image.png

高階元件

高階元件就是一個函式,且該函式接受一個元件作為引數,並返回一個新的元件

const EnhancedComponent = higherOrderComponent(WrappedComponent);
複製程式碼

​ 對比元件將props屬性轉變成UI,高階元件則是將一個元件轉換成另一個新元件。

​ 高階元件在React第三方庫中很常見,比如Redux的connect方法和Relay的createContainer.

Refs屬性

建立 Refs

​ 使用 React.createRef() 建立 refs,通過 ref 屬性來獲得 React 元素。當構造元件時,refs 通常被賦值給例項的一個屬性,這樣你可以在元件中任意一處使用它們.

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}
複製程式碼

ref的值取決於節點的型別:

  • ref 屬性被用於一個普通的 HTML 元素時,React.createRef() 將接收底層 DOM 元素作為它的 current 屬性以建立 ref
  • ref 屬性被用於一個自定義類元件時,ref 物件將接收該元件已掛載的例項作為它的 current
  • 你不能在函式式元件上使用 ref 屬性,因為它們沒有例項。

React-Router路由

​ Router就是React的一個元件,它並不會被渲染,只是一個建立內部路由規則的配置物件,根據匹配的路由地址展現相應的元件。

​ Route則對路由地址和元件進行繫結,Route具有巢狀功能,表示路由地址的包涵關係,這和元件之間的巢狀並沒有直接聯絡。Route可以向繫結的元件傳遞7個屬性:children,history,location,params,route,routeParams,routes,每個屬性都包涵路由的相關的資訊。

​ 比較常用的有children(以路由的包涵關係為區分的元件),location(包括地址,引數,地址切換方式,key值,hash值)。

​ react-router提供Link標籤,這只是對a標籤的封裝,值得注意的是,點選連結進行的跳轉並不是預設的方式,react-router阻止了a標籤的預設行為並用pushState進行hash值的轉變。

​ 切換頁面的過程是在點選Link標籤或者後退前進按鈕時,會先發生url地址的轉變,Router監聽到地址的改變根據Route的path屬性匹配到對應的元件,將state值改成對應的元件並呼叫setState觸發render函式重新渲染dom。

路由(按需載入)

​ 當頁面比較多時,專案就會變得越來越大,尤其對於單頁面應用來說,初次渲染的速度就會很慢,這時候就需要按需載入,只有切換到頁面的時候才去載入對應的js檔案。react配合webpack進行按需載入的方法很簡單,Route的component改為getComponent,元件用require.ensure的方式獲取,並在webpack中配置chunkFilename。

const chooseProducts = (location, cb) => {
    require.ensure([], require => {
        cb(null, require('../Component/chooseProducts').default)
    },'chooseProducts')
}

const helpCenter = (location, cb) => {
    require.ensure([], require => {
        cb(null, require('../Component/helpCenter').default)
    },'helpCenter')
}

const saleRecord = (location, cb) => {
    require.ensure([], require => {
        cb(null, require('../Component/saleRecord').default)
    },'saleRecord')
}

const RouteConfig = (
    <Router history={history}>
        <Route path="/" component={Roots}>
            <IndexRoute component={index} />//首頁
            <Route path="index" component={index} />
            <Route path="helpCenter" getComponent={helpCenter} />//幫助中心
            <Route path="saleRecord" getComponent={saleRecord} />//銷售記錄
            <Redirect from='*' to='/'  />
        </Route>
    </Router>
);
複製程式碼

元件之間的通訊

​ react推崇的是單向資料流,通常被稱為自頂向下單向資料流。 任何狀態始終由某些特定元件所有,並且從該狀態匯出的任何資料或 UI 只能影響樹中下方的元件。

解決通訊問題的方法很多:

  1. 如果只是父子級關係,父級可以將一個回撥函式當作屬性傳遞給子級,子級可以直接呼叫函式從而和父級通訊。
  2. 元件層級巢狀到比較深,可以使用上下文getChildContext來傳遞資訊,這樣在不需要將函式一層層往下傳,任何一層的子級都可以通過this.context直接訪問。
  3. 兄弟關係的元件之間無法直接通訊,它們只能利用同一層的上級作為中轉站。而如果兄弟元件都是最高層的元件,為了能夠讓它們進行通訊,必須在它們外層再套一層元件,這個外層的元件起著儲存資料,傳遞資訊的作用,這其實就是redux所做的事情。
  4. 元件之間的資訊還可以通過全域性事件來傳遞。不同頁面可以通過引數傳遞資料,下個頁面可以用location.param來獲取。

React的事件委託

​ 我們在 JSX 中看到一個元件使用了 onClick,但並沒有產生直接使用 onclick (注意是 onclick 不是 onClick)的HTML ,而是使用了事件委託(event delegation)的方式處理點選事件,無論有多少個 onClick 出現,其實最後都只在 DOM 樹上新增了一個事件處理函式,掛在最頂層的 DOM 節點上。

​ 所有的點選事件都被這個事件處理函式捕獲,然後根據具體元件分配給特定函式,使用事件委託的效能當然要比為每個 onClick 都掛載一個事件處理函式要高 。 ​ 因為 React 控制了元件的生命週期,在 unmount 的時候自然能夠清除相關的所有事 件處理函式,記憶體洩露也不再是一個問題。

② 進階

Redux

基本原則

Flux 的基本原則是“單向資料流”, Redux 在此基礎上強調三個基本原則:

  • 唯一資料來源( Single Source of Truth);

    ​ 在 Flux 中,應用可以擁有多個 Store ,往往根據功能把應用的狀態 資料劃分給若干個 Store 分別儲存管理 。

    ​ Redux 對這個問題的解決方法就是,整個應用只保持一個 Store ,所有元件的資料來源 就是這個 Store 上的狀態 。

  • 保持狀態只讀( State is read-only);

    ​ 保持狀態只讀,就是說不能去直接修改狀態,要修改 Store 的狀態,必須要通過派發 一個 action 物件完成,這一點 ,和 Flux 的要求並沒有什麼區別 。

    ​ 當然,要驅動使用者介面渲染,就要改變應用的狀態,但是改變狀態的方法不是去修 改狀態上值,而是建立一個新的狀態物件返回給 Redux ,由 Redux 完成新的狀態的組裝 。

  • 資料改變只能通過純函式完成( Changes are made with pure functions ) 。

    ​ 在 Redux 中, 每個 reducer 的函式簽名如下所示 : ​ reducer(state , action ) ​ 第一個引數 state 是當前的狀態,第二個引數 action 是接收到的 action 物件,而 reducer函式要做的事情,就是根據 state 和 action 的值產生一個新的物件返回,注意 reducer 必須是純函式,也就是說函式的返回結果必須完全由引數 state 和 action 決定,而且不產生任何副作用,也不能修改引數 state 和 action 物件。

Redux核心API

Redux主要由三部分組成:store,reducer,action。

store

Redux的核心是store,它由Redux提供的 createStore(reducer, defaultState)這個方法生成,生成三個方法,getState(),dispatch(),subscrible()

image.png

  • getState():儲存的資料,狀態樹;
  • dispatch(action):分發action,並返回一個action,這是唯一能改變store中資料的方式;
  • subscrible(listener):註冊一個監聽者,store發生變化的時候被呼叫。

reducer

reducer是一個純函式,它根據previousState和action計算出新的state。 reducer(previousState,action)

image.png

action

action本質上是一個JavaScript物件,其中必須包含一個type欄位來表示將要執行的動作,其他的欄位都可以根據需求來自定義。

const ADD_TODO = 'ADD_TODO'
複製程式碼
{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}
複製程式碼

整合

他們三者之間的互動,可以由下圖概括:

image.png

image.png

概念分析:

redux主要由三部分組成:store,reducer,action。

store是一個物件,它有四個主要的方法:

1、dispatch:

​ 用於action的分發——在createStore中可以用middleware中介軟體對dispatch進行改造,比如當action傳入dispatch會立即觸發reducer,有些時候我們不希望它立即觸發,而是等待非同步操作完成之後再觸發,這時候用redux-thunk對dispatch進行改造,以前只能傳入一個物件,改造完成後可以傳入一個函式,在這個函式裡我們手動dispatch一個action物件,這個過程是可控的,就實現了非同步。

2、subscribe:

​ 監聽state的變化——這個函式在store呼叫dispatch時會註冊一個listener監聽state變化,當我們需要知道state是否變化時可以呼叫,它返回一個函式,呼叫這個返回的函式可以登出監聽。

let unsubscribe = store.subscribe(() => {console.log('state發生了變化')})

3、getState:

​ 獲取store中的state——當我們用action觸發reducer改變了state時,需要再拿到新的state裡的資料,畢竟資料才是我們想要的。

​ getState主要在兩個地方需要用到,一是在dispatch拿到action後store需要用它來獲取state裡的資料,並把這個資料傳給reducer,這個過程是自動執行的,二是在我們利用subscribe監聽到state發生變化後呼叫它來獲取新的state資料,如果做到這一步,說明我們已經成功了。

4、replaceReducer:

替換reducer,改變state修改的邏輯。

​ store可以通過createStore()方法建立,接受三個引數,經過combineReducers合併的reducer和state的初始狀態以及改變dispatch的中介軟體,後兩個引數並不是必須的。store的主要作用是將action和reducer聯絡起來並改變state。

action:

​ action是一個物件,其中type屬性是必須的,同時可以傳入一些資料。action可以用actionCreactor進行創造。dispatch就是把action物件傳送出去。

reducer:

​ reducer是一個函式,它接受一個state和一個action,根據action的type返回一個新的state。根據業務邏輯可以分為很多個reducer,然後通過combineReducers將它們合併,state樹中有很多物件,每個state物件對應一個reducer,state物件的名字可以在合併時定義。

const reducer = combineReducers({
     a: doSomethingWithA,
     b: processB,
     c: c
})
複製程式碼

combineReducers:

​ 其實它也是一個reducer,它接受整個state和一個action,然後將整個state拆分傳送給對應的reducer進行處理,所有的reducer會收到相同的action,不過它們會根據action的type進行判斷,有這個type就進行處理然後返回新的state,沒有就返回預設值,然後這些分散的state又會整合在一起返回一個新的state樹。

流程分析:

  1. 首先呼叫store.dispatchaction作為引數傳入,同時用getState獲取當前的狀態樹state並註冊subscribelistener監聽state變化,再呼叫combineReducers並將獲取的stateaction傳入。
  2. combineReducers會將傳入的state和action傳給所有reducer,並根據action的type返回新的state,觸發state樹的更新,我們呼叫subscribe監聽到state發生變化後用getState獲取新的state資料。

redux的state和react的state兩者完全沒有關係,除了名字一樣。


React-Redux

React-redux是怎麼配合的

react-redux 的兩個最主要功能:

  • connect :連線容器元件和檢視元件;
  • Provider :提供包含 store 的 context。
  1. react-redux提供了connect和Provider兩個好基友,它們一個將元件與redux關聯起來,一個將store傳給元件。
  2. 元件通過dispatch發出action,store根據action的type屬性呼叫對應的reducer並傳入state和這個action,reducer對state進行處理並返回一個新的state放入store,connect監聽到store發生變化,呼叫setState更新元件,此時元件的props也就跟著變化。
  3. 值得注意的是connect,Provider,mapStateToProps,mapDispatchToProps是react-redux提供的,redux本身和react沒有半毛錢關係,它只是資料處理中心,沒有和react產生任何耦合,是react-redux讓它們聯絡在一起。

Redux 本身和React沒有關係,只是資料處理中心,是React-Redux讓他們聯絡在一起。

React-Redux的兩個方法

connect

掘金資料

connect連線React元件和Redux store。connect實際上是一個高階函式,返回一個新的已與 Redux store 連線的元件類。

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)
複製程式碼

TodoList是 UI 元件,VisibleTodoList就是由 react-redux 通過connect方法自動生成的容器元件。

  1. mapStateToProps:從Redux狀態樹中提取需要的部分作為props傳遞給當前的元件。
  2. mapDispatchToProps:將需要繫結的響應事件(action)作為props傳遞到元件上。

**書籍資料 **

export default connect(mapStateToProps, mapDispatchToProps) ( Counter);
複製程式碼

這個 connect 函式具體做了什麼工作呢? 作為容器元件,要做的工作無外乎兩件事:

  • 把 Store 上的狀態轉化為內層傻瓜元件的 prop;
  • 把內層傻瓜元件中的使用者動作轉化為派送給 Store 的動作 。

image.png

Provider

Provider實現store的全域性訪問,將store傳給每個元件。

原理:使用React的context,context可以實現跨元件之間的傳遞。

如果只使用redux,那麼流程是這樣的:

component --> dispatch(action) --> reducer --> subscribe --> getState --> component

用了react-redux之後流程是這樣的:

component --> actionCreator(data) --> reducer --> component

store的三大功能:dispatch,subscribe,getState都不需要手動來寫了。

react-redux幫我們做了這些,同時它提供了兩個好基友Provider和connect。

Provider是一個元件,它接受store作為props,然後通過context往下傳,這樣react中任何元件都可以通過context獲取store。

​ 也就意味著我們可以在任何一個元件裡利用dispatch(action)來觸發reducer改變state,並用subscribe監聽state的變化,然後用getState獲取變化後的值。但是並不推薦這樣做,它會讓資料流變的混亂,過度的耦合也會影響元件的複用,維護起來也更麻煩。

connect --connect(mapStateToProps, mapDispatchToProps, mergeProps, options) 是一個函式,它接受四個引數並且再返回一個函式--wrapWithConnect,wrapWithConnect接受一個元件作為引數wrapWithConnect(component),它內部定義一個新元件Connect(容器元件)並將傳入的元件(ui元件)作為Connect的子元件然後return出去。

所以它的完整寫法是這樣的:`connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(component)

mapStateToProps(state, [ownProps]):

mapStateToProps 接受兩個引數,store的state和自定義的props,並返回一個新的物件,這個物件會作為props的一部分傳入ui元件。我們可以根據元件所需要的資料自定義返回一個物件。ownProps的變化也會觸發mapStateToProps

function mapStateToProps(state) {
   return { todos: state.todos };
}
複製程式碼

mapDispatchToProps(dispatch, [ownProps]):

mapDispatchToProps如果是物件,那麼會和store繫結作為props的一部分傳入ui元件。

如果是個函式,它接受兩個引數,bindActionCreators會將action和dispatch繫結並返回一個物件,這個物件會和ownProps一起作為props的一部分傳入ui元件。

所以不論mapDispatchToProps是物件還是函式,它最終都會返回一個物件,如果是函式,這個物件的key值是可以自定義的

function mapDispatchToProps(dispatch) {
   return {
      todoActions: bindActionCreators(todoActionCreators, dispatch),
      counterActions: bindActionCreators(counterActionCreators, dispatch)
   };
}
複製程式碼

mapDispatchToProps返回的物件其屬性其實就是一個個actionCreator,因為已經和dispatch繫結,所以當呼叫actionCreator時會立即傳送action,而不用手動dispatch。ownProps的變化也會觸發mapDispatchToProps。

mergeProps(stateProps, dispatchProps, ownProps):

將mapStateToProps() 與 mapDispatchToProps()返回的物件和元件自身的props合併成新的props並傳入元件。預設返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的結果。

options:

pure = true 表示Connect容器元件將在shouldComponentUpdate中對store的state和ownProps進行淺對比,判斷是否發生變化,優化效能。為false則不對比。

其實connect函式並沒有做什麼,大部分的邏輯都是在它返回的wrapWithConnect函式內實現的,確切的說是在wrapWithConnect內定義的Connect元件裡實現的。


在專案中我使用的大store目錄結構是:

image.png

// index.js
import {createStore, compose, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import reducers from './reducers';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducers, composeEnhancers(
  applyMiddleware(thunk)
));

export default store;
複製程式碼
// reducers.js
// 合併小的reducer
import {combineReducers} from 'redux-immutable'; // 提供的是immutable資料
import {reducer as userReducer} from './user'
import {reducer as chatUserReducer} from './chat_user'
import {reducer as chatReducer} from './chat'

const reducer = combineReducers({
  user: userReducer,
  chatUser: chatUserReducer,
  chat: chatReducer
});

export default reducer;
複製程式碼

在專案中我使用的小store(舉例)目錄結構是:

image.png

核心程式碼:

// _reducer.js
import * as constants from './constants'
import {getRedirectPath} from '../../common/js/util'

const initState = {
  isAuth: false,
  msg: '',
  user: '',
  pwd: '',
  type: ''
}

const defaultState = (localStorage.getItem('jobUser') && JSON.parse(localStorage.getItem('jobUser'))) || initState


export default (state = defaultState, action) => {
  switch (action.type) {
    case constants.AUTH_SUCCESS:
      localStorage.setItem('jobUser', JSON.stringify({
        ...state,
        msg: '',
        redirectTo: getRedirectPath(action.payload), ...action.payload
      }))
      return {...state, msg: '', redirectTo: getRedirectPath(action.payload), ...action.payload}
    case constants.LOAD_DATA:
      return {...state, ...action.payload}
    case constants.ERROR_MSG:
      return {...state, isAuth: false, msg: action.msg}
    case constants.LOGIN_OUT:
      return {redirectTo: '/login', ...initState}
    default:
      return state
  }
}
複製程式碼
// actionCreators.js
import * as constants from './constants'
import axios from 'axios'
const authSuccess = (obj) => {
  const {pwd, ...data} = obj
  return {type: constants.AUTH_SUCCESS, payload: data}
}
const errorMsg = (msg) => {
  return {msg, type: constants.ERROR_MSG}
}

// 註冊
export function register({user, pwd, repeatpwd, type}) {
  if (!user || !pwd || !type) {
    return errorMsg('使用者名稱密碼必須輸入')
  }
  if (pwd !== repeatpwd) {
    return errorMsg('密碼和確認密碼不同')
  }

  return dispatch => {
    axios.post('/user/register', {user, pwd, type})
      .then(res => {
        if (res.status === 200 && res.data.code === 0) {
          dispatch(authSuccess(res.data.data))
        } else {
          dispatch(errorMsg(res.data.msg))
        }
      })
  }
}

// 登入
export function login({user, pwd}) {
  if (!user || !pwd) {
    return errorMsg('使用者名稱密碼必須輸入')
  }
  return dispatch => {
    axios.post('/user/login', {user, pwd})
      .then(res => {
        if (res.status === 200 && res.data.code === 0) {
          dispatch(authSuccess(res.data.data))
        } else {
          dispatch(errorMsg(res.data.msg))
        }
      })
  }
}

// 登出
export function logoutSubmit() {
  return {type: constants.LOGIN_OUT}
}

// 修改
export function update(data) {
  return dispatch => {
    axios.post('/user/update', data)
      .then(res => {
        if (res.status === 200 && res.data.code === 0) {
          dispatch(authSuccess(res.data.data[0]))
        } else {
          dispatch(errorMsg(res.data.msg))
        }
      })
  }
}
複製程式碼
// constants.js
export  const AUTH_SUCCESS = 'AUTH_SUCCESS'
export const LOGIN_OUT = 'LOGIN_OUT'
export const ERROR_MSG = 'ERROR_MSG'
export const LOAD_DATA = 'LOAD_DATA'
複製程式碼
// index.js
import reducer from './_reducer'
import * as actionCreators from './actionCreators'
import * as constants from './constants'

export {reducer, actionCreators, constants}
複製程式碼

完整的 react --> redux --> react 流程

一、Provider元件接受redux的store作為props,然後通過context往下傳。

二、

  1. connect函式在初始化的時候會將mapDispatchToProps物件繫結到store,

  2. 如果mapDispatchToProps是函式則在Connect元件獲得store後,根據傳入的store.dispatch和action通過bindActionCreators進行繫結,再將返回的物件繫結到store,connect函式會返回一個wrapWithConnect函式,同時wrapWithConnect會被呼叫且傳入一個ui元件,wrapWithConnect內部使用class Connect extends Component定義了一個Connect元件,傳入的ui元件就是Connect的子元件,

  3. 然後Connect元件會通過context獲得store,並通過store.getState獲得完整的state物件,將state傳入mapStateToProps返回stateProps物件、mapDispatchToProps物件或mapDispatchToProps函式會返回一個dispatchProps物件,stateProps、dispatchProps以及Connect元件的props三者通過Object.assign(),或者mergeProps合併為props傳入ui元件。然後在ComponentDidMount中呼叫store.subscribe,註冊了一個回撥函式handleChange監聽state的變化。

三、

  1. 此時ui元件就可以在props中找到actionCreator,當我們呼叫actionCreator時會自動呼叫dispatch,在dispatch中會呼叫getState獲取整個state,同時註冊一個listener監聽state的變化,store將獲得的state和action傳給combineReducers,
  2. combineReducers會將state依據state的key值分別傳給子reducer,並將action傳給全部子reducer,reducer會被依次執行進行action.type的判斷,如果有則返回一個新的state,如果沒有則返回預設。
  3. combineReducers再次將子reducer返回的單個state進行合併成一個新的完整的state。此時state發生了變化。
  4. dispatch在state返回新的值之後會呼叫所有註冊的listener函式其中包括handleChange函式,handleChange函式內部首先呼叫getState獲取新的state值並對新舊兩個state進行淺對比,如果相同直接return,如果不同則呼叫mapStateToProps獲取stateProps並將新舊兩個stateProps進行淺對比,如果相同,直接return結束,不進行後續操作。
  5. 如果不相同則呼叫this.setState()觸發Connect元件的更新,傳入ui元件,觸發ui元件的更新,此時ui元件獲得新的props,react --> redux --> react 的一次流程結束。

上面的有點複雜,簡化版的流程是:

一、Provider元件接受redux的store作為props,然後通過context往下傳。

二、connect函式收到Provider傳出的store,然後接受三個引數mapStateToProps,mapDispatchToProps和元件,並將state和actionCreator以props傳入元件,這時元件就可以呼叫actionCreator函式來觸發reducer函式返回新的state,connect監聽到state變化呼叫setState更新元件並將新的state傳入元件。

connect可以寫的非常簡潔,mapStateToProps,mapDispatchToProps只不過是傳入的回撥函式,connect函式在必要的時候會呼叫它們,名字不是固定的,甚至可以不寫名字。

簡化版本:

connect(state => state, action)(Component);
複製程式碼

redux以及react-redux到底是怎麼實現的?

image.png

總結

下圖闡述了它們三者之間的工作流程:

image.png


redux-thunk 中介軟體

程式碼示例:

function createThunkMiddleware(extraArgument) {
	return ({ dispatch , getState }) => next => action=> {
	if (typeof action === ’ function ’){
		return action(dispatch , getState , extraArgument);
	}
		return next(action);
	}
}
const thunk= createThunkMiddleware();
export default thunk;
複製程式碼

​ 我們看 redux-thunk 這一串函式中最裡層的函式,也就是實際處理每個 action 物件的函式。 首先檢查引數 action 的型別,如果是函式型別的話,就執行這個 action 函式,把dispatch 和 getState 作為引數傳遞進去,否則就呼叫 next 讓下一個中介軟體繼續處理 action,這個處理過程和 redux-thunk 文件中描述的功能一致。

Redux的單向資料流是同步操作,驅動 Redux 流程的 是 action 物件, 每一個 action物件被派發到 Store 上之後,同步地被分配給所有的 reducer 函式,每個 reducer 都是純函式,純函式不產生任何副作用,自然是完成資料操作之後立刻同步返回, reducer 返回的結果又被同步地拿去更新 Store 上的狀態資料,更新狀態資料的操作會立刻被同步給監聽Store 狀態改變的函式,從而引發作為檢視的 React 元件更新過程。

​ 當我們想要讓 Redux 幫忙處理一個非同步操作的時候,程式碼一樣也要派發一個 action物件,畢竟 Redux 單向資料流就是由 action 物件驅動的 。 但是這個引發非同步操作的action 物件比較特殊,我們叫它們“非同步 action 物件” 。 ​ 前面例子中的 action 建構函式返回的都是一個普通的物件,這個物件包含若干欄位,其中必不可少的欄位是 type ,但是“非同步 action 物件”不是一個普通 JavaScript 物件,而是一個函式 。 ​ 如果沒有 redux-thunk 中介軟體的存在 這樣一個函式型別的 action 物件被派發出來會一路傳送到各個 reducer 函式, reducer 函式從這些實際上是函式的 action 物件上是無法獲得 type 欄位的,所以也做不了什麼實質的處理。

​ 不過,有了redux-thunk中介軟體之後,這些 action 物件根本沒有機會觸及到 reducer函式,在中介軟體一層就被 redux-thunk 截獲 。

redux-thunk 的工作是檢查 action 物件是不是函式,如果不是函式就放行,完成普通action 物件的生命週期,而如果發現 action 物件是函式,那就執行這個函式,並把 Store的 dispatch 函式和 getState 函式作為引數傳遞到函式中去,處理過程到此為止,不會讓這個非同步 action 物件繼續往前派發到 reducer 函式 。

React中介軟體機制

​ 在 Redux框架中,中介軟體處理的是 action 物件,而派發 action 物件的就是 Store 上的dispatch 函式,之前介紹過通過 dispatch 派發的 action 物件會進入 reducer 。 在 action 物件進入 reducer 之前,會經歷中介軟體的管道 。

​ 在這個中介軟體管道中,每個中介軟體都會接收到 action 物件,在處理完畢之後,就會把 action 物件交給下一個中介軟體來處理,只有所有的中介軟體都處理完 action 物件之後,在這個中介軟體管道中,每個中介軟體都會接收到 action 物件,在處理完畢之後,就會把 action 物件交給下一個中介軟體來處理,只有所有的中介軟體都處理完 action 物件之後,才輪到 reducer 來處理 action 物件,然而,如果某個中介軟體覺得沒有必要繼續處理這個action 物件了,就不會把 action 物件交給下一個中介軟體,對這個 action 物件的處理就此中止,也就輪不到 reducer 上場了 。

​ 每個中介軟體必須要定義成一個函式,返回一個接受 next 引數的函式,而這個接受next 引數的函式又返回一個接受 action 引數的函式 。 next 引數本身也是一個函式,中介軟體呼叫這個 next 函式通知 Redux 自己的處理工作已經結束 。

程式碼舉例:

// 一個實際上什麼事都不做的中介軟體程式碼如下:
function doNothingMiddleware{{dispatch, getState)) {
	return function {next) {
		return function {action) {
			return next{action)
     }
	}
}
複製程式碼

​ 以 action 為引數的函式對傳人的 action 物件進行處理,因為 JavaScript 支援閉包 ( Clousure ),在這個函式裡可以訪問上面兩層函式的引數,所以可以根據需要做很多事 情,包括以下功能:

  • 呼叫 dispatch 派發出一個新 action 物件;
  • 呼叫 getState 獲得當前 Redux Store 上的狀態;
  • 呼叫 next 告訴 Redux 當前中介軟體工作完畢,讓 Redux 呼叫下一個中介軟體;
  • 訪問 action 物件 action 上的所有資料。 具有上面這些功能,一箇中介軟體足夠獲取 Store 上的所有資訊,也具有足夠能力控制資料的流轉 。

中介軟體用於擴充套件 dispatch 函式的功能,多箇中介軟體實際構成了一個處理 action 物件的管道, action 物件被這個管道中所有中介軟體依次處理過之後,才有機會被 reducer 處理。

③ 起步

上面說了react,react-router和redux的知識點。但是怎麼樣將它們整合起來,搭建一個完整的專案。

1、先引用 react.js,redux,react-router 等基本檔案,建議用npm安裝,直接在檔案中引用。

2、從 react.js,redux,react-router 中引入所需要的物件和方法。

import React, {Component, PropTypes} from 'react';
import ReactDOM, {render} from 'react-dom';
import {Provider, connect} from 'react-redux';
import {createStore, combineReducers, applyMiddleware} from 'redux';
import { Router, Route, Redirect, IndexRoute, browserHistory, hashHistory } from 'react-router';
複製程式碼

3、根據需求建立頂層ui元件,每個頂層ui元件對應一個頁面。

4、建立actionCreators和reducers,並用combineReducers將所有的reducer合併成一個大的reduer。利用createStore建立store並引入combineReducers和applyMiddleware。

5、利用connect將actionCreator,reuder和頂層的ui元件進行關聯並返回一個新的元件。

6、利用connect返回的新的元件配合react-router進行路由的部署,返回一個路由元件Router。

7、將Router放入最頂層元件Provider,引入store作為Provider的屬性。

8、呼叫render渲染Provider元件且放入頁面的標籤中。

可以看到頂層的ui元件其實被套了四層元件,Provider,Router,Route,Connect,這四個元件並不會在檢視上改變react,它們只是功能性的。


Github地址: wq93

相關文章