2020了,還不開始學react嗎?| react 入門必知必會知識點(萬字總結✍)

title.png

筆者從去年 12 月開始接觸react,逐步由懵逼走向熟悉。不過react的須要掌握的知識點可真的有點多呢。因此花了很長一段時間來整理這樣一篇react的基礎知識總結的文章,以此達到溫故知新的目的。文章會涉及到react自己的基礎知識(包括組件通信、生命週期、路由管理、狀態管理等方面),相信你認認真真看完這篇文章之後,你會對react開發有個大體的瞭解,而且可以快速入門。這篇文章也可用做面試複習 react 基礎,而且這篇文章會持續更新,小夥伴們能夠點個收藏,防止迷路。廢話很少說,so ,Let's go!!!css

組件通訊

props

適用於父子組件通訊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
複製代碼

Context

適用於跨層級組件之間通訊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

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

refreact提供給咱們的一個屬性,經過它,咱們能夠訪問 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 庫等。

第三方工具

events(發佈訂閱)

這種方式適用於沒有任何嵌套關係的組件通訊。其原理就是使用事件訂閱。便是一個發佈者,一個或者多個訂閱者。 使用以前須要先安裝:

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-routerreact-router-domreact-router-native

react-router提供最基本的路由功能,可是實際使用的時候咱們不會直接安裝 react-router,而是根據應用運行的環境來選擇安裝 react-router-domreact-router-native。由於react-router-domreact-router-native 都依賴 react-router,因此在安裝時,react-router 也會⾃自動安裝。

其中react-router-dom 在瀏覽器器中使⽤,而react-router-nativereact-native 中使用。

在 react-router 裏面,一共有 3 種基礎組件,他們分別是

  • 路由組件(router components) 好比 <BrowserRouter><HashRouter>
  • 路由匹配組件(route matchers components) 好比 <Route><Switch>
  • 導航組件(navigation components) 好比 <Link>, <NavLink>, 和 <Redirect>

路由組件

對於 web 項目,react-roruter-dom 提供了 <BrowserRouter><HashRouter>兩個路由組件。

  • BrowserRouter:瀏覽器的路由方式,也就是使用 HTML5 提供的 history API ( pushState , replaceState 和 popstate 事件) 來保持 UIurl 的同步。這種方式在react開發中是常用的路由方式,可是在打包後,打開會發現訪問不了頁面,因此須要經過配置 nginx 解決或者後臺配置代理。
  • HashRouter:在路徑前加入#號成爲一個哈希值,Hash 模式的好處是,不再會由於咱們刷新而找不到咱們的對應路徑,可是連接上面會有#/。在vue開發中,常用這種方式。

要使用路由組件,咱們只須要確保它是在根組件使用,咱們應該將<App />包裹在路由組件下面:

import { BrowserRouter } from 'react-router-dom';
...
<BrowserRouter>
    <App />
</BrowserRouter>
...
複製代碼

匹配組件

有兩種路由匹配組件:<Route><Switch>

這兩個路由匹配組件一般在一塊兒使用,在<Switch>裏面包裹多個<Route>,而後它會逐步去比對每一個<Route>path屬性 和瀏覽器當前locationpathname是否一致,若是一致則返回內容,不然返回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>的最後一個位置
  • 另外一種解決方案是添加'exact' 實現精確匹配: <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

redux

咱們都知道,react 是單向的數據流,數據幾乎都是經過 props 依次從上往下傳:

react-porps.gif

圖片來自 When do I know I’m ready for Redux?

一個組件的狀態有兩種方式改變:

  • 來自父組件的 props 改變了,那麼這個組件也會從新渲染
  • 自身有 state,自身的 state 能夠經過this.setstate方法改變

如今假如咱們組件樹的層級比較深,有不少子組件須要共享狀態,那麼咱們只能經過狀態提高來改變狀態,將狀態提高到頂級父組件改變,當頂級父組件的狀態改變了,那麼旗下的全部子節點都會從新渲染:

state-change.gif

當出現這種狀況當時候,你就該使用redux了。那麼使用redux以後,就會變成這樣:

redux-state.gif

以上 gif 動圖很生動的展現了 redux 解決的問題,下面咱們來介紹一下 redux 相關的知識點:

Store

在 redux 裏面,只有一個Store,整個應用須要管理的數據都在這個Store裏面。這個Store咱們不能直接去改變,咱們只能經過返回一個新的Store去更改它。redux提供了一個createStore來建立state

