目前,在前端Web開發中,三大熱門框架為React.js,Vue.js,Angular.js 。當然,三大框架各有各的優缺點,這里就不多說了,下面我就針對前段時間所學的React框架做一下整體知識點的概述。
React是什么?
我們在用js腳本操作DOM元素時如果存在大量的dom元素操作時就很消耗瀏覽器性能。有個貼切的比喻:把 DOM
和 Javascript
各自想象成一個島嶼,它們之間用一個橋梁連接,每過一次橋就需要交納 ”過橋費“,當 js 頻繁操作 DOM 時,”過橋費“ 就越高昂,有沒有一種途徑來減少費用呢?推薦方法就是盡量少過橋(即非必須情況下),都呆在各自的島嶼上。所以 React 就創造了一個叫 虛擬DOM 的方法,它創造了虛擬dom並把它們儲存起來,每當狀態變化的時候就會創造新的虛擬節點和以前的進行對比,讓變化的部分進行渲染。整個過程沒有對dom進行獲取和操作,只有一個渲染過程。所以說 React 是一種 ui框架。
React的組件化
React 的 diff 算法用在了什么地方呢?當組件更新的時候,react會創建一個新的虛擬dom樹 並且會和之前存儲的 dom樹進比較,這個比較的過程就用到了 diff 算法
,所以組件初始化的時候是用不到的。react提出了一種假設,相同的節點具有類似的結構,而不同的節點具有不同的結構。在這種假設上進行逐層的比較,如果發現對應的節點是不同的,那就直接刪除舊的節點以及它所包含的所有子節點然后替換成新的節點。如果是相同的節點,則只是進行屬性的更改。
對於列表的 diff算法稍有不同,因為列表同程具有相同的結構,在對列表節點進行刪除、插入、排序的時候,單個節點的整體操作遠比一個個對比一個個替換要好很多,所以在創建列表的時候需要 設置key值,這樣 react 才能分清楚具體哪一個節點。當然不寫 key值也沒有錯誤(會有警告提示),會提示加上key值以提高 react 的性能。
diff算法圖示:
1 class Index extends React.Component{ 2 constructor(props){ 3 super(props) 4 this.state = { // 初始狀態 5 productList:[] // 商品列表 6 params:'', // 傳入的參數 7 } 8 } 9 }
當我們再使用 <Index /> 組件時,其實是對 Index 類的實例化 — new Index,只不過 react 對這個過程進行了封裝,讓它看起來更像是一個標簽。
注:1.定義的類名的首字母必須大寫;2.因為 class 變成了關鍵字,所以在添加類名的時候不能用 class,需要使用 className 代替;3.類和模塊內部默認使用 嚴格模式
,所以不需要使用 use strict 指定運行模式。
組件的生命周期函數
在 react 中經常會使用到生命周期函數,下面就列舉了一些可能會用到的生命周期函數:
組件在初始化時會觸發的5個鈎子函數:
-
getDefaultProps()
設置默認的 props,也可以用 defaultProps 設置組件的默認屬性
-
getInitalState()
在使用 es6 的 class 語法時是沒有這個鈎子函數的,可以直接在 constructor 中定義 this.state。此時可以訪問 this.props
-
componentWillMount()
組件初始化時調用,以后組件更新不調用,整個生命周期只調用一次,此時可以修改 state
-
render()
react 中最重要的步驟,創建虛擬dom,進行diff算法,更新(渲染)dom樹都在該函數中進行,此時就不能更改 state了
-
componentDidMount()
組件渲染之后調用,可以通過 this.getDOMNode()獲取和操作 dom 節點,只調用一次
在更新時也會觸發的5個鈎子函數:
-
componentWillReceiveProps(nextProps)
組件初始化時不調用,在接收新的 props 時調用
-
shouldComponentUpdate(nextProps,nextState)
react 性能優化非常重要的一環,在組件接收新的 state 或者 props 時調用,我們可以設置在此對比前后兩個 props 和 state 是否相同,如果相同則返回 false 阻止更新,因為相同的屬性狀態一定會生成相同的 dom樹,這樣就不需要創造新的dom樹和舊的dom樹進行 diff算法對比,從而節省大量性能,尤其是在 dom結構復雜的時候。(該函數存在返回值)
-
componentWillUpdate(nextProps,nextState)
組件初始化時不調用,只有在組件將要更新時才調用,此時可以修改 state
-
render()
和第一階段 render 函數相同
-
componentDidUpdate()
組件初始化時不調用,組件更新完成后調用,此時可以獲取 dom節點
在組件卸載時會觸發的1個鈎子函數:
-
componentWillUnmount()
組件將要被卸載時調用,一些事件監聽和定時器需要在此清除
以上可以看出來 react 在整個生命周期中存在 10個周期函數(render重復一次),這10個函數可以滿足我們對所有組件操作的需求,利用的好可以提高開發效率和組件性能
React-Router路由
Router 就是 React 的一個組件,它並不會渲染,只是一個創建內部路由規則的配置對象,根據匹配的路由地址展現相應的組件。Route 則對路由地址和組件進行綁定,Route 具有嵌套功能,表示路由地址的包含關系(多級路由),這和組件之間的嵌套並沒有直接聯系。Route可以綁定的組件傳遞七個屬性:children,history,location,params,route,routeParams,routes,每個屬性都包含路由的相關信息。比較常用的有children(以路由的包含關系來區分的組件),location(包括地址,參數,地址切換方式,key值,hash值)。react-router 提供 link 標簽,這只是對 a 標簽的封裝,注:點擊鏈接跳轉的並不是默認的方式,react-router阻止了a標簽的默認行為並用pushState進行hash值的轉變。切換頁面的過程是在點擊Link標簽或者后退前進按鈕時,會先發生url地址的轉變,Route監聽到地址的改變根據Route的path屬性匹配到對應的組件,將state值改成對應的組件並調用setState觸發render函數重新渲染dom。
當頁面比較多時,項目就會變得越來越大,尤其是對於單頁面應用來說,初次渲染的速度就會很慢(需要加載大量資源),這時候就需要按需加載,只有切換頁面的時候才去加載對應的js文件。react配合webpack(react-cli)進行按需加載的方法很簡單,Route的Component改為getComponent,組件用require.ensure的方式獲取,並在webpack中配置chunkFilename。
1 const chooseProducts = ( loaction,cb )=>{ 2 require.ensure( [],require =>{ 3 cb(null,require('../Component/chooseProducts').default) 4 },'chooseProducts' ) 5 } 6 const helpCenter = ( loaction,cb )=>{ 7 require.ensure( [],require =>{ 8 cb(null,require('../Component/helpCenter').default) 9 },'helpCenter' ) 10 } 11 const saleRecord = ( loaction,cb )=>{ 12 require.ensure( [],require =>{ 13 cb(null,require('../Component/saleRecord').default) 14 },'saleRecord' ) 15 } 16 const RouteConfig = { 17 <Router history = {history}> 18 <Route path = '/' component = {Roots}> 19 <Route path = 'index' component = {index} /> 20 <Route path = 'helpCenter' component = {helpCenter} /> 21 <Route path ='saleRecord' component = {saleRecord} /> 22 <Redirect from ='*' to = '/' /> // 路由重定向 23 </Route> 24 </Router> 25 }
react 推崇的是單向數據流
,自上而下進行數據的傳遞,但是由下而上或者不在一條數據流上的組件之間的通信就會變的很復雜。解決通信問題的方法很多,如果只是父子級關系,父級可以將一個需要傳遞的值當做一個屬性傳遞給子級,子級通過this.props
獲取父級傳來的值。
組件層級嵌套到比較深,可以使用上下文getChildContext
來傳遞信息,這樣不需要將函數一層一層往下傳,任何一層都可以通過this.context
直接訪問。
兄弟關系的組件之間無法直接通信,它們只能利用同一層的上級(父級)作為中間件進行傳遞。而如果兄弟組件都是最高層的組件,為了能夠讓它們通信,必須在它們外層再嵌套一層組件,這個外層的組件起着保存數據,傳遞信息的作用,這其實就是redux所做的事情。
組件之間的信息還可以通過全局事件來傳遞。不同頁面可以通過參數傳遞數據,下個頁面可以用location.param
來獲取。這就是react中的核心知識點
首先,聲明一下,redux並不是項目開發所必須的,它的作用相當於在頂層組件之上又加了一個組件,作用就是進行邏輯運算、儲存數據和實現組件尤其是頂層組件的通信。如果組件之間的通信不多,邏輯不復雜,只是單純的進行視圖渲染,這時候用回調,context就足夠了,沒必要使用redux,用了反而影響開發效率。但是如果組件通信特別多,邏輯也很復雜,那使用redux就方便多了。
先來說一下react和redux是怎么配合的。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讓它們聯系在一起的。
再具體分析 react 和 react-redux 的實現過程:
先具體介紹Redux
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 unsubsrible = store.subscrible(()=>{
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可以用actionCreator
進行創造。dispatch就是把action對象發送出去。
reducer
:
reducer是一個函數,它接收一個state和一個action,根據action的type返回一個新的state。根據業務邏輯可以分為很多個reducer,然后通過combineReducers將它們合並,state樹中有很多對象,每隔state對象對應一個reducer,state對象的名字可以在合並時定義。如下:
1 const reducer = combineReducers({ 2 a:doSomethingWithA, 3 b:processB, 4 c:c 5 })
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
如果只使用redux,那么流程是這樣:
component --> dispatch(action) --> reducer --> subscrilbe --> getState --> component
用了reac-redux之后的流程為:
component --> actionCreator(data) --> reducer --> component
store的三大功能:dispatch,subscribe和getState都不需要手動來寫了,react-redux幫我們做了這些,同時它提供了兩個方法:Provider和connect。
Provider是一個組件,它接收store作為props,然后通過context往下傳,這樣react中任何組件都可以通過context獲取store。也就意味着我們可以在任何一個組件里利用dispatch(action)
來觸發reducr改變state,並用subscribe監聽state的變化,然后用getState獲取變化后的值。但是並不推薦這樣做,它會讓數據流變的婚戀,過度的耦合也會影響組件的復用,維護起來也更麻煩。
connect是一個函數,connect(mapStateToProps,mapDispatchToProps,mergeProps,options)
接收四個參數並且再返回一個函數wrapWithConnect(component)
,該函數接收一個組件作為參數,其內部定義一個新組件Connect(容器組件)並將傳入的組件(ui組件)作為Connect的子組件然后return出去。
完整寫法:connect(mapStateToProps,mapDispatchToProps,mergeProps,options)(component)
mapStateToProps(state,[ownProps])
:
mapStateToProps接收兩個參數,store的state和自定義的props,並返回一個對象,這個對象會作為props的一部分傳入ui組件。我們可以根據組件所需要的數據自定義返回一個對象。ownProps的變化也會觸發mapStateToProps
1 function mapStateToProps(state){ 2 return { todos:state.todos } 3 }
mapDispatchToProps(dispatch,[ownProps])
:
mapDispatchToProps如果是對象,那么會和store綁定作為props的一部分傳入ui組件。如果是個函數,它接收兩個參數,bindActionCreators會將action和dispatch綁定並返回一個對象,這個對象會和ownProps的一部分傳入ui組件。所有不論是對象還是函數,它最終都會返回一個對象,如果是函數,這個對象的key值時可以自定義的
1 function mapDispatchToProps(dispatch){ 2 return { 3 todoActions: bindActionCreators(todoActionCreators,dispatch), 4 counterActions: bindActionCreators(counterActionCreators,dispatch) 5 }; 6 }
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組件里實現的。
復雜版:
-
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的變化;
-
簡化版:
-
Provider組件接受redux的store作為props,然后通過context往下傳
-
connect函數收到Provider傳出的store,然后接受三個參數mapStateToProps,mapDispatchToProps和組件,並將state和actionCreator以props傳入組件,這時組件就可以調用actionCreator函數來觸發reducer函數返回新的state,connect監聽到state變化調用setState更新組件並將新的state傳入組件
1 connect(state => state,action)(Component)
以上就是我對整個React框架的知識體系做出的簡單整理,希望對大家有所幫助(Tips:有錯誤望指出,謝謝!!!)