學習React的只知其一;不知其二

① 初探

HMTL的渲染過程

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

純函數

React 的理念 ,歸結爲一個公式,就像下面這樣 : ​ UI=render(data) ​ 讓咱們來看看這個公式表達的含義,用戶看到的界面( UI),應該是一個函數(在這裏叫 render)的執行結果,只接受數據( data)做爲參數 。react

​ 這個函數是一個純函數,所謂純函數,指的是沒有任何反作用,輸出徹底依賴於輸入的函數,兩次函數調用若是輸人 ​ 相同,獲得的結果也絕對相同 。 如此一來,最終的用戶界面,在 render 函數肯定的狀況下徹底取決於輸入數據 。webpack

React初解

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

PS: react感受相似VScode或sublime須要裝各類插件來寫代碼,而angular就像webstorm同樣集成了不少功能git

React 是什麼

​ 用腳本進行DOM操做的代價很昂貴。把DOM和JavaScript各自想象爲一個島嶼,它們之間用收費橋樑鏈接,js每次訪問DOM,都要途徑這座橋,並交納「過橋費」,訪問DOM的次數越多,費用也就越高。es6

​ 所以,推薦的作法是儘可能減小過橋的次數,努力待在ECMAScript島上。由於這個緣由react的虛擬dom就顯得難能難得了,它創造了虛擬dom而且將它們儲存起來,每當狀態發生變化的時候就會創造新的虛擬節點和之前的進行對比,讓變化的部分進行渲染。github

整個過程沒有對dom進行獲取和操做,只有一個渲染的過程,因此react說是一個ui框架。web

組件的生命週期

image.png

組件在初始化時會觸發5個鉤子函數:算法

一、getDefaultProps()npm

​ 設置默認的props,也能夠用defaultProps設置組件的默認屬性。

這個函數只在 React.createClass 方法創造的組件類纔會用到 。

二、getInitialState()

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

​ 這個函數只在 React.createClass 方法創造的組件類纔會用到 。

三、componentWillMount()

​ 組件初始化時只調用,之後組件更新不調用,整個生命週期只調用一次,此時能夠修改state。

四、 render()

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

​ 一般一個組件要發揮做用,老是要渲染一些東西, render 函數並不作實際的誼染動做,它只是返回一個 JSX 描述的結構,最終由 React 來操做渲染過程。 ​ 固然,某些特殊組件的做用不是渲染界面,或者,組件在某些狀況下選擇沒有東西可畫,那就讓 render 函數返回一個 null 或者 false ,等於告訴 React,這個組件此次不須要渲染任何 DOM 元素 。 ​ 須要注意, render 函數應該是一個純函數,徹底根據 this.state 和 this.props 來決定返回的結果,並且不要產生任何反作用。在 render 函數中去調用 this.setState 毫無疑問是錯誤的,由於一個純函數不該該引發狀態的改變。

五、componentDidMount()

​ Render 函數返回的東西已 經引起了渲染,組件已經被「裝載」到了 DOM 樹上 。 組件渲染以後調用,能夠經過this.getDOMNode()獲取和操做dom節點,只調用一次。


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

六、componentWillReceivePorps(nextProps)

組件初始化時不調用,組件接受新的props時調用。

七、shouldComponentUpdate(nextProps, nextState)

​ React性能優化很是重要的一環。組件接受新的state或者props時調用,咱們能夠設置在此對比先後兩個props和state是否相同,若是相同則返回false阻止更新,由於相同的屬性狀態必定會生成相同的dom樹,這樣就不須要創造新的dom樹和舊的dom樹進行diff算法對比,節省大量性能,尤爲是在dom結構複雜的時候。不過調用this.forceUpdate會跳過此步驟。

八、componentWillUpdate(nextProps, nextState)

組件初始化時不調用,只有在組件將要更新時才調用,此時能夠修改state

九、render()

當組件的state或者props發生改變的時候,render函數就會從新執行

十、componentDidUpdate()

組件初始化時不調用,組件更新完成後調用,此時能夠獲取dom節點。

還有一個卸載鉤子函數

十一、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是一個對象,它有四個主要的方法:

一、dispatch:

​ 用於action的分發——在createStore中能夠用middleware中間件對dispatch進行改造,好比當action傳入dispatch會當即觸發reducer,有些時候咱們不但願它當即觸發,而是等待異步操做完成以後再觸發,這時候用redux-thunk對dispatch進行改造,之前只能傳入一個對象,改造完成後能夠傳入一個函數,在這個函數裏咱們手動dispatch一個action對象,這個過程是可控的,就實現了異步。

二、subscribe:

​ 監聽state的變化——這個函數在store調用dispatch時會註冊一個listener監聽state變化,當咱們須要知道state是否變化時能夠調用,它返回一個函數,調用這個返回的函數能夠註銷監聽。

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

三、getState:

​ 獲取store中的state——當咱們用action觸發reducer改變了state時,須要再拿到新的state裏的數據,畢竟數據纔是咱們想要的。

​ getState主要在兩個地方須要用到,一是在dispatch拿到action後store須要用它來獲取state裏的數據,並把這個數據傳給reducer,這個過程是自動執行的,二是在咱們利用subscribe監聽到state發生變化後調用它來獲取新的state數據,若是作到這一步,說明咱們已經成功了。

四、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 流程

1、Provider組件接受redux的store做爲props,而後經過context往下傳。

2、

  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的變化。

3、

  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 的一次流程結束。

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

1、Provider組件接受redux的store做爲props,而後經過context往下傳。

2、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的知識點。可是怎麼樣將它們整合起來,搭建一個完整的項目。

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

二、從 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';
複製代碼

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

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

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

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

七、將Router放入最頂層組件Provider,引入store做爲Provider的屬性。

八、調用render渲染Provider組件且放入頁面的標籤中。

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


Github地址: wq93

相關文章
相關標籤/搜索