import { createStore } from 'redux'
const store = createStore(reducer)
複製代碼

action

這個 action 指的是視圖層發起的一個操做,告訴Store 咱們須要改變。好比用戶點擊了按鈕,咱們就要去請求列表,列表的數據就會變動。每一個 action 必須有一個 type 屬性,這表示 action 的名稱,而後還能夠有一個 payload 屬性,這個屬性能夠帶一些參數,用做 Store 變動:

const action = {
  type: 'ADD_ITEM',
  payload: 'new item', // 可選屬性
}
複製代碼

上面這個例子就定義了一個名爲ADD_ITEMAction,它還攜帶了一個payload的參數。 Redux 能夠用 Action Creator 批量來生成一些 Action

reducer

在上面咱們定義了一個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,咱們爲何要引入中間件呢?到目前爲止,咱們來捋一下咱們剛剛已經進行的步驟:

  1. 建立 store
import { createStore } from 'redux'
const store = createStore(reducer)
複製代碼
  1. 發出 action
store.dispatch({
  type: 'ADD_ITEM',
  payload: 'new item', // 可選屬性
})
複製代碼
  1. reducer 計算返回新的 state
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-thunxredux-promiseredux-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-reduxredux

爲了解決上述的痛點問題,更好的將 reduxreact 結合,官方給咱們提供了react-redux這個包(可能你到如今腦子有點亂了,我剛開始也是)。那麼如今,咱們須要明確一個概念:reduxreact 是兩個八竿子不着的人。redux 只是一個狀態管理框架,react 只是一個前端應用框架。redux 能夠用於前端任何框架,例如 vue,甚至純 javaScript 均可以。後來 react-redux 出現了,他把 reduxreact 撮合在一塊兒了,因而他們兩強強聯合,風雲合璧,所向披靡,好了不扯了。說了這麼多就是想說明 react-redux 這個包的做用。

在詳細說明react-redux的做用以前,咱們先介紹如下知識點: react-redux將 react 組件劃分爲容器組件展現組件,其中

  • 展現組件:只是負責展現 UI,不涉及到邏輯的處理,數據來自父組件的props;
  • 容器組件:負責邏輯、數據交互,將 state 裏面的數據傳遞給展現組件進行 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 組件原理就是經過reactContext來實現的,咱們能夠看看源碼:

....
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方法。這個方法能夠傳入兩個可選參數:mapStateToPropsmapDispatchToProps,而後會返回一個容器組件,這個組件能夠自動監聽到 state 的變動,將 state 的值映射爲組件的 props 參數,以後咱們能夠直接經過 this.props 取到 state 裏面的值。

const mapStateToProps = (state) => ({
  goodsList: state.goodsList,
  totalCount: state.totalCount,
});

export default connect(
  mapStateToProps, // 可選
// mapDispatchToProps, // 可選
(GoodsList);
複製代碼

mapStateToProps就是將 state 的值映射爲組件的propsmapDispatchToProps就是將store.dispatch映射爲props。若是咱們不傳mapDispatchToProps的話,connect會自動將 dispatch 注入到 props 裏面,咱們在組件裏能夠直接經過this.props.dispatch發起一個actionreducer

connected-react-routerredux

當咱們在項目中同時用了react-routerredux的時候,咱們能夠把他們兩個深度整合。咱們想要在store裏面拿到router,甚至能夠操做router,還能夠記錄router的改變。例如咱們把用戶是否登陸的狀態存在redux裏面,在登陸以後會進行頁面的跳轉。正常的操做是咱們在發起請求以後,獲得一個狀態,此時咱們須要dispatch一個action去改變redux的狀態,同時咱們須要進行路由的跳轉,相似於這樣:store.dispatch(replace('/home'))。想要實現這種深度整合,咱們須要用到 connected-react-routerhistory兩個庫。

首先須要history生成history對象,結合connected-react-routerconnectRouter生成routerReducer,同時利用connected-react-routerrouterMiddleware實現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]
複製代碼

接着利用reduxcombineReducers將咱們本身的reducerrouterReducer合併起來,組成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-routerConnectedRouter組件做爲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-routerredux整合起來了。如今當咱們在View利用Link進行路由跳轉的時候,會經過react-router-dom進行路由跳轉,而且也會經過connected-react-router發起一個action去更新redux state裏面的router對象,以記錄路由的變化。同時如今咱們在狀態管理的時候,也能夠直接經過connected-react-router提供的pushreplace等方法了,他們是從 redux 出發,也就是說先發起一個action,而後再進行路由跳轉。

