① 初探
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框架。
組件的生命周期
組件在初始化時會觸發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個函數可以滿足我們所有對組件操作的需求,利用的好可以提高開發效率和組件性能。
? render
和 shouldComponentUpdate
函數,也是 React 生命周期函數中唯二兩個要求有返回結果的函數。 render
函數的返回結果將用于構造 DOM 對象,而 shouldComponentUpdate
函數返回一個布爾值,告訴 React 庫這個組件在這次更新過程中是否要繼續 。
V16 生命周期函數用法建議
class ExampleComponent extends React.Component {// 用于初始化 stateconstructor() {}// 用于替換 `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()函數,只是一個函數,沒有聲明周期函數,效率更高
React的 Diff算法
? 當組件更新的時候,react會創建一個新的虛擬dom樹并且會和之前儲存的dom樹進行比較,這個比較多過程就用到了diff算法,所以組件初始化的時候是用不到的。
? react提出了一種假設,相同的節點具有類似的結構,而不同的節點具有不同的結構。在這種假設之上進行逐層的比較,如果發現對應的節點是不同的,那就直接刪除舊的節點以及它所包含的所有子節點然后替換成新的節點。如果是相同的節點,則只進行屬性的更改。
? 對于列表的diff算法稍有不同,因為列表通常具有相同的結構,在對列表節點進行刪除,插入,排序的時候,單個節點的整體操作遠比一個個對比一個個替換要好得多,所以在創建列表的時候需要設置key值,這樣react才能分清誰是誰。當然不寫key值也可以,但這樣通常會報出警告,通知我們加上key值以提高react的性能。
演變過程: 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算法流程圖
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.props
和 this.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組件寫法
ES6的class類可以看作是構造函數的一個語法糖,可以把它當成構造函數來看,extends實現了類之間的繼承 —— 定義一個類Main 繼承React.Component所有的屬性和方法,組件的生命周期函數就是從這來的。
constructor是構造器,在實例化對象時調用,super調用了父類的constructor創造了父類的實例對象this,然后用子類的構造函數進行修改。
super(props)
? 如果在構造函數中沒有調用super(props)
,那么組件實例被構造之后,類實例的所有成員函數就無法通過 this.props 訪問到父組件傳遞過來的 props 值。很明顯,給 this.props
賦值是 React.Component
構造函數的工作之一 。
shouldCompnentUpdate生命周期
? 在通用的 shouldCompnentUpdate 函數中做“淺層比較”,是一個被普遍接受的做法;如果需要做“深層比較”,那就是某個特定組件的行為,需要開發者自己根據組件情況去編寫 。
PureComponent
? React15.3
中新加了一個類PureComponent,前身是 PureRenderMixin
,和 Component
基本一樣,只不過會在 render
之前幫組件自動執行一次shallowEqual(淺比較),來決定是否更新組件,淺比較類似于淺復制,只會比較第一層。使用 PureComponent
相當于省去了寫 shouldComponentUpdate
函數,當組件更新時,如果組件的 props
和 state
:
- 引用和第一層數據都沒發生改變,
render
方法就不會觸發,這是我們需要達到的效果。 - 雖然第一層數據沒變,但引用變了,就會造成虛擬
DOM
計算的浪費。 - 第一層數據改變,但引用沒變,會造成不渲染,所以需要很小心的操作數據。
so. 為了性能,React只做了淺對比,于是就有了immutable.js
immutable.js
高階組件
? 高階組件就是一個函數,且該函數接受一個組件作為參數,并返回一個新的組件
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 只能影響樹中下方
的組件。
解決通信問題的方法很多:
- 如果只是父子級關系,父級可以將一個回調函數當作屬性傳遞給子級,子級可以直接調用函數從而和父級通信。
- 組件層級嵌套到比較深,可以使用上下文getChildContext來傳遞信息,這樣在不需要將函數一層層往下傳,任何一層的子級都可以通過this.context直接訪問。
- 兄弟關系的組件之間無法直接通信,它們只能利用同一層的上級作為中轉站。而如果兄弟組件都是最高層的組件,為了能夠讓它們進行通信,必須在它們外層再套一層組件,這個外層的組件起著保存數據,傳遞信息的作用,這其實就是redux所做的事情。
- 組件之間的信息還可以通過全局事件來傳遞。不同頁面可以通過參數傳遞數據,下個頁面可以用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()
。
- getState():存儲的數據,狀態樹;
- dispatch(action):分發action,并返回一個action,這是唯一能改變store中數據的方式;
- subscrible(listener):注冊一個監聽者,store發生變化的時候被調用。
reducer
reducer是一個純函數,它根據previousState和action計算出新的state。 reducer(previousState,action)
action
action本質上是一個JavaScript對象,其中必須包含一個type字段來表示將要執行的動作,其他的字段都可以根據需求來自定義。
const ADD_TODO = 'ADD_TODO'
復制代碼
{type: ADD_TODO,text: 'Build my first Redux app'
}
復制代碼
整合
他們三者之間的交互,可以由下圖概括:
概念分析:
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樹。
流程分析:
- 首先調用
store.dispatch
將action
作為參數傳入,同時用getState
獲取當前的狀態樹state
并注冊subscribe
的listener
監聽state
變化,再調用combineReducers
并將獲取的state
和action
傳入。 - 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。
- react-redux提供了connect和Provider兩個好基友,它們一個將組件與redux關聯起來,一個將store傳給組件。
- 組件通過dispatch發出action,store根據action的type屬性調用對應的reducer并傳入state和這個action,reducer對state進行處理并返回一個新的state放入store,connect監聽到store發生變化,調用setState更新組件,此時組件的props也就跟著變化。
- 值得注意的是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方法自動生成的容器組件。
mapStateToProps
:從Redux狀態樹中提取需要的部分作為props傳遞給當前的組件。mapDispatchToProps
:將需要綁定的響應事件(action)作為props傳遞到組件上。
**書籍資料 **
export default connect(mapStateToProps, mapDispatchToProps) ( Counter);
復制代碼
這個 connect 函數具體做了什么工作呢? 作為容器組件,要做的工作無外乎兩件事:
- 把 Store 上的狀態轉化為內層傻瓜組件的 prop;
- 把內層傻瓜組件中的用戶動作轉化為派送給 Store 的動作 。
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目錄結構是:
// 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(舉例)目錄結構是:
核心代碼:
// _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'))) || initStateexport 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} = objreturn {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往下傳。
二、
-
connect函數在初始化的時候會將mapDispatchToProps對象綁定到store,
-
如果mapDispatchToProps是函數則在Connect組件獲得store后,根據傳入的store.dispatch和action通過bindActionCreators進行綁定,再將返回的對象綁定到store,connect函數會返回一個wrapWithConnect函數,同時wrapWithConnect會被調用且傳入一個ui組件,wrapWithConnect內部使用class Connect extends Component定義了一個Connect組件,傳入的ui組件就是Connect的子組件,
-
然后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的變化。
三、
- 此時ui組件就可以在props中找到actionCreator,當我們調用actionCreator時會自動調用dispatch,在dispatch中會調用getState獲取整個state,同時注冊一個listener監聽state的變化,store將獲得的state和action傳給combineReducers,
- combineReducers會將state依據state的key值分別傳給子reducer,并將action傳給全部子reducer,reducer會被依次執行進行action.type的判斷,如果有則返回一個新的state,如果沒有則返回默認。
- combineReducers再次將子reducer返回的單個state進行合并成一個新的完整的state。此時state發生了變化。
- dispatch在state返回新的值之后會調用所有注冊的listener函數其中包括handleChange函數,handleChange函數內部首先調用getState獲取新的state值并對新舊兩個state進行淺對比,如果相同直接return,如果不同則調用mapStateToProps獲取stateProps并將新舊兩個stateProps進行淺對比,如果相同,直接return結束,不進行后續操作。
- 如果不相同則調用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到底是怎么實現的?
總結
下圖闡述了它們三者之間的工作流程:
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,它們只是功能性的。