学习React的一知半解
① 初探
HMTL的渲染過程
? 這個結(jié)構(gòu)化文本就是 HTML 文本, HTML 中的每個元素都對應(yīng) DOM中某個節(jié)點,這樣,因為 HTML 元素的逐級包含關(guān)系, DOM 節(jié)點自然就構(gòu)成了一個樹形結(jié)構(gòu),稱為 DOM 樹 。 ? 瀏覽器為了渲染 HTML 格式的網(wǎng)頁,會先將 HTML 文本解析以構(gòu)建 DOM 樹,然后根據(jù) DOM 樹渲染出用戶看到的界面,當(dāng)要改變界面內(nèi)容的時候,就去改變 DOM 樹上的節(jié)點 。
純函數(shù)
React 的理念 ,歸結(jié)為一個公式,就像下面這樣 : ? UI=render(data) ? 讓我們來看看這個公式表達的含義,用戶看到的界面( UI),應(yīng)該是一個函數(shù)(在這里叫 render)的執(zhí)行結(jié)果,只接受數(shù)據(jù)( data)作為參數(shù) 。
? 這個函數(shù)是一個純函數(shù),所謂純函數(shù),指的是沒有任何副作用,輸出完全依賴于輸入的函數(shù),兩次函數(shù)調(diào)用如果輸人 ? 相同,得到的結(jié)果也絕對相同 。 如此一來,最終的用戶界面,在 render 函數(shù)確定的情況下完全取決于輸入數(shù)據(jù) 。
React初解
? react的功能其實很單一,主要負責(zé)渲染的功能,現(xiàn)有的框架,比如angular是一個大而全的框架,用了angular幾乎就不需要用其他工具輔助配合.
PS: react感覺類似VScode或sublime需要裝各種插件來寫代碼,而angular就像webstorm一樣集成了很多功能
React 是什么
? 用腳本進行DOM操作的代價很昂貴。把DOM和JavaScript各自想象為一個島嶼,它們之間用收費橋梁連接,js每次訪問DOM,都要途徑這座橋,并交納“過橋費”,訪問DOM的次數(shù)越多,費用也就越高。
? 因此,推薦的做法是盡量減少過橋的次數(shù),努力待在ECMAScript島上。因為這個原因react的虛擬dom就顯得難能可貴了,它創(chuàng)造了虛擬dom并且將它們儲存起來,每當(dāng)狀態(tài)發(fā)生變化的時候就會創(chuàng)造新的虛擬節(jié)點和以前的進行對比,讓變化的部分進行渲染。
整個過程沒有對dom進行獲取和操作,只有一個渲染的過程,所以react說是一個ui框架。
組件的生命周期
組件在初始化時會觸發(fā)5個鉤子函數(shù):
1、getDefaultProps()
? 設(shè)置默認的props,也可以用defaultProps設(shè)置組件的默認屬性。
這個函數(shù)只在 React.createClass 方法創(chuàng)造的組件類才會用到 。
2、getInitialState()
? 在使用es6的class語法時是沒有這個鉤子函數(shù)的,可以直接在constructor中定義this.state。此時可以訪問this.props。
? 這個函數(shù)只在 React.createClass 方法創(chuàng)造的組件類才會用到 。
3、componentWillMount()
? 組件初始化時只調(diào)用,以后組件更新不調(diào)用,整個生命周期只調(diào)用一次,此時可以修改state。
4、 render()
? react最重要的步驟,創(chuàng)建虛擬dom,進行diff算法,更新dom樹都在此進行。此時就不能更改state了。
? 通常一個組件要發(fā)揮作用,總是要渲染一些東西, render 函數(shù)并不做實際的誼染動作,它只是返回一個 JSX 描述的結(jié)構(gòu),最終由 React 來操作渲染過程。 ? 當(dāng)然,某些特殊組件的作用不是渲染界面,或者,組件在某些情況下選擇沒有東西可畫,那就讓 render 函數(shù)返回一個 null 或者 false ,等于告訴 React,這個組件這次不需要渲染任何 DOM 元素 。 ? 需要注意, render 函數(shù)應(yīng)該是一個純函數(shù),完全根據(jù) this.state 和 this.props 來決定返回的結(jié)果,而且不要產(chǎn)生任何副作用。在 render 函數(shù)中去調(diào)用 this.setState 毫無疑問是錯誤的,因為一個純函數(shù)不應(yīng)該引起狀態(tài)的改變。
5、componentDidMount()
? Render 函數(shù)返回的東西已 經(jīng)引發(fā)了渲染,組件已經(jīng)被“裝載”到了 DOM 樹上 。 組件渲染之后調(diào)用,可以通過this.getDOMNode()獲取和操作dom節(jié)點,只調(diào)用一次。
在更新時也會觸發(fā)5個鉤子函數(shù):
6、componentWillReceivePorps(nextProps)
組件初始化時不調(diào)用,組件接受新的props時調(diào)用。
7、shouldComponentUpdate(nextProps, nextState)
? React性能優(yōu)化非常重要的一環(huán)。組件接受新的state或者props時調(diào)用,我們可以設(shè)置在此對比前后兩個props和state是否相同,如果相同則返回false阻止更新,因為相同的屬性狀態(tài)一定會生成相同的dom樹,這樣就不需要創(chuàng)造新的dom樹和舊的dom樹進行diff算法對比,節(jié)省大量性能,尤其是在dom結(jié)構(gòu)復(fù)雜的時候。不過調(diào)用this.forceUpdate會跳過此步驟。
8、componentWillUpdate(nextProps, nextState)
組件初始化時不調(diào)用,只有在組件將要更新時才調(diào)用,此時可以修改state
9、render()
當(dāng)組件的state或者props發(fā)生改變的時候,render函數(shù)就會重新執(zhí)行
10、componentDidUpdate()
組件初始化時不調(diào)用,組件更新完成后調(diào)用,此時可以獲取dom節(jié)點。
還有一個卸載鉤子函數(shù)
11、componentWillUnmount()
組件將要卸載時調(diào)用,一些事件監(jiān)聽和定時器需要在此時清除。
? 以上可以看出來react總共有10個周期函數(shù)(render重復(fù)一次),這個10個函數(shù)可以滿足我們所有對組件操作的需求,利用的好可以提高開發(fā)效率和組件性能。
? render 和 shouldComponentUpdate函數(shù),也是 React 生命周期函數(shù)中唯二兩個要求有返回結(jié)果的函數(shù)。 render 函數(shù)的返回結(jié)果將用于構(gòu)造 DOM 對象,而 shouldComponentUpdate函數(shù)返回一個布爾值,告訴 React 庫這個組件在這次更新過程中是否要繼續(xù) 。
V16 生命周期函數(shù)用法建議
class ExampleComponent extends React.Component {// 用于初始化 stateconstructor() {}// 用于替換 `componentWillReceiveProps` ,該函數(shù)會在初始化和 `update` 時被調(diào)用// 因為該函數(shù)是靜態(tài)函數(shù),所以取不到 `this`// 如果需要對比 `prevProps` 需要單獨在 `state` 中維護static getDerivedStateFromProps(nextProps, prevState) {}// 判斷是否需要更新組件,多用于組件性能優(yōu)化shouldComponentUpdate(nextProps, nextState) {}// 組件掛載后調(diào)用// 可以在該函數(shù)中進行請求或者訂閱componentDidMount() {}// 用于獲得最新的 DOM 數(shù)據(jù)getSnapshotBeforeUpdate() {}// 組件即將銷毀// 可以在此處移除訂閱,定時器等等componentWillUnmount() {}// 組件銷毀后調(diào)用componentDidUnMount() {}// 組件更新后調(diào)用componentDidUpdate() {}// 渲染組件函數(shù)render() {}// 以下函數(shù)不建議使用UNSAFE_componentWillMount() {}UNSAFE_componentWillUpdate(nextProps, nextState) {}UNSAFE_componentWillReceiveProps(nextProps) {} } 復(fù)制代碼父子組件的渲染過程
? 因為 render 函數(shù)本身并不往 DOM 樹上渲染或者裝載內(nèi)容,它只是返回一個 JSX 表示的對象,然后由 React 庫來根據(jù)返回對象決定如何渲染 。而 React 庫肯定是要把所有組件返回的結(jié)果綜合起來,才能知道該如何產(chǎn)生對應(yīng)的 DOM修改 。 所以,只有 React 庫調(diào)用三個 Counter 組件的 render 函數(shù)之后,才有可能完成裝載,這時候才會依次調(diào)用各個組件的 componentDidMount 函數(shù)作為裝載過程的收尾 。
React的組件化
? react的一個組件很明顯的由dom視圖和state數(shù)據(jù)組成,兩個部分涇渭分明。
? state是數(shù)據(jù)中心,它的狀態(tài)決定著視圖的狀態(tài)。這時候發(fā)現(xiàn)似乎和我們一直推崇的MVC開發(fā)模式有點區(qū)別,沒了Controller控制器,那用戶交互怎么處理,數(shù)據(jù)變化誰來管理?
? 然而這并不是react所要關(guān)心的事情,它只負責(zé)ui的渲染。與其他框架監(jiān)聽數(shù)據(jù)動態(tài)改變dom不同,react采用setState來控制視圖的更新。
? setState會自動調(diào)用render函數(shù),觸發(fā)視圖的重新渲染,如果僅僅只是state數(shù)據(jù)的變化而沒有調(diào)用setState,并不會觸發(fā)更新。
? 組件就是擁有獨立功能的視圖模塊,許多小的組件組成一個大的組件,整個頁面就是由一個個組件組合而成。它的好處是利于重復(fù)利用和維護。
UI = render(data)
? React 組件扮 演的是 render 函數(shù)的角色,應(yīng)該是一個沒有副作用的純函數(shù)。修改 props 的值, 是一個副作用,組件應(yīng)該避免。
組件類別
概念: 所謂組件,簡單說,指的是能完成某個特定功能的獨立的 、 可重用的代碼 。
- 容器組件 只關(guān)心邏輯,不負責(zé)頁面渲染
- UI組件 不關(guān)心邏輯,只負責(zé)頁面渲染
- 無狀態(tài)組件 沒有render()函數(shù),只是一個函數(shù),沒有聲明周期函數(shù),效率更高
React的 Diff算法
? 當(dāng)組件更新的時候,react會創(chuàng)建一個新的虛擬dom樹并且會和之前儲存的dom樹進行比較,這個比較多過程就用到了diff算法,所以組件初始化的時候是用不到的。
? react提出了一種假設(shè),相同的節(jié)點具有類似的結(jié)構(gòu),而不同的節(jié)點具有不同的結(jié)構(gòu)。在這種假設(shè)之上進行逐層的比較,如果發(fā)現(xiàn)對應(yīng)的節(jié)點是不同的,那就直接刪除舊的節(jié)點以及它所包含的所有子節(jié)點然后替換成新的節(jié)點。如果是相同的節(jié)點,則只進行屬性的更改。
? 對于列表的diff算法稍有不同,因為列表通常具有相同的結(jié)構(gòu),在對列表節(jié)點進行刪除,插入,排序的時候,單個節(jié)點的整體操作遠比一個個對比一個個替換要好得多,所以在創(chuàng)建列表的時候需要設(shè)置key值,這樣react才能分清誰是誰。當(dāng)然不寫key值也可以,但這樣通常會報出警告,通知我們加上key值以提高react的性能。
演變過程: JSX > createElement > 虛擬dom (JS對象) > 真實dom
虛擬Dom的對比算法
不同類型的元素
? 每當(dāng)根元素有不同類型,React將卸載舊樹并重新構(gòu)建新樹。從<a>到<img>或從<Article>到<Comment>,或從<Button> 到 <div>,任何的調(diào)整都會導(dǎo)致全部重建。
? 當(dāng)樹被卸載,舊的DOM節(jié)點將被銷毀。組件實例會調(diào)用componentWillUnmount()。當(dāng)構(gòu)建一棵新樹,新的DOM節(jié)點被插入到DOM中。組件實例將依次調(diào)用componentWillMount()和componentDidMount()。任何與舊樹有關(guān)的狀態(tài)都將丟棄。
? 這個根節(jié)點下所有的組件都將會被卸載,同時他們的狀態(tài)將被銷毀。
相同類型的DOM元素
? 當(dāng)比較兩個相同類型的React DOM元素時,React則會觀察二者的屬性,保持相同的底層DOM節(jié)點,并僅更新變化的屬性。
相同類型的組件元素
? 當(dāng)組件更新時,實例仍保持一致,以讓狀態(tài)能夠在渲染之間保留。React通過更新底層組件實例的props來產(chǎn)生新元素,并在底層實例上依次調(diào)用componentWillReceiveProps() 和 componentWillUpdate() 方法。
? 接下來,render()方法被調(diào)用,同時對比算法會遞歸處理之前的結(jié)果和新的結(jié)果。
React diff算法流程圖
key的作用
? React DOM 首先會比較元素內(nèi)容先后的不同,而在渲染過程中只會更新改變了的部分。
? key的重要性: 提高對比的效率
? Keys可以在DOM中的某些元素被增加或刪除的時候幫助React識別哪些元素發(fā)生了變化。因此你應(yīng)當(dāng)給數(shù)組中的每一個元素賦予一個確定的標(biāo)識。
? 用數(shù)組下標(biāo)作為 key,看起來 key 值是唯一的,但是卻不是穩(wěn)定不變的,隨著 todos數(shù)組值的不同,同樣一個 Todoltem 實例在不同的更新過程中在數(shù)組中的下標(biāo)完全可能不同,把下標(biāo)當(dāng)做 key 就讓 React 徹底亂套了 。 ? 需要注意,雖然 key 是一個 prop ,但是接受 key 的組件并不能讀取到 key 的值,因為 key 和 ref 是 React 保留的兩個特殊 prop ,并沒有預(yù)期讓組件直接訪問 。
為什么使用setState修改數(shù)據(jù)?
? 直接修改this.state的值,雖然事實上改變了組件的內(nèi)部狀態(tài),但只是野蠻地修改了state ,卻沒有驅(qū)動組件進行重新渲染,既然組件沒有重新渲染,當(dāng)然不會反應(yīng) this.state值的變化;
? 而 this.setState()函數(shù)所做的事情,首先是改變 this.state 的值,然后驅(qū)動組件經(jīng)歷更新過程,這樣才有機會讓 this.state 里新的值出現(xiàn)在界面上 。
setState 是異步函數(shù)?
? setState() 排隊更改組件的 state ,并通過更新 state來告訴 React,該組件及其子組件需要重新渲染。這是用于 響應(yīng)事件處理程序 和 服務(wù)器響應(yīng) 更新用戶界面的主要方法。
? 記住 setState() 作為一個請求,而不是立即命令來更新組件。為了更好的感知性能,React 可能會延遲它,然后合并多個setState()更新多個組件。React不保證state 更新就立即應(yīng)用(重新渲染)。
? React 可以將多個setState() 調(diào)用合并成一個調(diào)用來提高性能。
? 因為 this.props 和 this.state 可能是異步更新的,你不應(yīng)該依靠它們的值來計算下一個狀態(tài)。setState() 并不總是立即更新組件。它可能會 批量 或 延遲到后面更新。這使得在調(diào)用 setState() 之后立即讀取 this.state 存在一個潛在的陷阱。 而使用 componentDidUpdate 或 setState 回調(diào)(setState(updater, callback)),在應(yīng)用更新后,都將被保證觸發(fā)。
舉個例子:
? 例如,此代碼可能無法更新計數(shù)器:
// Wrong this.setState({counter: this.state.counter + this.props.increment, }); 復(fù)制代碼? 要修復(fù)它,請使用第二種形式的 setState() 來接受一個函數(shù)而不是一個對象。 該函數(shù)將接收先前的狀態(tài)作為第一個參數(shù),將此次更新被應(yīng)用時的props做為第二個參數(shù):
// Correct this.setState((prevState, props) => ({counter: prevState.counter + props.increment })); 復(fù)制代碼? setState()總是會導(dǎo)致重新渲染,除非 shouldComponentUpdate()返回 false 。如果可變對象被使用,并且條件渲染邏輯不能在shouldComponentUpdate() 中實現(xiàn),只有當(dāng)新state與先前 state 不同時調(diào)用 setState()才能避免不必要的重新渲染。
不能在render()里面寫this.setState()會導(dǎo)致循環(huán)修改
React組件寫法
ES6的class類可以看作是構(gòu)造函數(shù)的一個語法糖,可以把它當(dāng)成構(gòu)造函數(shù)來看,extends實現(xiàn)了類之間的繼承 —— 定義一個類Main 繼承React.Component所有的屬性和方法,組件的生命周期函數(shù)就是從這來的。
constructor是構(gòu)造器,在實例化對象時調(diào)用,super調(diào)用了父類的constructor創(chuàng)造了父類的實例對象this,然后用子類的構(gòu)造函數(shù)進行修改。
super(props)
? 如果在構(gòu)造函數(shù)中沒有調(diào)用super(props),那么組件實例被構(gòu)造之后,類實例的所有成員函數(shù)就無法通過 this.props 訪問到父組件傳遞過來的 props 值。很明顯,給 this.props 賦值是 React.Component 構(gòu)造函數(shù)的工作之一 。
shouldCompnentUpdate生命周期
? 在通用的 shouldCompnentUpdate 函數(shù)中做“淺層比較”,是一個被普遍接受的做法;如果需要做“深層比較”,那就是某個特定組件的行為,需要開發(fā)者自己根據(jù)組件情況去編寫 。
PureComponent
? React15.3 中新加了一個類PureComponent,前身是 PureRenderMixin ,和 Component 基本一樣,只不過會在 render之前幫組件自動執(zhí)行一次shallowEqual(淺比較),來決定是否更新組件,淺比較類似于淺復(fù)制,只會比較第一層。使用 PureComponent 相當(dāng)于省去了寫 shouldComponentUpdate 函數(shù),當(dāng)組件更新時,如果組件的 props 和 state:
so. 為了性能,React只做了淺對比,于是就有了immutable.js
immutable.js
高階組件
? 高階組件就是一個函數(shù),且該函數(shù)接受一個組件作為參數(shù),并返回一個新的組件
const EnhancedComponent = higherOrderComponent(WrappedComponent); 復(fù)制代碼? 對比組件將props屬性轉(zhuǎn)變成UI,高階組件則是將一個組件轉(zhuǎn)換成另一個新組件。
? 高階組件在React第三方庫中很常見,比如Redux的connect方法和Relay的createContainer.
Refs屬性
創(chuàng)建 Refs
? 使用 React.createRef() 創(chuàng)建 refs,通過 ref 屬性來獲得 React 元素。當(dāng)構(gòu)造組件時,refs 通常被賦值給實例的一個屬性,這樣你可以在組件中任意一處使用它們.
class MyComponent extends React.Component {constructor(props) {super(props);this.myRef = React.createRef();}render() {return <div ref={this.myRef} />;} } 復(fù)制代碼ref的值取決于節(jié)點的類型:
- 當(dāng) ref 屬性被用于一個普通的 HTML 元素時,React.createRef() 將接收底層 DOM 元素作為它的 current 屬性以創(chuàng)建 ref 。
- 當(dāng) ref 屬性被用于一個自定義類組件時,ref 對象將接收該組件已掛載的實例作為它的 current 。
- 你不能在函數(shù)式組件上使用 ref 屬性,因為它們沒有實例。
React-Router路由
? Router就是React的一個組件,它并不會被渲染,只是一個創(chuàng)建內(nèi)部路由規(guī)則的配置對象,根據(jù)匹配的路由地址展現(xiàn)相應(yīng)的組件。
? Route則對路由地址和組件進行綁定,Route具有嵌套功能,表示路由地址的包涵關(guān)系,這和組件之間的嵌套并沒有直接聯(lián)系。Route可以向綁定的組件傳遞7個屬性:children,history,location,params,route,routeParams,routes,每個屬性都包涵路由的相關(guān)的信息。
? 比較常用的有children(以路由的包涵關(guān)系為區(qū)分的組件),location(包括地址,參數(shù),地址切換方式,key值,hash值)。
? react-router提供Link標(biāo)簽,這只是對a標(biāo)簽的封裝,值得注意的是,點擊鏈接進行的跳轉(zhuǎn)并不是默認的方式,react-router阻止了a標(biāo)簽的默認行為并用pushState進行hash值的轉(zhuǎn)變。
? 切換頁面的過程是在點擊Link標(biāo)簽或者后退前進按鈕時,會先發(fā)生url地址的轉(zhuǎn)變,Router監(jiān)聽到地址的改變根據(jù)Route的path屬性匹配到對應(yīng)的組件,將state值改成對應(yīng)的組件并調(diào)用setState觸發(fā)render函數(shù)重新渲染dom。
路由(按需加載)
? 當(dāng)頁面比較多時,項目就會變得越來越大,尤其對于單頁面應(yīng)用來說,初次渲染的速度就會很慢,這時候就需要按需加載,只有切換到頁面的時候才去加載對應(yīng)的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> ); 復(fù)制代碼組件之間的通信
? react推崇的是單向數(shù)據(jù)流,通常被稱為自頂向下或單向數(shù)據(jù)流。 任何狀態(tài)始終由某些特定組件所有,并且從該狀態(tài)導(dǎo)出的任何數(shù)據(jù)或 UI 只能影響樹中下方的組件。
解決通信問題的方法很多:
React的事件委托
? 我們在 JSX 中看到一個組件使用了 onClick,但并沒有產(chǎn)生直接使用 onclick (注意是 onclick 不是 onClick)的HTML ,而是使用了事件委托(event delegation)的方式處理點擊事件,無論有多少個 onClick 出現(xiàn),其實最后都只在 DOM 樹上添加了一個事件處理函數(shù),掛在最頂層的 DOM 節(jié)點上。
? 所有的點擊事件都被這個事件處理函數(shù)捕獲,然后根據(jù)具體組件分配給特定函數(shù),使用事件委托的性能當(dāng)然要比為每個 onClick 都掛載一個事件處理函數(shù)要高 。 ? 因為 React 控制了組件的生命周期,在 unmount 的時候自然能夠清除相關(guān)的所有事 件處理函數(shù),內(nèi)存泄露也不再是一個問題。
② 進階
Redux
基本原則
Flux 的基本原則是“單向數(shù)據(jù)流”, Redux 在此基礎(chǔ)上強調(diào)三個基本原則:
-
唯一數(shù)據(jù)源( Single Source of Truth);
? 在 Flux 中,應(yīng)用可以擁有多個 Store ,往往根據(jù)功能把應(yīng)用的狀態(tài) 數(shù)據(jù)劃分給若干個 Store 分別存儲管理 。
? Redux 對這個問題的解決方法就是,整個應(yīng)用只保持一個 Store ,所有組件的數(shù)據(jù)源 就是這個 Store 上的狀態(tài) 。
-
保持狀態(tài)只讀( State is read-only);
? 保持狀態(tài)只讀,就是說不能去直接修改狀態(tài),要修改 Store 的狀態(tài),必須要通過派發(fā) 一個 action 對象完成,這一點 ,和 Flux 的要求并沒有什么區(qū)別 。
? 當(dāng)然,要驅(qū)動用戶界面渲染,就要改變應(yīng)用的狀態(tài),但是改變狀態(tài)的方法不是去修 改狀態(tài)上值,而是創(chuàng)建一個新的狀態(tài)對象返回給 Redux ,由 Redux 完成新的狀態(tài)的組裝 。
-
數(shù)據(jù)改變只能通過純函數(shù)完成( Changes are made with pure functions ) 。
? 在 Redux 中, 每個 reducer 的函數(shù)簽名如下所示 : ? reducer(state , action ) ? 第一個參數(shù) state 是當(dāng)前的狀態(tài),第二個參數(shù) action 是接收到的 action 對象,而 reducer函數(shù)要做的事情,就是根據(jù) state 和 action 的值產(chǎn)生一個新的對象返回,注意 reducer 必須是純函數(shù),也就是說函數(shù)的返回結(jié)果必須完全由參數(shù) state 和 action 決定,而且不產(chǎn)生任何副作用,也不能修改參數(shù) state 和 action 對象。
Redux核心API
Redux主要由三部分組成:store,reducer,action。
store
? Redux的核心是store,它由Redux提供的 createStore(reducer, defaultState)這個方法生成,生成三個方法,getState(),dispatch(),subscrible()。
- getState():存儲的數(shù)據(jù),狀態(tài)樹;
- dispatch(action):分發(fā)action,并返回一個action,這是唯一能改變store中數(shù)據(jù)的方式;
- subscrible(listener):注冊一個監(jiān)聽者,store發(fā)生變化的時候被調(diào)用。
reducer
reducer是一個純函數(shù),它根據(jù)previousState和action計算出新的state。 reducer(previousState,action)
action
action本質(zhì)上是一個JavaScript對象,其中必須包含一個type字段來表示將要執(zhí)行的動作,其他的字段都可以根據(jù)需求來自定義。
const ADD_TODO = 'ADD_TODO' 復(fù)制代碼{type: ADD_TODO,text: 'Build my first Redux app' } 復(fù)制代碼整合
他們?nèi)咧g的交互,可以由下圖概括:
概念分析:
redux主要由三部分組成:store,reducer,action。
store是一個對象,它有四個主要的方法:
1、dispatch:
? 用于action的分發(fā)——在createStore中可以用middleware中間件對dispatch進行改造,比如當(dāng)action傳入dispatch會立即觸發(fā)reducer,有些時候我們不希望它立即觸發(fā),而是等待異步操作完成之后再觸發(fā),這時候用redux-thunk對dispatch進行改造,以前只能傳入一個對象,改造完成后可以傳入一個函數(shù),在這個函數(shù)里我們手動dispatch一個action對象,這個過程是可控的,就實現(xiàn)了異步。
2、subscribe:
? 監(jiān)聽state的變化——這個函數(shù)在store調(diào)用dispatch時會注冊一個listener監(jiān)聽state變化,當(dāng)我們需要知道state是否變化時可以調(diào)用,它返回一個函數(shù),調(diào)用這個返回的函數(shù)可以注銷監(jiān)聽。
let unsubscribe = store.subscribe(() => {console.log('state發(fā)生了變化')})
3、getState:
? 獲取store中的state——當(dāng)我們用action觸發(fā)reducer改變了state時,需要再拿到新的state里的數(shù)據(jù),畢竟數(shù)據(jù)才是我們想要的。
? getState主要在兩個地方需要用到,一是在dispatch拿到action后store需要用它來獲取state里的數(shù)據(jù),并把這個數(shù)據(jù)傳給reducer,這個過程是自動執(zhí)行的,二是在我們利用subscribe監(jiān)聽到state發(fā)生變化后調(diào)用它來獲取新的state數(shù)據(jù),如果做到這一步,說明我們已經(jīng)成功了。
4、replaceReducer:
替換reducer,改變state修改的邏輯。
? store可以通過createStore()方法創(chuàng)建,接受三個參數(shù),經(jīng)過combineReducers合并的reducer和state的初始狀態(tài)以及改變dispatch的中間件,后兩個參數(shù)并不是必須的。store的主要作用是將action和reducer聯(lián)系起來并改變state。
action:
? action是一個對象,其中type屬性是必須的,同時可以傳入一些數(shù)據(jù)。action可以用actionCreactor進行創(chuàng)造。dispatch就是把action對象發(fā)送出去。
reducer:
? reducer是一個函數(shù),它接受一個state和一個action,根據(jù)action的type返回一個新的state。根據(jù)業(yè)務(wù)邏輯可以分為很多個reducer,然后通過combineReducers將它們合并,state樹中有很多對象,每個state對象對應(yīng)一個reducer,state對象的名字可以在合并時定義。
const reducer = combineReducers({a: doSomethingWithA,b: processB,c: c }) 復(fù)制代碼combineReducers:
? 其實它也是一個reducer,它接受整個state和一個action,然后將整個state拆分發(fā)送給對應(yīng)的reducer進行處理,所有的reducer會收到相同的action,不過它們會根據(jù)action的type進行判斷,有這個type就進行處理然后返回新的state,沒有就返回默認值,然后這些分散的state又會整合在一起返回一個新的state樹。
流程分析:
redux的state和react的state兩者完全沒有關(guān)系,除了名字一樣。
React-Redux
React-redux是怎么配合的
react-redux 的兩個最主要功能:
- connect :連接容器組件和視圖組件;
- Provider :提供包含 store 的 context。
Redux 本身和React沒有關(guān)系,只是數(shù)據(jù)處理中心,是React-Redux讓他們聯(lián)系在一起。
React-Redux的兩個方法
connect
掘金資料
connect連接React組件和Redux store。connect實際上是一個高階函數(shù),返回一個新的已與 Redux store 連接的組件類。
const VisibleTodoList = connect(mapStateToProps,mapDispatchToProps )(TodoList) 復(fù)制代碼TodoList是 UI 組件,VisibleTodoList就是由 react-redux 通過connect方法自動生成的容器組件。
**書籍資料 **
export default connect(mapStateToProps, mapDispatchToProps) ( Counter); 復(fù)制代碼這個 connect 函數(shù)具體做了什么工作呢? 作為容器組件,要做的工作無外乎兩件事:
- 把 Store 上的狀態(tài)轉(zhuǎn)化為內(nèi)層傻瓜組件的 prop;
- 把內(nèi)層傻瓜組件中的用戶動作轉(zhuǎn)化為派送給 Store 的動作 。
Provider
Provider實現(xiàn)store的全局訪問,將store傳給每個組件。
原理:使用React的context,context可以實現(xiàn)跨組件之間的傳遞。
如果只使用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)來觸發(fā)reducer改變state,并用subscribe監(jiān)聽state的變化,然后用getState獲取變化后的值。但是并不推薦這樣做,它會讓數(shù)據(jù)流變的混亂,過度的耦合也會影響組件的復(fù)用,維護起來也更麻煩。
connect --connect(mapStateToProps, mapDispatchToProps, mergeProps, options) 是一個函數(shù),它接受四個參數(shù)并且再返回一個函數(shù)--wrapWithConnect,wrapWithConnect接受一個組件作為參數(shù)wrapWithConnect(component),它內(nèi)部定義一個新組件Connect(容器組件)并將傳入的組件(ui組件)作為Connect的子組件然后return出去。
所以它的完整寫法是這樣的:`connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(component)
mapStateToProps(state, [ownProps]):
mapStateToProps 接受兩個參數(shù),store的state和自定義的props,并返回一個新的對象,這個對象會作為props的一部分傳入ui組件。我們可以根據(jù)組件所需要的數(shù)據(jù)自定義返回一個對象。ownProps的變化也會觸發(fā)mapStateToProps
function mapStateToProps(state) {return { todos: state.todos }; } 復(fù)制代碼mapDispatchToProps(dispatch, [ownProps]):
mapDispatchToProps如果是對象,那么會和store綁定作為props的一部分傳入ui組件。
如果是個函數(shù),它接受兩個參數(shù),bindActionCreators會將action和dispatch綁定并返回一個對象,這個對象會和ownProps一起作為props的一部分傳入ui組件。
所以不論mapDispatchToProps是對象還是函數(shù),它最終都會返回一個對象,如果是函數(shù),這個對象的key值是可以自定義的
function mapDispatchToProps(dispatch) {return {todoActions: bindActionCreators(todoActionCreators, dispatch),counterActions: bindActionCreators(counterActionCreators, dispatch)}; } 復(fù)制代碼mapDispatchToProps返回的對象其屬性其實就是一個個actionCreator,因為已經(jīng)和dispatch綁定,所以當(dāng)調(diào)用actionCreator時會立即發(fā)送action,而不用手動dispatch。ownProps的變化也會觸發(fā)mapDispatchToProps。
mergeProps(stateProps, dispatchProps, ownProps):
將mapStateToProps() 與 mapDispatchToProps()返回的對象和組件自身的props合并成新的props并傳入組件。默認返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的結(jié)果。
options:
pure = true 表示Connect容器組件將在shouldComponentUpdate中對store的state和ownProps進行淺對比,判斷是否發(fā)生變化,優(yōu)化性能。為false則不對比。
其實connect函數(shù)并沒有做什么,大部分的邏輯都是在它返回的wrapWithConnect函數(shù)內(nèi)實現(xiàn)的,確切的說是在wrapWithConnect內(nèi)定義的Connect組件里實現(xiàn)的。
在項目中我使用的大store目錄結(jié)構(gòu)是:
// 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; 復(fù)制代碼// reducers.js // 合并小的reducer import {combineReducers} from 'redux-immutable'; // 提供的是immutable數(shù)據(jù) 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; 復(fù)制代碼在項目中我使用的小store(舉例)目錄結(jié)構(gòu)是:
核心代碼:
// _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} } 復(fù)制代碼// 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))}})} } 復(fù)制代碼// 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' 復(fù)制代碼// index.js import reducer from './_reducer' import * as actionCreators from './actionCreators' import * as constants from './constants'export {reducer, actionCreators, constants} 復(fù)制代碼完整的 react --> redux --> react 流程
一、Provider組件接受redux的store作為props,然后通過context往下傳。
二、
connect函數(shù)在初始化的時候會將mapDispatchToProps對象綁定到store,
如果mapDispatchToProps是函數(shù)則在Connect組件獲得store后,根據(jù)傳入的store.dispatch和action通過bindActionCreators進行綁定,再將返回的對象綁定到store,connect函數(shù)會返回一個wrapWithConnect函數(shù),同時wrapWithConnect會被調(diào)用且傳入一個ui組件,wrapWithConnect內(nèi)部使用class Connect extends Component定義了一個Connect組件,傳入的ui組件就是Connect的子組件,
然后Connect組件會通過context獲得store,并通過store.getState獲得完整的state對象,將state傳入mapStateToProps返回stateProps對象、mapDispatchToProps對象或mapDispatchToProps函數(shù)會返回一個dispatchProps對象,stateProps、dispatchProps以及Connect組件的props三者通過Object.assign(),或者mergeProps合并為props傳入ui組件。然后在ComponentDidMount中調(diào)用store.subscribe,注冊了一個回調(diào)函數(shù)handleChange監(jiān)聽state的變化。
三、
上面的有點復(fù)雜,簡化版的流程是:
一、Provider組件接受redux的store作為props,然后通過context往下傳。
二、connect函數(shù)收到Provider傳出的store,然后接受三個參數(shù)mapStateToProps,mapDispatchToProps和組件,并將state和actionCreator以props傳入組件,這時組件就可以調(diào)用actionCreator函數(shù)來觸發(fā)reducer函數(shù)返回新的state,connect監(jiān)聽到state變化調(diào)用setState更新組件并將新的state傳入組件。
connect可以寫的非常簡潔,mapStateToProps,mapDispatchToProps只不過是傳入的回調(diào)函數(shù),connect函數(shù)在必要的時候會調(diào)用它們,名字不是固定的,甚至可以不寫名字。
簡化版本:
connect(state => state, action)(Component); 復(fù)制代碼redux以及react-redux到底是怎么實現(xiàn)的?
總結(jié)
下圖闡述了它們?nèi)咧g的工作流程:
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; 復(fù)制代碼? 我們看 redux-thunk 這一串函數(shù)中最里層的函數(shù),也就是實際處理每個 action 對象的函數(shù)。 首先檢查參數(shù) action 的類型,如果是函數(shù)類型的話,就執(zhí)行這個 action 函數(shù),把dispatch 和 getState 作為參數(shù)傳遞進去,否則就調(diào)用 next 讓下一個中間件繼續(xù)處理 action,這個處理過程和 redux-thunk 文檔中描述的功能一致。
? Redux的單向數(shù)據(jù)流是同步操作,驅(qū)動 Redux 流程的 是 action 對象, 每一個 action對象被派發(fā)到 Store 上之后,同步地被分配給所有的 reducer 函數(shù),每個 reducer 都是純函數(shù),純函數(shù)不產(chǎn)生任何副作用,自然是完成數(shù)據(jù)操作之后立刻同步返回, reducer 返回的結(jié)果又被同步地拿去更新 Store 上的狀態(tài)數(shù)據(jù),更新狀態(tài)數(shù)據(jù)的操作會立刻被同步給監(jiān)聽Store 狀態(tài)改變的函數(shù),從而引發(fā)作為視圖的 React 組件更新過程。
? 當(dāng)我們想要讓 Redux 幫忙處理一個異步操作的時候,代碼一樣也要派發(fā)一個 action對象,畢竟 Redux 單向數(shù)據(jù)流就是由 action 對象驅(qū)動的 。 但是這個引發(fā)異步操作的action 對象比較特殊,我們叫它們“異步 action 對象” 。 ? 前面例子中的 action 構(gòu)造函數(shù)返回的都是一個普通的對象,這個對象包含若干字段,其中必不可少的字段是 type ,但是“異步 action 對象”不是一個普通 JavaScript 對象,而是一個函數(shù) 。 ? 如果沒有 redux-thunk 中間件的存在 這樣一個函數(shù)類型的 action 對象被派發(fā)出來會一路發(fā)送到各個 reducer 函數(shù), reducer 函數(shù)從這些實際上是函數(shù)的 action 對象上是無法獲得 type 字段的,所以也做不了什么實質(zhì)的處理。
? 不過,有了redux-thunk中間件之后,這些 action 對象根本沒有機會觸及到 reducer函數(shù),在中間件一層就被 redux-thunk 截獲 。
? redux-thunk 的工作是檢查 action 對象是不是函數(shù),如果不是函數(shù)就放行,完成普通action 對象的生命周期,而如果發(fā)現(xiàn) action 對象是函數(shù),那就執(zhí)行這個函數(shù),并把 Store的 dispatch 函數(shù)和 getState 函數(shù)作為參數(shù)傳遞到函數(shù)中去,處理過程到此為止,不會讓這個異步 action 對象繼續(xù)往前派發(fā)到 reducer 函數(shù) 。
React中間件機制
? 在 Redux框架中,中間件處理的是 action 對象,而派發(fā) action 對象的就是 Store 上的dispatch 函數(shù),之前介紹過通過 dispatch 派發(fā)的 action 對象會進入 reducer 。 在 action 對象進入 reducer 之前,會經(jīng)歷中間件的管道 。
? 在這個中間件管道中,每個中間件都會接收到 action 對象,在處理完畢之后,就會把 action 對象交給下一個中間件來處理,只有所有的中間件都處理完 action 對象之后,在這個中間件管道中,每個中間件都會接收到 action 對象,在處理完畢之后,就會把 action 對象交給下一個中間件來處理,只有所有的中間件都處理完 action 對象之后,才輪到 reducer 來處理 action 對象,然而,如果某個中間件覺得沒有必要繼續(xù)處理這個action 對象了,就不會把 action 對象交給下一個中間件,對這個 action 對象的處理就此中止,也就輪不到 reducer 上場了 。
? 每個中間件必須要定義成一個函數(shù),返回一個接受 next 參數(shù)的函數(shù),而這個接受next 參數(shù)的函數(shù)又返回一個接受 action 參數(shù)的函數(shù) 。 next 參數(shù)本身也是一個函數(shù),中間件調(diào)用這個 next 函數(shù)通知 Redux 自己的處理工作已經(jīng)結(jié)束 。
代碼舉例:
// 一個實際上什么事都不做的中間件代碼如下: function doNothingMiddleware{{dispatch, getState)) {return function {next) {return function {action) {return next{action)}} } 復(fù)制代碼? 以 action 為參數(shù)的函數(shù)對傳人的 action 對象進行處理,因為 JavaScript 支持閉包 ( Clousure ),在這個函數(shù)里可以訪問上面兩層函數(shù)的參數(shù),所以可以根據(jù)需要做很多事 情,包括以下功能:
- 調(diào)用 dispatch 派發(fā)出一個新 action 對象;
- 調(diào)用 getState 獲得當(dāng)前 Redux Store 上的狀態(tài);
- 調(diào)用 next 告訴 Redux 當(dāng)前中間件工作完畢,讓 Redux 調(diào)用下一個中間件;
- 訪問 action 對象 action 上的所有數(shù)據(jù)。 具有上面這些功能,一個中間件足夠獲取 Store 上的所有信息,也具有足夠能力控制數(shù)據(jù)的流轉(zhuǎn) 。
中間件用于擴展 dispatch 函數(shù)的功能,多個中間件實際構(gòu)成了一個處理 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'; 復(fù)制代碼3、根據(jù)需求創(chuàng)建頂層ui組件,每個頂層ui組件對應(yīng)一個頁面。
4、創(chuàng)建actionCreators和reducers,并用combineReducers將所有的reducer合并成一個大的reduer。利用createStore創(chuàng)建store并引入combineReducers和applyMiddleware。
5、利用connect將actionCreator,reuder和頂層的ui組件進行關(guān)聯(lián)并返回一個新的組件。
6、利用connect返回的新的組件配合react-router進行路由的部署,返回一個路由組件Router。
7、將Router放入最頂層組件Provider,引入store作為Provider的屬性。
8、調(diào)用render渲染Provider組件且放入頁面的標(biāo)簽中。
可以看到頂層的ui組件其實被套了四層組件,Provider,Router,Route,Connect,這四個組件并不會在視圖上改變react,它們只是功能性的。
Github地址: wq93
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的学习React的一知半解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 东风小康为什么是dfsk_助力地摊经济瑞
- 下一篇: (完整版)c语言初学必背代码