小結一下

看了上面的這些東西,是否是感受腦子賊亂,什麼reactreduxreact-reduxreact-routerreact-router-domconnected-react-router,這些概念是真的多,我剛開始接觸的時候直接看懵逼。。因此 react 的可組合性比 vue 高不少,因此說 vue 真的是自動擋、react 是手動擋。可是咱們只需記住,前端的一切概念,都是紙老虎而已。靜下心來捋一捋,很快就理解了。好了,如今來看看這個圖:

react.png

結合上面介紹的知識點,把思路捋順,我們再繼續 💪

dva

經過上面的這些工具,咱們已經能夠搭建一個很棒的基於reactredux的工程。若是單純使用redux,當項目愈來愈大的時候,咱們的redux的操做也會變得繁雜,代碼和文件的組織會變得臃腫,咱們就必須把actionreducercreateActionsactionType等等文件放在不一樣的目錄下面,當咱們須要使用的時候,就要打開文件目錄去翻半天,咱們須要在這些文件裏面不停切換,簡直抓狂。因此咱們不由會想:要是全部的操做都放在一個文件該多好!沒錯,它來了。它就是dva

dva 首先是一個基於 redux 和 redux-saga 的數據流方案,而後爲了簡化開發體驗,dva 還額外內置了 react-router 和 fetch,因此也能夠理解爲一個輕量級的應用框架。

由於咱們前面已經本身組織了react-router,因此咱們只使用dva-core,有了這個,咱們就能夠將 reducers, effects 等等組織在一個model文件裏面了。之前咱們組織代碼的方式是createAction.tsactionType.tsreducer/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這個概念就是跟reduxreducer是同一個意思(返回一個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-reduxconnect來獲取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被修改了,同時頁面也刷新了:

model.gif

除了put,還有call(用於發起異步請求)、select(用於取出 state 裏面的值),都是redux-saga提供的,這裏不過多敘述,具體使用的方法請查閱redux-saga文檔。

我這裏整理了兩個圖,對比reduxdva的流程操做:

redux.png

dva.png

簡單總結一下:dvaaction -> reducer 拆分紅了,action -> model(reducer, effect)咱們在寫業務的時候,不再用處處去找咱們的reduceraction了。dva真的給咱們提供了極大的便利。

生命週期(新舊對比)

舊版生命週期

舊版生命週期 指的是 React 16.3 及其以前的版本。

image

請看下面兩段代碼

// 父組件
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 父子組件的渲染順序遵循洋蔥模型

新版生命週期

react-16.4.png

static getDerivedStateFromProps

  • static getDerivedStateFromProps(nextProps,prevState):接收父組件傳遞過來的 props 和組件以前的狀態,返回一個對象來更新 state 或者返回 null 來表示接收到的 props 沒有變化,不須要更新 state.
  • 該生命週期鉤子的做用: 將父組件傳遞過來的 props 映射 到子組件的 state 上面,這樣組件內部就不用再經過 this.props.xxx 獲取屬性值了,統一經過 this.state.xxx 獲取。映射就至關於拷貝了一份父組件傳過來的 props ,做爲子組件本身的狀態。注意:子組件經過 setState 更新自身狀態時,不會改變父組件的 props
  • 配合 componentDidUpdate,能夠覆蓋 componentWillReceiveProps 的全部用法
  • 該生命週期鉤子觸發的時機:
  1. 在 React 16.3.0 版本中:在組件實例化、接收到新的 props 時會被調用
  2. 在 React 16.4.0 版本中:在組件實例化、接收到新的 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

  • getSnapshotBeforeUpdate(prevProps, prevState):接收父組件傳遞過來的 props 和組件以前的狀態,今生命週期鉤子必須有返回值,返回值將做爲第三個參數傳遞給 componentDidUpdate。必須和 componentDidUpdate 一塊兒使用,不然會報錯。
  • 該生命週期鉤子觸發的時機 :被調用於 render 以後、更新 DOMrefs 以前
  • 該生命週期鉤子的做用: 它能讓你在組件更新 DOMrefs 以前,從 DOM 中捕獲一些信息(例如滾動位置)
  • 配合 componentDidUpdate, 能夠覆蓋 componentWillUpdate 的全部用法
  • demo:每次組件更新時,都去獲取以前的滾動位置,讓組件保持在以前的滾動位置
getSnapshotBeforeUpdate() {
    // 返回更新內容的高度
    return this.wrapper.current.scrollHeight;
  }
componentDidUpdate(prevProps, prevState, prevScrollHeight) {// 在這裏拿到高度
    this.wrapper.current.scrollTop =
      this.wrapper.current.scrollTop +
      (this.wrapper.current.scrollHeight - prevScrollHeight);
  }
複製代碼

版本遷移

  • componentWillMountcomponentWillReceivePropscomponentWillUpdate 這三個生命週期由於常常會被誤解和濫用,因此被稱爲 不安全(不是指安全性,而是表示使用這些生命週期的代碼,有可能在將來的 React 版本中存在缺陷,可能會影響將來的異步渲染) 的生命週期。
  • React 16.3 版本:爲不安全的生命週期引入別名 UNSAFE_componentWillMountUNSAFE_componentWillReceivePropsUNSAFE_componentWillUpdate。(舊的生命週期名稱和新的別名均可以在此版本中使用)
  • React 16.3 以後的版本:爲 componentWillMountcomponentWillReceivePropscomponentWillUpdate 啓用棄用警告。(舊的生命週期名稱和新的別名均可以在此版本中使用,但舊名稱會記錄 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
複製代碼

上面這個例子,父組件提供了兩個值:info1info2,其中 Son1 組件只用到了 info1Son2 組件只用到了 info2。咱們在父組件中,點擊了按鈕改變了 info1 的值,父組件必須從新渲染,由於它自身狀態改變了,Son1 也應該從新渲染,由於它依賴於 info1,而 Son2 是否應該從新渲染呢?按道理,它不該該從新渲染,由於 info2 沒有改變,可是當咱們每次點擊按鈕改變 info1 的時候,Son1Son2 都從新渲染了,這就明顯存在問題了。

shouldComponentUpdate

在上面 👆 生命週期章節,咱們講到了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就不會再從新渲染了。

PureComponet

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>
    )
  }
