筆者從去年 12 月開始接觸react
,逐步由懵逼走向熟悉。不過react
的須要掌握的知識點可真的有點多呢。因此花了很長一段時間來整理這樣一篇react
的基礎知識總結的文章,以此達到溫故知新的目的。文章會涉及到react
自己的基礎知識(包括組件通信、生命週期、路由管理、狀態管理等方面),相信你認認真真看完這篇文章之後,你會對react
開發有個大體的瞭解,而且可以快速入門。這篇文章也可用做面試複習 react 基礎,而且這篇文章會持續更新,小夥伴們能夠點個收藏,防止迷路。廢話很少說,so ,Let's go!!!css
適用於父子組件通訊html
父組件將須要傳遞的參數經過key={xxx}
方式傳遞至子組件,子組件經過this.props.key
獲取參數.前端
import React from 'react'
import Son from './son'
class Father extends React.Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
}
state = {
info: '父組件',
}
handleChange = (e) => {
this.setState({
info: e.target.value,
})
}
render() {
return (
<div>
<input type='text' value={this.state.info} onChange={this.handleChange} />
<Son info={this.state.info} />
</div>
)
}
}
export default Father
// 子組件
import React from 'react'
interface IProps {
info?: string
}
class Son extends React.Component<IProps> {
constructor(props) {
super(props)
}
render() {
return (
<div>
<p>{this.props.info}</p>
</div>
)
}
}
export default Son
複製代碼
利用 props callback 通訊,父組件傳遞一個 callback 到子組件,當事件觸發時將參數放置到 callback 帶回給父組件.vue
// 父組件
import React from 'react'
import Son from './son'
class Father extends React.Component {
constructor(props) {
super(props)
}
state = {
info: '',
}
callback = (value) => {
// 此處的value即是子組件帶回
this.setState({
info: value,
})
}
render() {
return (
<div>
<p>{this.state.info}</p>
<Son callback={this.callback} />
</div>
)
}
}
export default Father
// 子組件
import React from 'react'
interface IProps {
callback: (string) => void
}
class Son extends React.Component<IProps> {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
}
handleChange = (e) => {
// 在此處將參數帶回
this.props.callback(e.target.value)
}
render() {
return (
<div>
<input type='text' onChange={this.handleChange} />
</div>
)
}
}
export default Son
複製代碼
適用於跨層級組件之間通訊java
// context.js
import React from 'react'
const { Consumer, Provider } = React.createContext(null) //建立 context 並暴露Consumer和Provide
export { Consumer, Provider }
// 父組件
import React from 'react'
import Son from './son'
import { Provider } from './context'
class Father extends React.Component {
constructor(props) {
super(props)
}
state = {
info: 'info from father',
}
render() {
return (
<Provider value={this.state.info}>
<div>
<p>{this.state.info}</p>
<Son />
</div>
</Provider>
)
}
}
export default Father
// 子組件
import React from 'react'
import GrandSon from './grandson'
import { Consumer } from './context'
class Son extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<Consumer>
{(info) => (
// 經過Consumer直接獲取父組件的值
<div>
<p>父組件的值:{info}</p>
<GrandSon />
</div>
)}
</Consumer>
)
}
}
export default Son
// 孫子組件
import React from 'react'
import { Consumer } from './context'
class GrandSon extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<Consumer>
{(info) => (
// 經過 Consumer 中能夠直接獲取組父組件的值
<div>
<p>組父組件的值:{info}</p>
</div>
)}
</Consumer>
)
}
}
export default GrandSon
複製代碼
特別注意react
若是須要消費多個 Context,則 React 須要使每個 consumer 組件的 context 在組件樹中成爲一個單獨的節點。webpack
// provider
...
<Context1.Provider value={this.state.info}>
<Context2.Provider value={this.state.theme}>
<div>
<p>{this.state.info}</p>
<p>{this.state.theme}</p>
<Son />
</div>
</Context2.Provider>
</Context1.Provider>
// consumer
...
<Context1.Consumer>
{(info: string) => (
// 經過Consumer直接獲取父組件的值
<Context2.Consumer>
{(theme: string) => (
<div>
<p>父組件info的值:{info}</p>
<p>父組件theme的值:{theme}</p>
</div>
)}
</Context2.Consumer>
)}
</Context1.Consumer>
複製代碼
不少優秀的 React 組件的核心功能都經過 Context 來實現的,好比 react-redux 和 react-router 等,因此掌握 Context 是必須的。nginx
OnRef 的原理很簡單,本質上就是經過 props 將子組件的組件實例(也是咱們常說的 this)看成參數,經過回調傳到父組件,而後在父組件就能夠拿到子組件的實例,拿到了它的實例就能夠調用它的方法(隨心所欲)了。git
// 父組件
import React from 'react'
import Son from './son'
import { Button } from 'antd'
class Father extends React.Component {
child: any
constructor(props) {
super(props)
}
sonRef = (ref) => {
this.child = ref // 在這裏拿到子組件的實例
}
clearSonInput = () => {
this.child.clearInput()
}
render() {
return (
<div>
<Son onRef={this.sonRef} />
<Button type='primary' onClick={this.clearSonInput}>
清空子組件的input
</Button>
</div>
)
}
}
export default Father
// 子組件
import React from 'react'
import { Button } from 'antd'
interface IProps {
onRef: any
}
class Son extends React.Component<IProps> {
constructor(props) {
super(props)
}
componentDidMount() {
this.props.onRef(this) // 在這將子組件的實例傳遞給父組件this.props.onRef()方法
}
state = {
info: 'son',
}
handleChange = (e) => {
this.setState({
info: e.target.value,
})
}
clearInput = () => {
this.setState({
info: '',
})
}
render() {
return (
<div>
<div>{this.state.info}</div>
<input type='text' onChange={this.handleChange} />
</div>
)
}
}
export default Son
複製代碼
ref
是react
提供給咱們的一個屬性,經過它,咱們能夠訪問 DOM
節點或者組件.github
// 父組件
import React from 'react'
import Son from './son'
import { Button } from 'antd'
class Father extends React.Component {
son: any
constructor(props) {
super(props)
this.son = React.createRef() // 在此處建立ref
}
clearSonInput = () => {
const { current } = this.son // 注意,這裏必須經過 this.son.current拿到子組件的實例
current.clearInput()
}
render() {
return (
<div>
<Son ref={this.son} />
<Button type='primary' onClick={this.clearSonInput}>
清空子組件的input
</Button>
</div>
)
}
}
export default Father
// 子組件
import React from 'react'
import { Button } from 'antd'
class Son extends React.Component {
constructor(props) {
super(props)
}
state = {
info: 'son',
}
handleChange = (e) => {
this.setState({
info: e.target.value,
})
}
clearInput = () => {
this.setState({
info: '',
})
}
render() {
return (
<div>
<div>{this.state.info}</div>
<input type='text' onChange={this.handleChange} />
</div>
)
}
}
export default Son
複製代碼
值得注意的是,咱們必須經過
this.childRef.current
才能拿到子組件的實例。
使用 ref 常見的場景有管理焦點,文本選擇或媒體播放、觸發強制動畫、集成第三方 DOM 庫等。
這種方式適用於沒有任何嵌套關係的組件通訊。其原理就是使用事件訂閱。便是一個發佈者,一個或者多個訂閱者。 使用以前須要先安裝:
yarn add events
複製代碼
// event.ts
import { EventEmitter } from 'events'
export default new EventEmitter()
// 發佈者 經過emit事件觸發方法,發佈訂閱消息給訂閱者
import React from 'react'
import Son1 from './son1'
import Son2 from './son2'
import { Button } from 'antd'
import emitter from './event'
class Father extends React.Component {
son: any
constructor(props) {
super(props)
}
handleClick = () => {
//emit事件觸發方法,經過事件名稱找對應的事件處理函數info,將事件處理函數做爲參數傳入
emitter.emit('info', '我是來自father的 info')
}
render() {
return (
<div>
<Button type='primary' onClick={this.handleClick}>
點擊按鈕發佈事件
</Button>
<Son1 />
<Son2 />
</div>
)
}
}
export default Father
// 訂閱者1
// 經過emitter.addListener(事件名稱,函數名)方法,進行事件監聽(訂閱)。
// 經過emitter.removeListener(事件名稱,函數名)方法 ,進行事件銷燬(取消訂閱)
import React from 'react'
import { Button } from 'antd'
import emitter from './event'
class Son1 extends React.Component {
constructor(props) {
super(props)
}
state = {
info: '',
}
componentDidMount() {
// 在組件掛載完成後開始監聽
emitter.addListener('info', (info) => {
this.setState({
info: 'Son1收到消息--' + info,
})
})
}
componentWillUnmount() {
// 組件銷燬前移除事件監聽
emitter.removeListener('info', (info) => {
this.setState({
info: 'Son1即將移除事件監聽--' + info,
})
})
}
render() {
return (
<div>
<div>{this.state.info}</div>
</div>
)
}
}
export default Son1
// 訂閱者2
import React from 'react'
import { Button } from 'antd'
import emitter from './event'
class Son2 extends React.Component {
constructor(props) {
super(props)
}
state = {
info: '',
}
componentDidMount() {
// 在組件掛載完成後開始監聽
emitter.addListener('info', (info) => {
this.setState({
info: 'Son2收到消息--' + info,
})
})
}
componentWillUnmount() {
// 組件銷燬前移除事件監聽
emitter.removeListener('info', (info) => {
this.setState({
info: 'Son2即將移除事件監聽--' + info,
})
})
}
render() {
return (
<div>
<div>{this.state.info}</div>
</div>
)
}
}
export default Son2
複製代碼
隨着前端工程的複雜度愈來愈高,因此路由管理在如今的前端工程中,也是一個值得注意的點,vue
提供了vue-router
來管理路由。類似的,react
則提供了react-router
來管理路由。
react-router
react-router
包含 3
個,分別爲react-router
、react-router-dom
和 react-router-native
。
react-router
提供最基本的路由功能,可是實際使用的時候咱們不會直接安裝 react-router
,而是根據應用運行的環境來選擇安裝 react-router-dom
和react-router-native
。由於react-router-dom
和 react-router-native
都依賴 react-router
,因此在安裝時,react-router
也會⾃自動安裝。
其中react-router-dom
在瀏覽器器中使⽤,而react-router-native
在 react-native
中使用。
在 react-router 裏面,一共有 3 種基礎組件,他們分別是
<BrowserRouter>
和 <HashRouter>
<Route>
和 <Switch>
<Link>
, <NavLink>
, 和 <Redirect>
對於 web 項目,react-roruter-dom 提供了 <BrowserRouter>
和 <HashRouter>
兩個路由組件。
BrowserRouter
:瀏覽器的路由方式,也就是使用 HTML5
提供的 history API
( pushState , replaceState 和 popstate 事件) 來保持 UI
和 url
的同步。這種方式在react
開發中是常用的路由方式,可是在打包後,打開會發現訪問不了頁面,因此須要經過配置 nginx
解決或者後臺配置代理。HashRouter
:在路徑前加入#號成爲一個哈希值,Hash
模式的好處是,不再會由於咱們刷新而找不到咱們的對應路徑,可是連接上面會有#/
。在vue
開發中,常用這種方式。要使用路由組件,咱們只須要確保它是在根組件使用,咱們應該將<App />
包裹在路由組件下面:
import { BrowserRouter } from 'react-router-dom';
...
<BrowserRouter>
<App />
</BrowserRouter>
...
複製代碼
有兩種路由匹配組件:<Route>
和 <Switch>
這兩個路由匹配組件一般在一塊兒使用,在<Switch>
裏面包裹多個<Route>
,而後它會逐步去比對每一個<Route>
的path
屬性 和瀏覽器當前location
的pathname
是否一致,若是一致則返回內容,不然返回null
。
<Switch>
<Route exact path='/' component={Home} />
{/* 若是當前的URL是`/about`,即 location = { pathname: '/about' } ,那麼About組件就應該被渲染,其他的Route就會被忽略 */
<Route path='/about' component={About} />
<Route path='/contact' component={Contact} />
</Switch>
複製代碼
值得注意 ⚠️ 的是: <Route path={xxx}>
只會匹配 URL
的開頭,而不是整個 URL。簡單的來講就是它不是精確匹配 ,例如<Route path ='/'>
和<Route path ='/about'>
它永遠都只能匹配到<Route path ='/'>
,他們開頭都有'/'。
在這裏咱們有兩種解決方法:
<Route path='/'>
放在<Switch>
的最後一個位置<Route exact='/'>
因此<Switch>
組件只會 render
第一個匹配到的路由,像上面咱們說的,若是沒有設置 path
,則必定會匹配,咱們能夠用來實現 404 的功能:
<Switch>
<Route exact path='/' component={Home} />
<Route path='/about' component={About} />
<Route path='/contact' component={Contact} />
{/* 當上面的組件都沒有匹配到的時候, 404頁面 就會被 render */}
<Route render={() => <div> 404頁面 </div>} />
</Switch>
複製代碼
導航組件有<Link>
, <NavLink>
, 和 <Redirect>
。
當咱們使用<Link>
的時候,在 html 頁面會被渲染爲一個a
標籤:
<Link to='/'>Home</Link>
// <a href='/'>Home</a>
複製代碼
<NavLink>
是一種特殊的<Link>
,當<NavLink>
中的地址和瀏覽器地址匹配成功的時候,會添加一個 style 樣式,以下:
<NavLink to='/about' activeClassName='active'>
About
</NavLink>
複製代碼
在 html 頁面當中,它會被渲染爲:
<a href='/about' className='active'>
About
</a>
複製代碼
可是有時你可能想要強制跳轉到某個頁面,好比未登陸不能進入首頁,這個時候你可使用<Redirect>
<Redirect to='/login' />
複製代碼
前端工程的複雜度愈來愈高,狀態管理也是一個很重要的點。在 react 生態中,如今最火的狀態管理方案就是redux
。
咱們都知道,react 是單向的數據流,數據幾乎都是經過 props 依次從上往下傳:
一個組件的狀態有兩種方式改變:
this.setstate
方法改變如今假如咱們組件樹的層級比較深,有不少子組件須要共享狀態,那麼咱們只能經過狀態提高來改變狀態,將狀態提高到頂級父組件改變,當頂級父組件的狀態改變了,那麼旗下的全部子節點都會從新渲染:
當出現這種狀況當時候,你就該使用redux
了。那麼使用redux
以後,就會變成這樣:
以上 gif 動圖很生動的展現了 redux 解決的問題,下面咱們來介紹一下 redux 相關的知識點:
在 redux 裏面,只有一個Store
,整個應用須要管理的數據都在這個Store
裏面。這個Store
咱們不能直接去改變,咱們只能經過返回一個新的Store
去更改它。redux
提供了一個createStore
來建立state
import { createStore } from 'redux'
const store = createStore(reducer)
複製代碼
這個 action
指的是視圖層發起的一個操做,告訴Store
咱們須要改變。好比用戶點擊了按鈕,咱們就要去請求列表,列表的數據就會變動。每一個 action
必須有一個 type
屬性,這表示 action
的名稱,而後還能夠有一個 payload
屬性,這個屬性能夠帶一些參數,用做 Store
變動:
const action = {
type: 'ADD_ITEM',
payload: 'new item', // 可選屬性
}
複製代碼
上面這個例子就定義了一個名爲ADD_ITEM
的Action
,它還攜帶了一個payload
的參數。 Redux
能夠用 Action Creator
批量來生成一些 Action
。
在上面咱們定義了一個Action
,可是Action
不會本身主動發出變動操做到Store
,因此這裏咱們須要一個叫dispatch
的東西,它專門用來發出action
,不過還好,這個dispatch
不須要咱們本身定義和實現,redux
已經幫咱們寫好了,在redux
裏面,store.dispatch()
是 View
發出 Action
的惟一方法。
store.dispatch({
type: 'ADD_ITEM',
payload: 'new item', // 可選屬性
})
複製代碼
當 dispatch
發起了一個 action
以後,會到達 reducer
,那麼這個 reducer
用來幹什麼呢?顧名思義,這個reducer
就是用來計算新的store
的,reducer
接收兩個參數:當前的state
和接收到的action
,而後它通過計算,會返回一個新的state
。(前面咱們已經說過了,不能直接更改state
,必須經過返回一個新的state
來進行變動。)
const reducer = function(prevState, action) {
...
return newState;
};
複製代碼
這個 reducer
是一個純函數。純函數的意思是說,對於相同的輸入,只會有相同的輸出,不會影響外部的值,也不會被外部的值所影響。純函數屬於函數式編程的概念,若是你想了解更多純函數的概念,請看這裏
能夠看到,咱們在建立store
的時候,咱們在createStore
裏面傳入了一個reducer
參數,在這裏,咱們就是爲了,每次store.dispatch
發送一個新的action
,redux
都會自動調用reducer
,返回新的state
。
那麼當項目特別大特別複雜的時候,state
確定是很是大的一個對象,因此咱們須要寫不少個 reducer
,那麼在這裏,咱們就須要把 reducer
進行拆分。每一個 reducer
只負責管理 state
的一部分數據。那麼咱們如何統一對這些 reducer
進行管理呢?redux
給咱們提供了 combineReducers
方法,顧名思義,就是將全部的子 reducer
合成一個 reducer
,方便咱們管理。
import { combineReducers } from 'redux'
import listReducer from './listReducer/reducers'
import detailReducer from './detailReducer/reducers'
import aboutReducer from './aboutReducer/reducers'
const rootReducer = combineReducers({
listReducer,
detailReducer,
aboutReducer,
})
export default rootReducer
複製代碼
熟悉koa
的朋友們,應該知道中間件的概念。中間件的意思簡單理解就是,在某兩個操做之間,咱們須要進行某些操做。那麼在 redux,咱們爲何要引入中間件呢?到目前爲止,咱們來捋一下咱們剛剛已經進行的步驟:
import { createStore } from 'redux'
const store = createStore(reducer)
複製代碼
store.dispatch({
type: 'ADD_ITEM',
payload: 'new item', // 可選屬性
})
複製代碼
const reducer = function(prevState, action) {
...
return newState;
};
複製代碼
咱們發現,咱們此次發起的變動,都是同步操做,那麼問題來了。假如咱們state
裏面有一個列表:list
,用戶根據在view
上面點擊了一些篩選條件,發起請求,而後變動state
裏面list
的值。在這裏,有異步請求,可是咱們變動 redux 的過程都是同步的,顯然是不支持異步的,因此這裏就用到中間件了。那麼咱們應該將異步請求放在以上哪一個步驟去執行呢?顯然第 1 步和第 3 步不可能,其中第 1 步只是在建立 store
,第 3 步 reducer
是純函數,根本不可能加入異步操做。因此咱們很天然的想到,就是在 store.dispatch
的以後,到達reducer
以前進行異步操做:
store.dispatch = function(prevAction) async{
console.log("發請求啦");
// 異步操做執行完成以後纔派發action
const list = await getList();
// 把 list 放到action裏面
const newAction = {
type: prevAction.type,
payload:list
}
store.dispatch(newAction);
};
複製代碼
就是給store.dispatch
再包裹一層,這就是中間件的原理。 redux 常見的中間件有redux-thunx
、redux-promise
、redux-saga
。相關的詳細用法在這裏再也不贅述(下面會介紹dva-core
的用法)。 redux 應用中間件的方法:
import { applyMiddleware, createStore } from 'redux'
import myMiddleware from './myMiddleware'
const store = createStore(reducer, applyMiddleware(myMiddleware))
複製代碼
那麼到這一步了,咱們變動了 state
,下一步是將變動通知給 view
了。在 redux 裏面,提供了store.subscribe(listener)
這個方法,這個方法傳入一個listener
,好比在 react
裏面,listener
能夠是this.setState(xxx)
,每當 redux
裏面的state
改變了,經過store.subscribe(listener)
咱們的頁面也會從新渲染。意思是咱們每一個頁面都得手動去store.subscribe(listener)
,這也太麻煩了吧,對吧。
react-redux
和 redux
爲了解決上述的痛點問題,更好的將 redux
和 react
結合,官方給咱們提供了react-redux
這個包(可能你到如今腦子有點亂了,我剛開始也是)。那麼如今,咱們須要明確一個概念:redux
和 react
是兩個八竿子不着的人。redux
只是一個狀態管理框架,react
只是一個前端應用框架。redux
能夠用於前端任何框架,例如 vue
,甚至純 javaScript
均可以。後來 react-redux
出現了,他把 redux
和 react
撮合在一塊兒了,因而他們兩強強聯合,風雲合璧,所向披靡,好了不扯了。說了這麼多就是想說明 react-redux
這個包的做用。
在詳細說明react-redux
的做用以前,咱們先介紹如下知識點: react-redux
將 react 組件劃分爲容器組件
和展現組件
,其中
props
;展現組件
進行 UI 呈現容器組件是react-redux
提供的,也就是說,咱們只須要負責展現組件,react-redux
負責狀態管理。
咱們知道,redux
提供了一個大的state
。這裏咱們須要面對兩個問題,第一個問題,如何讓咱們react
項目裏面的全部組件都可以拿到state
?;第二個,每當state
變動以後,組件如何收到變動信息?
Provider
針對第一個問題,react-redux
提供了Provider
組件。用這個Provider
包裹根組件,將redux
導出的state
,做爲參數往下面傳
import React from "react";
import { Provider } from "react-redux";
import App from "./App";
import { store } from "./store"; // 這個store由redux導出
···
<Provider store={store}>
<App />
</Provider>;
···
return
複製代碼
這樣全部的組件都能拿到state
了。這個 Provider 組件原理就是經過react
的Context
來實現的,咱們能夠看看源碼:
....
const Context = context || ReactReduxContext;
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
....
複製代碼
這裏的contextValue
就包裹了咱們傳入的store
,很明顯,它建立了 Context,經過<Context.Provider value={contextValue}>{children}</Context.Provider>
這種方式將咱們傳入的store
提供給了react
全部組件。
connect
在上面咱們知道怎麼將 redux 暴露出來的 state 提供給 react 組件的,那麼接下來,咱們在某個子組件裏面如何收到 state 的變動呢?react-redux
給咱們提供了connect
方法。這個方法能夠傳入兩個可選參數:mapStateToProps
和mapDispatchToProps
,而後會返回一個容器組件,這個組件能夠自動監聽到 state
的變動,將 state
的值映射爲組件的 props
參數,以後咱們能夠直接經過 this.props
取到 state
裏面的值。
const mapStateToProps = (state) => ({
goodsList: state.goodsList,
totalCount: state.totalCount,
});
export default connect(
mapStateToProps, // 可選
// mapDispatchToProps, // 可選
(GoodsList);
複製代碼
mapStateToProps
就是將 state 的值映射爲組件的props
,mapDispatchToProps
就是將store.dispatch
映射爲props
。若是咱們不傳mapDispatchToProps
的話,connect
會自動將 dispatch
注入到 props
裏面,咱們在組件裏能夠直接經過this.props.dispatch
發起一個action
給reducer
。
connected-react-router
和 redux
當咱們在項目中同時用了react-router
和 redux
的時候,咱們能夠把他們兩個深度整合。咱們想要在store
裏面拿到router
,甚至能夠操做router
,還能夠記錄router
的改變。例如咱們把用戶是否登陸的狀態存在redux
裏面,在登陸以後會進行頁面的跳轉。正常的操做是咱們在發起請求以後,獲得一個狀態,此時咱們須要dispatch
一個action
去改變redux
的狀態,同時咱們須要進行路由的跳轉,相似於這樣:store.dispatch(replace('/home'))
。想要實現這種深度整合,咱們須要用到 connected-react-router
和history
兩個庫。
首先須要history
生成history
對象,結合connected-react-router
的connectRouter
生成routerReducer
,同時利用connected-react-router
的routerMiddleware
實現dispatch action
導航,也就是咱們剛剛說的store.dispatch(replace('/home'))
:
// APP.tsx
const createHistory = require('history').createBrowserHistory
export const history = createHistory()
// reducer/index.ts
const routerReducer = connectRouter(history)
const routerMiddlewareForDispatch = routerMiddleware(history)
const middleware = [routerMiddlewareForDispatch]
複製代碼
接着利用redux
的combineReducers
將咱們本身的reducer
和routerReducer
合併起來,組成rootReducer
,而後利用 createStore
建立store
並暴露出去:
// reducer/index.ts
export default function geneGrateSotore(history: any) {
const routerReducer = connectRouter(history)
const routerMiddlewareForDispatch = routerMiddleware(history)
const middleware = [routerMiddlewareForDispatch]
//合併routerReducer
const rootRuder = combineReducers({
info: infoRuder,
router: routerReducer,
})
const store = createStore(rootRuder, applyMiddleware(...middleware))
return store
}
複製代碼
最後咱們在App.tsx
導入剛剛咱們建立的這個方法,生成store
,接着將咱們建立的history
對象傳入connected-react-router
的ConnectedRouter
組件做爲props
,並用它包裹咱們的Router
組件:
// App.tsx
import React from 'react'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import geneGrateSotore from './store'
import Router from './router'
import './App.css'
const createHistory = require('history').createBrowserHistory
const history = createHistory()
const store = geneGrateSotore(history)
const f: React.FC = () => {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<Router></Router>
</ConnectedRouter>
</Provider>
)
}
export default f
複製代碼
這樣咱們就將connected-react-router
和 redux
整合起來了。如今當咱們在View
利用Link
進行路由跳轉的時候,會經過react-router-dom
進行路由跳轉,而且也會經過connected-react-router
發起一個action
去更新redux state
裏面的router
對象,以記錄路由的變化。同時如今咱們在狀態管理的時候,也能夠直接經過connected-react-router
提供的push
、replace
等方法了,他們是從 redux
出發,也就是說先發起一個action
,而後再進行路由跳轉。
看了上面的這些東西,是否是感受腦子賊亂,什麼react
、redux
、react-redux
、react-router
、react-router-dom
、connected-react-router
,這些概念是真的多,我剛開始接觸的時候直接看懵逼。。因此 react 的可組合性比 vue 高不少,因此說 vue 真的是自動擋、react 是手動擋。可是咱們只需記住,前端的一切概念,都是紙老虎而已。靜下心來捋一捋,很快就理解了。好了,如今來看看這個圖:
結合上面介紹的知識點,把思路捋順,我們再繼續 💪
經過上面的這些工具,咱們已經能夠搭建一個很棒的基於react
和redux
的工程。若是單純使用redux
,當項目愈來愈大的時候,咱們的redux
的操做也會變得繁雜,代碼和文件的組織會變得臃腫,咱們就必須把action
、reducer
、createActions
、actionType
等等文件放在不一樣的目錄下面,當咱們須要使用的時候,就要打開文件目錄去翻半天,咱們須要在這些文件裏面不停切換,簡直抓狂。因此咱們不由會想:要是全部的操做都放在一個文件該多好!沒錯,它來了。它就是dva
。
dva 首先是一個基於 redux 和 redux-saga 的數據流方案,而後爲了簡化開發體驗,dva 還額外內置了 react-router 和 fetch,因此也能夠理解爲一個輕量級的應用框架。
由於咱們前面已經本身組織了react-router
,因此咱們只使用dva-core
,有了這個,咱們就能夠將 reducers
, effects
等等組織在一個model
文件裏面了。之前咱們組織代碼的方式是createAction.ts
、actionType.ts
、reducer/xxx.ts
,可是經過dva
,咱們就能夠把這些都寫在一個文件裏面。接下來咱們來看看如何配置dva-core
:
首先咱們須要經過dva create
暴露一個dva app
:
// dva/index.tsx
import React from 'react'
import { create } from 'dva-core'
import { Provider } from 'react-redux'
export default function(options: any) {
const app = create(options)
options.models.forEach((model: any) => app.model(model))
app.start()
const store = app._store
app.start = (container: any) => {
return () => <Provider store={store}>{container}</Provider>
}
app.getStore = () => store
return app
}
複製代碼
上面的 options
便是咱們的 model
文件。而後咱們須要利用這個 app
從新配置咱們的 store
:
/// store/index.ts
import { connectRouter, routerMiddleware } from 'connected-react-router'
import dva from './dva'
import models from '../models'
export default function geneGrateSotore(history: any) {
const routerReducer = connectRouter(history)
const routerMiddlewareForDispatch = routerMiddleware(history)
const app = dva({
models,
initState: {},
extraReducers: { router: routerReducer },
onAction: [routerMiddlewareForDispatch],
})
return app
}
複製代碼
而後同樣的,咱們在App.tsx
使用它便可:
import React from 'react'
import { ConnectedRouter } from 'connected-react-router'
import geneGrateSotore from './store'
import Router from './router'
import './App.css'
const createHistory = require('history').createBrowserHistory
const history = createHistory()
const app = geneGrateSotore(history)
const f: React.FC = app.start(
<ConnectedRouter history={history}>
<Router />
</ConnectedRouter>
)
export default f
複製代碼
這和以前配置 redux
對比起來,咱們已經省了不少的文件了,咱們的 store
文件清淨多了。下面咱們要來編寫model
文件了。例若有個模塊叫model1
:
// model/model1.ts
export const namespace = 'model1'
interface IDvaState {
info1: string
}
const state: IDvaState = {
info1: 'init info1',
}
const model1 = {
namespace,
state,
effects: {
*changeInfo1({ payload }, { put }) {
try {
const { text } = payload
yield put({
type: 'setState',
payload: { info1: text },
})
// eslint-disable-next-line no-empty
} catch (error) {}
},
},
reducers: {
setState(state: IDvaState, { payload }) {
return { ...state, ...payload }
},
},
}
export default [model1]
複製代碼
這個文件可能有的小夥伴一會兒看不明白,接下來咱們來一塊兒捋一捋。
namespace
: 命名空間(咱們組件能夠經過這個 namespace
拿到咱們的 model
,從而取值和進行操做)state
:這個就是咱們須要管理的狀態(存放狀態的地方)effects
:一些操做,咱們能夠把同步操做和異步操做都放到effects
裏面,簡單來講就是咱們的業務邏輯。這裏會詳細介紹,我們先明白它是作什麼的reducers
: reducer
這個概念就是跟redux
的reducer
是同一個意思(返回一個newState
,更新state
) 好了有了上面這些概念,接下來咱們來看看如何把組件和model
聯繫起來。// home.tsx
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { Button } from 'antd'
import { namespace } from '../models/model1'
interface Iprops {
info1: string
dispatch: any
}
export class Home extends Component<Iprops> {
changeFormHome = () => {
const { dispatch } = this.props
dispatch({
type: `${namespace}/changeInfo1`,
payload: {
text: '從Home改變的info1',
},
})
}
render() {
console.log(this.props.info1)
return (
<div>
<p>我是home頁</p>
<p>{this.props.info1}</p>
<Button onClick={this.changeFormHome}> home點擊更改redux</Button>
</div>
)
}
}
const mapStateToProps = (model: any) => ({
info1: model[namespace].info1,
})
export default connect(mapStateToProps)(Home)
複製代碼
看了上面的代碼,你是否是瞬間理解了,我們仍是經過react-redux
的connect
來獲取model
的,只不過咱們經過namespace
指定了具體是哪一個model
。到目前爲止,我們已經知道,組件如何從model
裏面取值。那麼咱們在組件裏面如何改變 model
裏面的 state
呢?
如今咱們的home.tsx
頁面上有一個按鈕,點擊這個按鈕會dispatch
一個action
:
...
dispatch({
type: `${namespace}/changeInfo1`,
payload: {
text: '從Home改變的info1',
},
})
...
複製代碼
能夠看到,這個 action 的 type 爲咱們的model/effects/changeInfo1
。看到這裏相信你已經理解得差很少了,對的,就是經過 dispatch
一個和 effects
裏面的一個同名方法,便可找到 effects
對應的方法。
接下來到effect
對應的changeInfo1
看看是如何改變 state。
...
*changeInfo1({ payload }, { put }) {
try {
const { text } = payload
yield put({
type: 'setState',
payload: { info1: text },
})
// eslint-disable-next-line no-empty
} catch (error) {}
}
...
複製代碼
咱們經過 put
,發起了一個action
,到最終的reducer
,其中的payload
來自咱們傳入的text
,而後state
被修改了,同時頁面也刷新了:
除了put
,還有call
(用於發起異步請求)、select
(用於取出 state
裏面的值),都是redux-saga
提供的,這裏不過多敘述,具體使用的方法請查閱redux-saga文檔。
我這裏整理了兩個圖,對比redux
和dva
的流程操做:
簡單總結一下:dva
把 action -> reducer
拆分紅了,action -> model(reducer, effect)
咱們在寫業務的時候,不再用處處去找咱們的reducer
、action
了。dva
真的給咱們提供了極大的便利。
舊版生命週期 指的是 React 16.3 及其以前的版本。
請看下面兩段代碼
// 父組件
import React, { Component } from 'react'
class Parent extends Component {
static defaultProps = {
info:'parent info'
}
constructor(props){
super();
//初始化默認的狀態對象
this.state = {
name:'parent life'
};
console.log('1. constructor 初始化 props 和 state')
}
componentWillMount(){
console.log('2. componentWillMount 組件將要掛載')
}
//通常在componentDidMount執行反作用,如異步請求
componentDidMount() {
console.log('4. componentDidMount 組件掛載完成')
this.fetch() //異步請求
}
shouldComponentUpdate(nextProps,nextState){
console.log('5. shouldComponentUpdate 詢問組件是否須要更新, 返回true則更新,不然不更新')
return true;
}
componentWillUpdate(nextProps, nextState){
console.log('6. componentWillUpdate 組件將要更新')
}
componentDidUpdate(prevProps, prevState)){
console.log('7. componentDidUpdate 組件更新完畢')
}
changeName = ()=>{
this.setState({number:this.state.number})
};
render() {
console.log('3.render渲染')
return (
const { info } = this.props
const { name } = this.state
<div>
<p>{this.props.info}:{this.state.name}</p>
<button onClick={this.changeName}>更改name</button>
<Son parentName={name}>
</div>
)
}
}
export default Parent
// 子組件
import React, { Component } from 'react'
class Son extends Component {
constructor(props) {
super(props)
this.state = {
info: 'son info',
}
}
componentWillUnmount() {
console.log('Son componentWillUnmount 組件將要掛載')
}
//調用此方法的時候會把新的屬性對象和新的狀態對象傳過來
shouldComponentUpdate(nextProps, nextState) {
if (nextProps.parentName !== 'parent info') {
return true
} else {
return false
}
}
//componentWillReceiveProp 組件收到新的屬性對象
componentWillReceiveProps() {
console.log('1.Son組件 componentWillReceiveProps')
}
render() {
console.log(' 2.Son組件 render')
return (
<div>
<p>{this.props.parentName}</p>
</div>
)
}
}
export default Son
複製代碼
react 父子組件的渲染順序遵循
洋蔥模型
static getDerivedStateFromProps(nextProps,prevState)
:接收父組件傳遞過來的 props
和組件以前的狀態,返回一個對象來更新 state
或者返回 null
來表示接收到的 props
沒有變化,不須要更新 state.state
上面,這樣組件內部就不用再經過 this.props.xxx
獲取屬性值了,統一經過 this.state.xxx
獲取。映射就至關於拷貝了一份父組件傳過來的 props
,做爲子組件本身的狀態。注意:子組件經過 setState
更新自身狀態時,不會改變父組件的 props
componentDidUpdate
,能夠覆蓋 componentWillReceiveProps
的全部用法props
時會被調用props
、組件狀態更新時會被調用// 根據新的屬性對象派生狀態對象
// nextProps:新的屬性對象 prevState:舊的狀態對象
static getDerivedStateFromProps(nextprops, state) {
console.log('props',nextprops);
// 返回一個對象來更新 state 或者返回 null 來表示接收到的 props 不須要更新 state
if (nextProps.parentName !== 'parent info') {
return {
info: nextprops.parentName,
// 注意:這裏不須要把組件自身的狀態也放進來
};
}
return null;
}
複製代碼
getSnapshotBeforeUpdate(prevProps
, prevState
):接收父組件傳遞過來的 props
和組件以前的狀態,今生命週期鉤子必須有返回值,返回值將做爲第三個參數傳遞給 componentDidUpdate
。必須和 componentDidUpdate
一塊兒使用,不然會報錯。render
以後、更新 DOM
和 refs
以前DOM
和 refs
以前,從 DOM
中捕獲一些信息(例如滾動位置)componentDidUpdate
, 能夠覆蓋 componentWillUpdate
的全部用法getSnapshotBeforeUpdate() {
// 返回更新內容的高度
return this.wrapper.current.scrollHeight;
}
componentDidUpdate(prevProps, prevState, prevScrollHeight) {// 在這裏拿到高度
this.wrapper.current.scrollTop =
this.wrapper.current.scrollTop +
(this.wrapper.current.scrollHeight - prevScrollHeight);
}
複製代碼
componentWillMount
,componentWillReceiveProps
,componentWillUpdate
這三個生命週期由於常常會被誤解和濫用,因此被稱爲 不安全(不是指安全性,而是表示使用這些生命週期的代碼,有可能在將來的 React
版本中存在缺陷,可能會影響將來的異步渲染) 的生命週期。UNSAFE_componentWillMount
,UNSAFE_componentWillReceiveProps
和 UNSAFE_componentWillUpdate
。(舊的生命週期名稱和新的別名均可以在此版本中使用)componentWillMount
,componentWillReceiveProps
和 componentWillUpdate
啓用棄用警告。(舊的生命週期名稱和新的別名均可以在此版本中使用,但舊名稱會記錄 DEV 模式警告)。此段總結均來自你真的瞭解 React 生命週期嗎
咱們都知道,react 是數據驅動視圖的變化,便是經過reder
來渲染視圖,當數據(即狀態)變化時,咱們的頁面就應當從新渲染。可是應用複雜以後就會出現這種狀況:一個父組件 A 下面包含了多個子組件 B、C、D。假如 B、C 組件用到了父組件 A 的某個屬性,子組件 D 卻沒有用到這個屬性,當父組件的這個屬性改變的時候,他下面的子組件 B、C 組件從新渲染,可是子組件 D 本不須要從新渲染,可是他沒辦法,他也被從新渲染了。這就形成了性能浪費了。說這麼多,不如咱們來看個例子:
// 父組件
import React, { Component } from 'react'
import { Button } from 'antd'
import Son1 from './son1'
import Son2 from './son2'
import Son3 from './son3'
interface Istate {
info1: string
info2: string
}
export class Parent extends Component<Istate> {
state: Istate = {
info1: 'info1',
info2: 'info2',
}
info1Change = () => {
this.setState({
info1: 'info1被改變了...',
})
}
render() {
return (
<div>
<p>父組件</p>
<Button onClick={this.info1Change}> 點擊更改info1</Button>
<Son1 info1={this.state.info1} />
<Son2 info2={this.state.info2} />
</div>
)
}
}
export default Parent
// 子組件1
import React, { Component } from 'react'
interface Iprops {
info1: string
}
class Son1 extends Component<Iprops> {
render() {
console.log('son1從新渲染了....')
return (
<div>
<p>我是son1</p>
<p>{this.props.info1}</p>
</div>
)
}
}
export default Son1
// 子組件2
import React, { Component } from 'react'
interface Iprops {
info2: string
}
class Son2 extends Component<Iprops> {
render() {
console.log('son2從新渲染了....')
return (
<div>
<p>我是son2</p>
<p>{this.props.info2}</p>
</div>
)
}
}
export default Son2
複製代碼
上面這個例子,父組件提供了兩個值:info1
和 info2
,其中 Son1
組件只用到了 info1
,Son2
組件只用到了 info2
。咱們在父組件中,點擊了按鈕改變了 info1
的值,父組件必須從新渲染,由於它自身狀態改變了,Son1
也應該從新渲染,由於它依賴於 info1
,而 Son2
是否應該從新渲染呢?按道理,它不該該從新渲染,由於 info2
沒有改變,可是當咱們每次點擊按鈕改變 info1
的時候,Son1
和Son2
都從新渲染了,這就明顯存在問題了。
在上面 👆 生命週期章節,咱們講到了shouldComponentUpdate
這個生命週期鉤子,它接收兩個參數,一個是下一次的 props
和下一次的 state
,在這裏,咱們拿到下一次的 props
(nextProps
)和當前的 props
進行比較,根據咱們的場景,返回一個 bool
變量,返回 true
,則表示要更新當前組件,返回 false
則表示不更新當前組件。
import React, { Component } from 'react'
interface Iprops {
info2: string
}
class Son2 extends Component<Iprops> {
// 利用生命週期 shouldComponentUpdate 進行比較
shouldComponentUpdate(nextProps: Iprops, nextState: any) {
if (nextProps.info2 === this.props.info2) return false
return true
}
render() {
console.log('son2從新渲染了....')
return (
<div>
<p>我是son2</p>
<p>{this.props.info2}</p>
</div>
)
}
}
export default Son2
複製代碼
當咱們再次點擊按鈕更改info1
的值,發現Son2
就不會再從新渲染了。
react
爲咱們提供了PureComponet
的語法糖,用它也能夠用做組件是否渲染的比較。它的原理就是內部實現了shouldComponentUpdate
。讓咱們用PureComponet
來改造一下剛剛的Son2
組件:
import React, { PureComponent } from 'react'
interface Iprops {
info2: string
}
class Son2 extends PureComponent<Iprops> {
render() {
console.log('son2從新渲染了....')
return (
<div>
<p>我是son2</p>
<p>{this.props.info2}</p>
</div>
)
}
}
export default Son2
複製代碼
再次點擊按鈕改變info1
的值,發現Son2
也不會渲染了。
雖然PureComponent
幫咱們很好的實現了shouldComponentUpdate
,可是它也是有缺點的。它只能用做對象的淺層比較,也就是它只會進行一層比較,當咱們的數據是嵌套的對象或者數組的時候,它就沒法比較了。因此PureComponent
最好只用於展現型組件
。
除了以上缺點之外,PureComponent
還有一些另外值得咱們注意的地方:
當咱們給PureComponent
包裹的子組件傳入一個當即執行函數
的時候,父組件的狀態改變的時候,這個子組件始終會從新渲染:
<Son2 info2={this.state.info2} change={() => {}} />
複製代碼
這個問題的出現是由於 change
這個函數每次都會執行,因此致使 Son2
組件每次都會從新渲染。這個問題的解決方法很簡單,有兩種方法:
constructor
bind this
:constructor(props: any) {
super(props)
this.change = this.change.bind(this)
}
state: Istate = {
info1: 'info1',
info2: 'info2',
}
info1Change = () => {
this.setState({
info1: 'info1被改變了...',
})
}
change() {}
render() {
return (
<div>
<p>父組件</p>
<Button onClick={this.info1Change}> 點擊更改info1</Button>
<Son1 info1={this.state.info1} />
<Son2 info2={this.state.info2} change={this.change} />
</div>
)
}
複製代碼
state: Istate = {
info1: 'info1',
info2: 'info2',
}
info1Change = () => {
this.setState({
info1: 'info1被改變了...',
})
}
change = () => {}
render() {
return (
<div>
<p>父組件</p>
<Button onClick={this.info1Change}> 點擊更改info1</Button>
<Son1 info1={this.state.info1} />
<Son2 info2={this.state.info2} change={this.change} />
</div>
)
}
複製代碼
剛剛咱們介紹了PureComponent
,可是這只是用於class
組件,當咱們用函數組件時,react
也給咱們提供了一種方式:memo
import React, { memo } from 'react'
interface Iprops {
info2: string
}
const Son3: React.FC<Iprops> = (props) => {
console.log('son3從新渲染了....')
return (
<div>
<p>我是Son3</p>
<p>{props.info2}</p>
</div>
)
}
export default memo(Son3)
複製代碼
不過使用 memo 的時候也有PureComponent
的限制,咱們仍然須要注意。
看到了這裏,確定不少同窗都在問,hooks 哪去了?別急呢,立刻就來 👇
隨着react 16.8
版本的出現,hooks
也問世了。hooks
解決了class
組件飽受詬病的衆多問題,好比綁定this
的問題、組件的邏輯複用的問題等等。其實起初我對hooks
並不十分感冒,由於那個時候筆者連class
寫法都還沒掌握,再學個這玩意簡直徒增煩惱。後來沒辦法,團隊裏的小夥伴都開始使用hooks
,因此我也被動學習了一波。這不寫不知道,一寫就真香!!!因此趕忙學起來吧
記住一個點,useState
返回兩個參數,一個是state
(也就是咱們的state
)、一個是用於更新state
的函數。其實你叫啥名都行,不過我們爲了給 hooks 一個標誌,大多采用以下寫法:
const [name, setName] = useState(initState)
複製代碼
好了,掌握這個就好了,咱們來使用一下:(別忘記了,咱們如今只須要寫函數式組件了哦)
import React, { useState } from 'react'
import { Button } from 'antd'
const Home: React.FC<Iprops> = ({ dispatch, goodsList }) => {
const [info, setInfo] = useState('init info')
return (
<div>
<p>{info}</p>
<Button onClick={() => setinfo('改變info')}> 點擊更改info</Button>
</div>
)
}
export default Home
複製代碼
當咱們初次進入 home
頁面時,頁面上會顯示 info
的初始值:init info
,而後當咱們點擊按鈕,調用 setInfo
,而後 info
的值就被改變了。就是這麼簡單。
useEffect
其實只比複雜了那麼一點。它合成了 calss
組件中的componentDidMount
、componentDidUpdate
、 componentWillUnmount
。 咱們很容易就明白,它是用來執行反作用的,最最多見的反作用就是異步請求。
下面咱們用它來實現class
組件的componentDidMount
的用法:
const Home: React.FC<Iprops> = ({ dispatch, goodsList }) => {
// 獲取商品列表
const getList = () => {
dispatch({
type: `${namespace}/getGoodsList`,
})
}
useEffect(() => {
getList() // 調用方法發起異步請求
}, [])
return (
<div style={{ marginTop: '5px', marginLeft: '400px', marginRight: '400px' }}>
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around' }}>
{goodsList.map((item, index) => {
return <Card hoverable style={{ width: 240 }} cover={<img alt='example' src={item} />}></Card>
})}
</div>
</div>
)
}
const mapStateToProps = (model) => ({
goodsList: model[namespace].goodsList,
})
export default connect(mapStateToProps)(Home)
複製代碼
上面的getList
就是我們發起異步請求的方法,咱們在useEffect
裏面使用了它,同時咱們還傳入了一個[]
,就表示咱們只需在頁面初始化的時候發起請求,這樣使用就至關於class
組件的componentDidMount
。
接下來咱們再來實現class
組件的componentDidUpdate
的用法:
import React, { useState, useEffect } from 'react'
import { connect } from 'react-redux'
import { Button, Card } from 'antd'
import { namespace } from '../models/model1'
interface Iprops {
goodsList: any[]
dispatch: any
}
const Home: React.FC<Iprops> = ({ dispatch, goodsList }) => {
const [info, setInfo] = useState('init info')
// 獲取商品列表
const getList = () => {
dispatch({
type: `${namespace}/getGoodsList`,
})
}
useEffect(() => {
getList()
}, [info])
return (
<div style={{ marginTop: '5px', marginLeft: '400px', marginRight: '400px' }}>
<p>我是home頁</p>
<p>{info}</p>
<Button onClick={() => setInfo('改變info')}> 點擊更改info</Button>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{goodsList.map((item, index) => {
return <Card hoverable style={{ width: 240 }} cover={<img alt='example' src={item} />}></Card>
})}
</div>
</div>
)
}
const mapStateToProps = (model) => ({
goodsList: model[namespace].goodsList,
})
export default connect(mapStateToProps)(Home)
複製代碼
看上面,咱們但願點擊按鈕時改變 info
時,它會自動再去發起請求,從而刷新頁面(也就是說,goodsList
的數據依賴於 info
)。能夠看見,咱們這裏仍是利用了useEffect
的第二個參數,只不過此次咱們傳入的是[info]
,意思就是告訴useEffect
,若是 info
的值發生改變了,就去發起請求。這至關於咱們在class
組件的componentDidMount
鉤子。
接下來還有最後一個class
組件的componentWillUnmount
的用法了。這個就更簡單了,咱們只須要在useEffect
return
一個回調函數,就能夠用來清除上一次反作用留下的反作用了:
....
useEffect(() => {
getList()
return () => dispatch({ type: `${namespace}/clearData` })
}, [])
....
複製代碼
這個hook
更簡單了,它就是用來拿到子組件的實例的,至關於class
組件的React.createRef()
:
import React, { useState, useEffect, useRef } from 'react'
import { connect } from 'react-redux'
import { Button, Card } from 'antd'
import { namespace } from '../models/model1'
import Son from './components/son'
interface Iprops {
goodsList: any[]
dispatch: any
}
const Home: React.FC<Iprops> = ({ dispatch, goodsList }) => {
const sonRef = useRef(null) // 在這裏新建一個子組件的ref
const [info, setInfo] = useState('init info')
// 獲取商品列表
const getList = () => {
conson.log(sonRef.current) // 在這裏就能夠經過sonRef拿到子組件
dispatch({
type: `${namespace}/getGoodsList`,
})
}
useEffect(() => {
getList()
}, [info])
return (
<div>
<p>我是home頁</p>
<p>{info}</p>
<Button onClick={() => setInfo('改變info')}> 點擊更改info</Button>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{goodsList.map((item, index) => {
return <Card hoverable style={{ width: 240 }} cover={<img alt='example' src={item} />}></Card>
})}
</div>
<Son />
</div>
)
}
const mapStateToProps = (model) => ({
goodsList: model[namespace].goodsList,
})
export default connect(mapStateToProps)(Home)
複製代碼
useContext
這個hook
的做用也很簡單,它可讓咱們在函數組件中使用Context
,並且它還解決了之前咱們須要利用Consumer
包裹組件的問題:
// context.js
import React from 'react'
const { Provider, Consumer } = React.createContext(null) //建立 context 並暴露Provider和Consumer
export { Provider, Consumer }
// 父組件
import React from 'react'
import Son from './son'
import { Provider } from './context'
class Father extends React.Component {
constructor(props) {
super(props)
}
state = {
info: 'info from father',
}
render() {
return (
<Provider value={this.state.info}>
<div>
<Son />
</div>
</Provider>
)
}
}
export default Father
複製代碼
在 class
組件裏面,咱們要想拿到 Context 裏面的值,必須經過 Consumer
包裹組件:
// 子組件
import React from 'react'
import { Consumer } from './context'
class Son extends React.Component {
constructor(props) {
super(props)
}
render() {
return (
<Consumer>
{(info) => (
// 經過Consumer直接獲取父組件的值
<div>
<p>父組件的值:{info}</p>
</div>
)}
</Consumer>
)
}
}
export default Son
複製代碼
有了 useContext
,就只須要這樣:
// 子組件
import React from 'react'
funcion Son() {
const info = useContext(Context)
render() {
return (
<p>父組件的值:{info}</p>
)
}
}
export default Son
複製代碼
咱們能夠看到上面直接使用 React.useContext(Context)
就能夠得到 context
,而在以前的版本中須要像這樣才能獲取 <Consumer>({vlaue} => {})</Consumer>
,這極大的簡化了代碼的書寫。
先來看看官網給出的用法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
複製代碼
根據官網的解釋和這個用法能夠看出,在 a
和 b
的變量值不變的狀況下,memoizedValue
的值不變。便是:useMemo
函數的第一個入參函數不會被執行,從而達到節省計算量的目的(有點像vue
的計算屬性)。那它有什麼用呢?一般來講能夠用做性能優化的手段。咱們來看一個例子:
// 父組件
import React, { useState } from 'react'
import { Input } from 'antd'
import Son1 from './son1'
interface Iprops {}
const Home: React.FC<Iprops> = () => {
const [info, setInfo] = useState('')
const [visible, setVisible] = useState(true)
const onVisible = () => {
setVisible((visible) => !visible)
}
const changeInfo = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setInfo(value)
}
return (
<div style={{ marginTop: '5px', marginLeft: '400px', marginRight: '400px' }}>
<p>{info}</p>
<Input onChange={(e) => changeInfo(e)}></Input>
<Son1 onVisible={onVisible} />
</div>
)
}
export default Home
// 子組件
import React from 'react'
import { Button } from 'antd'
interface Iprops {
onVisible: () => void
}
const Son1: React.FC<Iprops> = ({ onVisible }) => {
console.log('我被從新渲染了....')
return (
<div>
<Button onClick={() => onVisible()}>button</Button>
</div>
)
}
export default Son1
複製代碼
在父組件中,有個Input
輸入框,每次輸入新的值,父組件的info
的值就會發生改變,同時咱們發現子組件每次都會從新渲染,即便咱們子組件沒用到info
的值,那是由於setInfo
致使父組件從新渲染了,也致使onVisible
每次都變成一個新的值,因此引發子組件從新渲染。那麼有的同窗就會說,能夠利用React.memo
,咱們來試一試:
import React, { memo } from 'react'
import { Button } from 'antd'
interface Iprops {
onVisible: () => void
}
const Son1: React.FC<Iprops> = ({ onVisible }) => {
console.log('我被從新渲染了....')
return (
<div>
<Button onClick={() => onVisible()}>button</Button>
</div>
)
}
export default memo(Son1)
複製代碼
而後咱們隨便在輸入框輸入新的值,咱們發現,子組件仍然會從新渲染,爲何呢?那是由於這裏的props.onVisible
是一個函數,它是一個引用類型的值,當父組件從新渲染onVisible
這個函數也會從新生成,這樣引用地址變化就致使對比出新的數據,子組件就會從新渲染。因此咱們須要緩存onVisible
這個函數,便是:咱們只須要建立一遍這個函數,之後父組件從新渲染的時候,onVisible
的值仍然是第一次渲染的值,這樣子組件纔不會從新渲染。這個時候咱們就用到了useMemo
:
import React, { useState } from 'react'
import { Input } from 'antd'
import Son1 from './son1'
interface Iprops {}
const Home: React.FC<Iprops> = () => {
const [info, setInfo] = useState('')
const [visible, setVisible] = useState(true)
const onVisible = useMemo(() => {
return () => {
setVisible((visible) => !visible)
}
}, [])
const changeInfo = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setInfo(value)
}
return (
<div style={{ marginTop: '5px', marginLeft: '400px', marginRight: '400px' }}>
<p>{info}</p>
<Input onChange={(e) => changeInfo(e)}></Input>
<Son1 onVisible={onVisible} />
</div>
)
}
export default Home
複製代碼
能夠看到,咱們利用useMemo
將onVisible
緩存起來了,咱們在useMemo
的第二個參數傳入了一個[]
,代表它只會在渲染時執行一次,這裏的用法跟useEffect
同樣,[]
傳入依賴項,當依賴項改變時,咱們緩存的值纔會從新計算。再次在輸入框輸入新的值,咱們發現子組件不渲染了。
useMemo
通常用於計算比較複雜的場景
若是掌握了useMemo
,那掌握 useCallback
簡直不在話下。咱們先來看看定義:
const memoizedCallback = useCallback(() => {
doSomething(a, b)
}, [a, b])
複製代碼
在 a
和b
的變量值不變的狀況下,memoizedCallback
的引用不變。即:useCallback
的第一個入參函數會被緩存,從而達到渲染性能優化的目的。是否是跟useMemo
很像?useMemo
是緩存值,useCallback
一個是緩存函數的引用。也就是說 useCallback(fn, [deps])
至關於 useMemo(() => fn, [deps])
。咱們如今用 useCallback
來改造一下剛剛上面 👆 那個例子:
....
const Home: React.FC<Iprops> = () => {
const [info, setInfo] = useState('')
const [visible, setVisible] = useState(true)
// const onVisible = useMemo(() => {
// return () => {
// setVisible((visible) => !visible)
// }
// }, [])
const onVisible = useCallback(() => {
setVisible(visible => !visible)
}, [])
const changeInfo = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setInfo(value)
}
return (
<div style={{ marginTop: '5px', marginLeft: '400px', marginRight: '400px' }}>
<p>{info}</p>
<Input onChange={(e) => changeInfo(e)}></Input>
<Son1 onVisible={onVisible} />
</div>
)
}
export default Home
複製代碼
我相信你確定已經看懂了,什麼?沒看懂?那再看一遍!
藉助於react
提供的基礎hook
,咱們一般也能夠自定義hook
,react
規定咱們自定義hook
時,必須以use
開頭。咱們來嘗試自定義一個控制對話框的hook
:
import { useState } from 'react'
type returnd = [boolean, (visible?: boolean) => void]
const useVisible = (initVisible = false): returnd => {
const [visible, setVisible] = useState(initVisible)
function onVisible(value?: boolean) {
const newValue = value === undefined ? !visible : value
setVisible(newValue)
}
return [visible, onVisible]
}
export default useVisible
複製代碼
首先咱們利用useState
聲明瞭visible
和setVisible
,而後咱們定義了onVisible
這個函數用來更改visible
,接着咱們返回[visible, onVisible]
。而後咱們來看看如何使用:
import { Button, Modal } from 'antd'
import useVisible from '../hooks/useVisible'
const Home: React.FC = () => {
const [visible, setVisible] = useVisible(false)
const modalShow = (value: boolean) => {
setVisible(value)
}
return (
<div style={{ marginTop: '5px', marginLeft: '400px', marginRight: '400px' }}>
<Button type='primary' onClick={() => modalShow(true)}>
Open Modal
</Button>
<Modal title='Basic Modal' visible={visible} onOk={() => modalShow(false)} onCancel={() => modalShow(false)}>
<p>Some contents...</p>
<p>Some contents...</p>
<p>Some contents...</p>
</Modal>
</div>
)
}
export default Home
複製代碼
就像咱們使用其餘hook
同樣方便。咱們在寫業務(搬磚)的過程當中,咱們能夠嘗試去將一些可複用的邏輯或者操做封裝爲咱們本身的hook
,這纔是hooks
的強大之處。
咱們在開發react
的時候,難免須要一些配置,例如別名、跨域等等。vue
給咱們提供了一個vue.config.js
用於配置,那麼react
項目呢?咱們須要用到react-app-rewired
和customize-cra
:
yarn add react-app-rewired -D
yarn add customize-cra -D
複製代碼
安裝完以後,咱們須要更改一下咱們的package.json
文件:
....
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test --env=jsdom",
"eject": "react-scripts eject"
},
....
複製代碼
接着咱們須要在項目的根目錄新建一個config-overrides.js
文件。接下來咱們對項目進行一些配置:
/* config-overrides.js */
const path = require('path')
const { override, addWebpackResolve, fixBabelImports, overrideDevServer } = require('customize-cra')
const { addReactRefresh } = require('customize-cra-react-refresh')
// 配置開發環境跨域
const devServerConfig = () => (config) => {
return {
...config,
port: 3000,
proxy: {
'/mock/158/airi': {
target: 'https://api.guaik.org',
changeOrigin: true,
secure: false,
},
},
}
}
module.exports = {
webpack: override(
// 熱加載
addReactRefresh(),
// 配置路徑別名
addWebpackResolve({
alias: {
'@': path.resolve(__dirname, 'src'),
},
}),
// antd 按需加載
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
})
),
devServer: overrideDevServer(devServerConfig()),
}
複製代碼
更多的配置請查看這裏:customize-cra
react
的知識點真的不少,對不?本文只是一個入門概覽,還有不少不少的知識點,但願小夥伴們經過這篇文章對 react
有個大體瞭解,這篇文章也能夠做爲面試複習基礎知識的筆記,筆者花了兩週時間,整理了這樣一篇react
入門的文章,寫到這裏已經凌晨 1 點,若是你以爲這篇文章對你有所幫助,那就點個贊吧 ❤️