複製代碼

Memo

剛剛咱們介紹了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 哪去了?別急呢,立刻就來 👇

hooks

隨着react 16.8版本的出現,hooks也問世了。hooks解決了class組件飽受詬病的衆多問題,好比綁定this的問題、組件的邏輯複用的問題等等。其實起初我對hooks並不十分感冒,由於那個時候筆者連class寫法都還沒掌握,再學個這玩意簡直徒增煩惱。後來沒辦法,團隊裏的小夥伴都開始使用hooks,因此我也被動學習了一波。這不寫不知道,一寫就真香!!!因此趕忙學起來吧

useState

記住一個點,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

useEffect 其實只比複雜了那麼一點。它合成了 calss 組件中的componentDidMountcomponentDidUpdatecomponentWillUnmount。 咱們很容易就明白,它是用來執行反作用的,最最多見的反作用就是異步請求。

下面咱們用它來實現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` })
  }, [])
....

複製代碼

useRef

這個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

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> ,這極大的簡化了代碼的書寫。

useMemo

先來看看官網給出的用法:

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
複製代碼

根據官網的解釋和這個用法能夠看出,在 ab 的變量值不變的狀況下,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
複製代碼

能夠看到,咱們利用useMemoonVisible緩存起來了,咱們在useMemo的第二個參數傳入了一個[],代表它只會在渲染時執行一次,這裏的用法跟useEffect同樣,[]傳入依賴項,當依賴項改變時,咱們緩存的值纔會從新計算。再次在輸入框輸入新的值,咱們發現子組件不渲染了。

useMemo 通常用於計算比較複雜的場景

useCallback

若是掌握了useMemo,那掌握 useCallback簡直不在話下。咱們先來看看定義:

const memoizedCallback = useCallback(() => {
  doSomething(a, b)
}, [a, b])
複製代碼

ab 的變量值不變的狀況下,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
複製代碼

我相信你確定已經看懂了,什麼?沒看懂?那再看一遍!

自定義 hook

藉助於react提供的基礎hook,咱們一般也能夠自定義hookreact規定咱們自定義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聲明瞭visiblesetVisible,而後咱們定義了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-rewiredcustomize-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 點,若是你以爲這篇文章對你有所幫助,那就點個贊吧 ❤️

參考

一、medium.com/dailyjs/whe…

二、juejin.im/post/5c18de…

三、juejin.im/post/5dcaaa…

四、juejin.im/post/5dbbdb…

五、github.com/monsterooo/…

六、github.com/monsterooo/…

相關文章
相關標籤/搜索