React入门指南: 6张脑图带你入门React

1,601 阅读22分钟

马上到了又一年的秋招, 还不会React? 没关系! 我这里有最精简的入门指南😄

通过6张脑图带你入门React, React文档相对于刚入门的新人来说, 其跳跃性强, 案例的综合性之高让人读起来都费劲. 为此我整理了一份脑图+超简单的Demo的笔记, 让你迅速明白ReactAPI的作用, 具体的Demo地址👉react-study-guide, 欢迎star⭐。 所有的脑图, Demo都在仓库里可以直接获取

并且在许多演示片段, 我都录制了GIF图, 让你更直观的体会到代码的实现与Demo的样式


总览

放心看起来虽然多, 但是内容都不复杂, 每个分支我都配备了对应的讲解

React.png

JSX语法

最重要的一点, 大括号 {} 内写的是JavaScript的语法

image.png

  • 这个Demo就展示了React中JSX语法需要注意的地方, 仅需注意脑图提到的规则即可(完全够用了)
<div id="test"></div>
<script type="text/babel">
  let data = ['a', 'b', 'c']
  // 1.创建虚拟DOM
  const VDom = (
    <div> {/* 必须只有一个根标签 */}
      <h1 style={{background: 'skyblue', marginLeft: '20px'}}>这是大标题</h1>
      <ul className='ul-class-name'>
        { {/* JS语法 */}
          data.map((item, index) => {
            return <li  key={index}>{item}</li>
          })
        }
      </ul>
    </div>
  )
  ReactDOM.render(VDom, document.querySelector('#test'))
</script>

React组件化

组件化是React最核心的内容, 我会将脑图拆分成一个部分一个部分来看

  • 如果你不理解组件化这个词, 没关系继续往下看, 稍后你就会慢慢明白了

函数式与类式组件

函数式组件是目前使用最多, 并且是最受欢迎得用法, 但是两者并非谁更加优秀, 用法看个人即可。 image.png

函数式

使用简单,没有烦人的this指向, 普遍被人接受, 不过无法使用类式组件中的部分功能State生命周期函数等(这些后续会做介绍), 但可以用过hook解决这一问题。

function Demo() {
    return <h1>Functional Component</h1>
}
ReactDOM.render(<Demo />, document.querySelector('#test'))

类式

书写相对复杂, 要求继承一个通过React暴露的Component类, 但是可以直接使用生命周期函数, state状态

class Demo extends React.Component {
    render() {
      return <h1>Class Component</h1>
    }
}
ReactDOM.render(<Demo />, document.querySelector('#test'))

事件对象

就是各种事件(点击等), 使用起来除了书写差异, 其实跟原生没有太大区别。

image.png

书写规范

使用 小驼峰 的形式,取代传统的写法即可

  • 传统写法
<a herf="#" onclick="console.log('ok!')"></a>
  • React事件写法
<a herf="#" onClick={clickFun}></a> 

function clickFun () { 
    console.log('ok!') 
}
  • React阻止事件默认行为
<a herf="#" onClick={clickFun}></a> 

function clickFun (e) { 
    e.preventDefault() 
    console.log('ok!') 
}

this的指向问题

  • 通常情况下由于onClick执行与函数并不同步, 所以this会指向undefined, 需要在实例内改变this指向
class Demo extends React.Component {
  constructor(props) {
    super(props)
    this.switchFn = this.switchFn.bind(this) // 👈 在构造器内部
                                             // 将函数的this绑定为当前实例
  }
  switchFn() {
    console.log('点击成功')
  }
  render(){
    return (
      <div onClick={this.switchFn}>
        这里是事件点击处
      </div>
    )
  }
} 
ReactDOM.render(<Demo />, document.querySelector('#test'))
  • 通过箭头函数的形式来解决, 如果你想了解为什么, 建议你复习一下箭头函数的特性
class Demo extends React.Component {
  constructor(props) {
    super(props)
  }
  switchFn = () => { // 👈
    console.log('点击成功') // 将函数改为箭头函数形式
  }
  render(){
    return (
      <div onClick={this.switchFn}>
        这里是事件点击处
      </div>
    )
  }
} 
ReactDOM.render(<Demo />, document.querySelector('#test'))

处理器的参数传递

  • 通过bind解决
<button click={this.switch.bind(this, id)}></button>
  • 写成回调函数的形式
 <button click={(e) => {this.switch(id, e)}}></button>

讲完三大属性,再来讲讲React的条件渲染与循环

组件的三大属性

了解了三大属性, 我们再来看一个完整的React组件应该长什么样

image.png

state(状态)

组件本身的状态,是一个挂载在当前组件实例上的一个属性

image.png

this.setState(): 有两种使用方式:

image.png

  • 对象式
// this.setState((state, props)=>{}, callback)

class Demo extends React.Component {
    state = { 
        name: 'link' 
    }
    switchFn = () => {
        this.setState({   // 👈
            name: 'kiki'  
        })
    }
    render(){
      return (
        <div onClick={this.switchFn}>
          name: {this.state.name}
        </div>
      )
    }
} 

  • 函数式
// this.setState((state, props)=>{}, callback)

class Demo extends React.Component {
    state = { 
        name: 'link' 
    }
    switchFn = () => {
        this.setState((state, prop) => { // 👈
            // 可以直接拿到state与prop
            name: 'kiki'
        })
    }
    render(){
      return (
        <div onClick={this.switchFn}>
          name: {this.state.name}
        </div>
      )
    }
} 

  • 由于setState的执行是异步的, 在setState()后如果还需要做一些处理, 这些处理就需要在callbak内了
let name = 'link'

this.setState({name: 'kiki'})

console.log(this.state.name) // 此时还会得到 'link'
  • 正确操作
state = {
    name: 'link'
}
this.setState({name: 'kiki'}, () => {  // 👈
    console.log(this.state.name) // 此时得到 'kiki'
})

: 以上均是类式组件的用法, 在函数式组件中, 使用 State 需要通过 hook 实现, 到hook章节我们再来阐述这一点

prop(属性 property)

props也是实例上挂载的一个属性,所有通过标签属性传递的值,都可以由它访问到,包括函数, 对象。(组件通信), 也有一些特殊值无法被访问, 如做唯一表示的key

image.png

// 根据JSX语法 假设这是一个组件

let name = 'link'

<TestComponent name={name} /> // 这个name就是一个prop(属性)

// TestComponent组件内部
class TestComponent extends React.Component {
   render() {
       <p>name: {this.props.name}</p> // 👈 {}大括号内可以写js语法
   }
}

ref(引用 reference)

获取到当前的一个DOM元素

image.png

  • 类似于document.getElementById('id') 只是这件事由React帮你做了

  • 使用方式

注意: ref的使用方式有三种, 但目前为止最被官方推荐的为第三种

image.png

字符串形式❌(不推荐

// 这是一个原生的DOM
export default class index extends Component {
  handleClick = () => {
      console.log(this.refs.index);  // 拿到这个实例
  }
  render() {
      return (
          <div className='index' ref='index'> // 👈
              <h1 ref='index2' onClick={this.handleClick}>index</h1>
          </div>
      )
  }
}

ref回调形式

回调函数的形式, 会把DOM挂载到实例上(this)

export default class index extends Component {
  handleClick = () => {
      console.log(this); 
  }
  render() {
      return (
          <div className='index' ref={c => this.input1 = c}>  // 👈
              <h1 onClick={this.handleClick}>index</h1>
          </div>
      )
  }
}

createRef

React基于craetRef创建的ref只能存放一个ref, 这种形式的官方最为推荐, 但是书写相对比较麻烦

  • creatRef() 即在每次使用的时候需要自己手动创建一个ref, 并且只能专人专用
export default class index extends Component {
  headerRef = React.createRef()  // 👈
  divRef = React.createRef()
  
  handleClick = () => {
      console.log(this.headerRef);  // 👈
      console.log(this.divRef);
  }
  render() {
      return (
          <div ref={this.divRef} className='index'>  // 👈
              <h1 ref={this.headerRef}onClick={this.handleClick}>index</h1>
          </div>
      )
  }
}

综合案例

结合事件对象与三大属性, 我们来看一个条件渲染的案例

  • 本案例在项目中的路径地址: 'react-study-guide\study-demo\test-Demo\React-组件化\事件对象\条件渲染.html'

  • 组件关系

请对照这个流程表理清关系

stateDiagram-v2
Demo(主组件) --> LogoutBtn: isLogin === true
Demo(主组件) --> Greeting
Demo(主组件) --> LoginBtn: isLogin === false
Greeting --> Welcome: isLogin === true
Greeting --> Bye: isLogin === false
  • 效果

动画.gif

也就是说, 在全局主组件中有一个isLogin(state)在管控全局状态.根据是否登录,我们来判断展示什么样的组件

  • Demo组件
class Demo extends React.Component{
  state = {
    isLogin: true
  }
  render(){
    let button
    const { isLogin } = this.state
    if(isLogin) {
      // 将点击事件函数作为props传入组件
      button = <LogoutBtn onLogoutClick={this.logout}/> 
    } else {
      button = <LoginBtn onLoginClick={this.login}/>
    }
    return (
      <div>
        <Greeting isLogin={this.state.isLogin}/>
        {button}  {/* 根据state中的islogin判断展示logoutBtn还是LoginBtn */}
      </div>
    )
  }
  // 控制事件
  login = () => {
    this.setState({isLogin: true})
  }
  logout = () =>{
    this.setState({isLogin: false})
  }
  
}
  • Greeting 组件(UI组件)

它包含了两个UI子组件, 根据isLogin的状态我们来判断展示那个UI组件, 请思考一下主组件Demo是怎么把isLogin的状态传递到UI组件Greeting中的?

function Greeting(props){
  const isLogin = props.isLogin
  if(isLogin) return (
    <Welcome />
  )
  return (
    <Bye />
  )
}
  • UI子组件
// UI
function Welcome(){
    return <h1>Welcome</h1>
}
function Bye(){
    return <h1>Bye</h1>
}

组件化思想

想想上面的组件之间的关系以及功能, 其实就是每个组件在负责他们自己的功能, 按钮组件负责登录退出的事件, UI组件负责展示标语, welcome与bye.

  • 在实际开发中也是同理. 你可以将你认为合理的一部分看成一个组件, 以此来提高它的复用性. 组件化开发条例更加清晰, 也能有效降低耦合度.

以React的官网为例:

image.png

  • 我们就可以把头部的红色部分看成一个头部组件.
  • 底部的橙色看成一个组件, 我们仅需写一个橙色组件, 然后将标题以及内容, 作为prop传递进去即可.

那么具体怎么做?

// 数据
let arr = [
    {
      title:'link',
      content: 'ok, 我要说点什么'
    },
    {
      title:'掘金',
      content: 'ok, 发文章'
    },
    {
      title:'公众号',
      content: 'ok, 偷个懒'
    },
]
// 通过prop传入Demo组件 
ReactDOM.render(<Demo number={arr} />, document.querySelector('#test'))  // 👈

class Demo extends React.Component {
  state = {
    arrInfo: this.props.number
  }
  // 根据arr的数据做循环, 将这个组件存到一个数组里
  itemList = this.props.number.map((item, index) => {
    return <ItemList item={item} key={index}/>
  })
  render() {
    return <ul>{this.itemList}</ul>
  }
}
// UI组件, 在上方我们循环了这个组件
function ItemList (props) {
  let item = props.item
  return (
    <li>
      <div>title: {item.title}</div>  
      <div>content: {item.content}</div>  
    </li>
  )
}

一个橙色的框框就是我们一个"信息"组件. 这样我们就复用了这个组件, 而不是自己去复制三个. 当然组件化远远比我形容的要强大, 剩下的就靠你的创造力了. image.png

注: 在循环处我加入了一个key属性, 他的作用是给每一个组件绑定一个唯一的标识, 这有助于react识别组件.但是该标识并不是要求在全局下唯一,而是在兄弟节点之间,也就是非兄弟节点(不同组件),是可以使用一个数据中的id作为标识的. 目前只需要知道这一点就可以了. 有兴趣可以深入了解, 它的作用与原理比想象的复杂

组件通信

从这里开始, 将会详细的书写一个React组件, 并且这些Demo都在react-study-guide项目中有对应Demo, 如果看不明白, 可以clone一下项目自己调试一下, 并且强烈建议你自己手敲一下这些Demo

image.png

父子组件

根据上面的例子 父传子 应该非常显而易见了, 就是通过prop传入.

  • 子传父

同样根据prop, 父组件传入一个函数, 子组件内执行的时候传参给它

  class Father extends React.Component {
    constructor(props) {
      super(props)
      this.state = {
        info: '还没消息...'
      }
    }
    getInfo = item => {
      this.setState({
        info: item
      })
    }
    render(){
      return (
        <div>
          <p>info: {this.state.info}</p>
          <Clild getChildInfo={this.getInfo}/>
        </div>
      )
    }
  } 
  • 子组件
// 为了让你习惯这两种组件创建方式, 我将会任意切换, 因为两者切换使用并无语法层面的问题
function Clild (props) {
  function emitInfo() {
    props.getChildInfo('我是子组件发送过去的数据') // 👈
  }
  return (
    <button onClick={emitInfo}>点击发送消息</button>
  )
}
ReactDOM.render(<Father />, document.querySelector('#test'))

兄弟组件

  • 状态提升: 原本两个组件各自管理着自己的state, 例如它们都有一个属性叫做title, 为了他们两个共同使用同一个 title state, 我们可以把 title 提升到父组件, 然后通过prop传入
classDiagram
Father <|-- ChildA
Father <|-- ChildB

class ChildA{
state: "title: 'child'"
}
class ChildB{
state: "title: 'child'"
}
  • 状态提升后
classDiagram
Father --|> ChildA: title={this.state.title}
Father --|> ChildB: title={this.state.title}
Father: title 'child'
class ChildA{
prop: "title: 'child'"
}
class ChildB{
prop: "title: 'child'"
}
  • 这里我仅提供思路, 因为它和上面那个案例的实现是一致的, 尝试自己实现一下吧!

非亲组件

image.png

  • redux 我们作为一个章节来讲, 这里只看pubsub-js

$ yarn add pubsub-js

pubsub

pubsub用的其实不多, 仅做了解, 需要的时候再使用就好了, 通常公共状态管理都是使用Redux, 而隔代的组件我们也可以通过 renderProp, 或者高阶组件来实现, 甚至可以通过context, 这些我们后续都会提及

  • 两个组件之间的关系( 可以没有任何关系)
export default class App extends Component {
    render() {
        return (
            <div>
                <Publish />
                <Subscribe />
            </div>
        )
    }
}
  • 发送组件
import React, { Component } from 'react'
import PubSub from 'pubsub-js'
export default class Publish extends Component {
    state = {
        value: '我是publish传给Subscribe的数据'
    }
    handleValue = (dataType, value) => {
        PubSub.publish('data of publish', this.state.value) // 数据发送 👈
    }
    render() {
        return (
            <div>
                <h1>Publish</h1> 
                <button onClick={this.handleValue}>点击发送数据</button>
            </div>
        )
    }
}
  • 接收组件
import React, { Component } from 'react'
import PubSub from 'pubsub-js'
export default class Subscribe extends Component {
    state = {
        receivedData: ''
    }
		// 订阅数据
    token = PubSub.subscribe('data of publish', (msg, data) => { // 👈
        this.setState({
            receivedData: data
        })
    })
    render() {
        return (
            <div>
                <h1>Subcribe: </h1>
                <div>{this.state.receivedData}</div>
            </div>
        )
    }
		// 卸载钩子, 记得清空接收器
    componentWillUnmount() {
        PubSub.unSubscribe(this.token)
    }
}

生命周期

生命周期Demo可以clone下来调试下面输出的例子,

新版

image.png

  • 页面初次渲染时

image.png

  • 数据更新时

image.png

旧版

旧版的生命周期在Demo中有演示, 这里就不做展示了

image.png

注意

即便父组件更新与子组件无任何关系, 也会导致子组件刷新, 可以通过继承PureComponent解决, 但是shouldComponentUpdate不可用

image.png

通过这张图可以看出, 尽管我传入的childName没有发生任何改变, 但是子组件还是发生了重新渲染

新旧生命周期对比

image.png

受控与非受控组件

原本表单元素是自己维护自己的值, 并且只能通过用户的输入进行值的修改. 而在React中可变状态的值只能保存于state中, 并且基于setState去修改. 两者结合, 使得用户输入的值保存至state中, 并在事件中基于setState去改变. 组件内部的state就成了唯一的数据源(值都保存于此). 基于这种形式控制的组件就成为 "受控组件"

看不懂? 看gif图

  • 上为受控组件, 下为非受控组件

受控与非受控.gif

发现区别在哪里没? 受控组件的数据会实时的渲染到页面上, 同步更新页面与数据。而非受控则不会.他们两者用的都是change事件, 受控组件用的是React做过修改的onChange, 而下方的是原生的change事件

  • 这就是受控与非受控的区别, 简单吧。当然这只是从视图层面解释, 请仔细看上方的定义

请思考一下, 如上方的受控组件, 我要求他们只能通过一个处理器函数去处理 event.target.value值,怎样才能保证两者的值不会覆盖到同一个state状态上?

可被创建为受控组件表

image.png


React-Router

React-Router.png

使用

$ yarn add react-router

  • 入口文件引入即可
import { Route,Link} from 'react-router-dom'

HashRouter与BrowserRouter

一般情况下, 与服务器做交互,使用的都是 BrowserRouter, 而使用静态文件的服务器的情况才用 HashRouter,并且这两者都要求包裹在使用路由的外部

image.png

  • 使用
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { BrowserRouter} from 'react-router-dom'

ReactDOM.render(
    <BrowserRouter> // 👈 包裹
        <App />
    </BrowserRouter>
    , document.getElementById('root'))

导航Link

导航就是用来进行跳转的交互按钮, 通过点击它后, 它去匹配相同的路径. 等介绍完link与Route组件, 我们再来看一个完整的路由匹配

image.png

to

  • 字符串
<Link to="/goWhere" > goWhere </Link>
  • 对象
<Link to={{ 
    pathname: "/goWhere",
    search: '?id=1',
    hash: '#nav'
}}> goWhere </Link>

NavLink(带激活效果的Link)

使用NavLink默认点击后给当前路由加上active类名, 如需更换类名, 使用activeClassName接受, 它能够触发一些点击效果

<NavLink activeClassName="avtive-name" className="" to="/home" >Home</NavLink>

路由组件Route

它就是负责展示与你在Link的to属性中传的路径相对应的组件.

image.png

  • 这是一个完整的路由匹配Demo.
// App.jsx
import React, { Component } from 'react'
import About from './pages/About'
import Home from './pages/Home'
import { Link, Route} from 'react-router-dom'
import './App.css'
class App extends Component {

  render() {
    return (
      <div className="">
        <h1>React-Demo</h1>
        <nav className='left'>
            <Link to="/about" >About</Link> 
            <Link to="/home" >Home</Link>
        </nav>
        <div className='right'>
          {/* 路由切換 */}
          <Route path='/about' component={About} /> {/* 👈如果匹配到/about则展示 About组件 */}
          <Route path='/home' component={Home} /> {/* 如果匹配到/about则展示 Home组件 */}
        </div>
      </div>
    )
  }
}

export default App

  • 这就是一个基本的路由组件的匹配过程, 请注意我后面点击了浏览器的回退键, 它同样回退了我之前的页面, 这就证明Route的跳转, 是会被记录到历史栈中的

基本路由组件.gif

  • 如果我们把Link换成 NavLink它就会根据类名高亮被激活的路由

NavLink.gif

渲染方式

Route的组件有三种传递方式, component, render, children

image.png

  • 注意看Route部分
import React, { Component } from 'react'
import About from './pages/About'
import Nav from './pages/Nav'
import { Route,Link} from 'react-router-dom'
import './App.css'
class App extends Component {

  refCallback = node => {
    console.log(node)
  }
  render() {
    return (
      <div className="">
        <h1>React-Demo</h1>
        <nav className='left'>
            <Link to='/about'>
              About
            </Link>
            <Link to="/home">Home</Link>
            <Link to="/nav">Nav</Link>
        </nav>
        <div className='right'>
          {/* 路由切換 */}
          
          <Route path='/about' component={About} /> {/* component */}
          
          <Route path='/home' render={              {/* render */}
            props => (
              <div {...props} >Home</div>
            )
          } />
          
          <Route path='/nav' children={             {/* children */}
            ({props, match}) => (
              match ? <Nav {...props}/> : <div>默认</div>
            )
              
          } />
        </div>
      </div>
    )
  }}

export default App

注意看这个图, 我们在children中接受到一个match: boolean的属性, 它用来判断当前是否与/nav路径匹配与否, 如不匹配, 则展示默认内容, 匹配则展示我们书写的内容

三种渲染方式2.gif

  • render属性可以直接写一个内联结构, 十分方便, 也可以直接传递prop

  • children 则当你需要展示默认内容的时候, 才需要用到它,其他的与render相似

switch

仅匹配一次, 如果同路径路由, 仅匹配第一次遇到的(提高效率)

  • 假设我们有这么多个相同路径的路由 那么路由就会这样匹配
<div className='right'>
  {/* 路由切換 */}
    <Route path='/about' component={About} />
    <Route path='/home' component={Home} />
    <Route path='/home' component={Home} />
    <Route path='/home' component={Home} />
</div>

image.png

  • Route组件外部加上Switch标签, 则可以预防这种匹配
import { Route, Link, Switch } from 'react-router-dom'

<div className='right'>
  {/* 路由切換 */}
  <Switch>
    <Route path='/about' component={About} />
    <Route path='/home' component={Home} />  {/* 完成匹配 终止 */}
    <Route path='/home' component={Home} />
    <Route path='/home' component={Home} />
  </Switch>
</div>

image.png

模糊与严格匹配

默认情况下为模糊匹配, 也就是路径如果为多层的,仅匹配到第一层就会渲染

class App extends Component {
  render() {
    return (
      <div className="">
        <h1>React-Demo</h1>
        <nav className='left'>
            <Link to='/about'>
              About
            </Link>
            <Link to="/home">Home</Link>
            <Link to="/home/a" >HomeA</Link>
        </nav>
        <div className='right'>
          {/* 路由切換 */}
            <Route path='/about' component={About} />
            <Route path='/home' component={Home} />
            <Route path='/home/a' component={HomeA} /> {/*home子路由*/}
        </div>
      </div>
    )
  }
}
  • 由于 Router 的模糊匹配机制, 如果我们点击home/a 上方的home组件也会被匹配到

image.png

<div className='right'>
      {/* 路由切換 */}
    <Route path='/about' component={About} />
    <Route path='/home' component={Home} exact /> {/*要求严格匹配*/}
    <Route path='/home/a' component={HomeA} /> {/*home子路由*/}
</div>
  • 如果加上 给对应的路由加上 exact 属性, 就能预防这种情况

image.png

传值

总的来说, 通过params的形式, React会从路径中解析出对应参数 image.png

params

明文传输, 但是刷新参数不消失

路由传参_params.gif

  • 将要传递的值写到路径上
<Link to={`/home/cHome/detail/${item.id}/${item.title}`}>{item.title}</Link>
  • 将要传递的值动态写到路径上
<Route path='/home/cHome/detail/:id/:title' component={Detail} />
  • 再从props中结构出来
export default class Detail extends Component {
  render() {
      const { id, title } = this.props.match.params
      let content = msData.find( item => {
          return item.id === id
      })
      return (
          <ul>
              <li>content: {content.title}</li>
              <li>id: {id}</li>
              <li>title: {title}</li>
          </ul>
      )
  }
}

search

问号传参的形式传递值, 并且无需在Route的path中标明键, 同样刷新不会丢失参数

  • 传值
<Link to={`/home/cHome/detail/?id=${item.id}&title=${item.title}`}>{item.title}</Link>
  • 接收(urlencoded)

$ yarn add qs

一个用于解析urlencoded格式的三方库

// in component of Route 
const {search} = this.props.location 
// 接受形式为未处理的字符串 ?id=xx&title=xx 
const { id, title } = qs.parse(search.substring(1))

query

非明文传输, 并且刷新了参数会丢失

  • 传入数据, 并且无需在Route中定义
<Link to={{pathname:'/home', query: totalData}}>Home</Link>
  • 接收参数
class Home extends Component {
  render() {
    console.log(this.props.location)
      const {title, age} = this.props.location.query
      return (
          <div>
              Home: {title}
              age: {age}
          </div>
      )
  }
}
  • 刷新后数据丢失

当让你需要自己做一点处理, 防止错误蔓延, 我们会在ErrorBoundary讲到如何预防子组件的错误蔓延到父组件

路由传参_qeury.gif

state

非明文传输, 并且刷新后数据不丢失, 使用方式跟query相同, 只不过键名换成了 state

<Link to={{pathname:'/home', query: totalData}}>Home</Link>
class Home extends Component {
  render() {
      const { state } = this.props.location
      return (
          <div>
              Home:{state}
          </div>
      )
  }
}
  • 四种传参方式感觉都有自己的使用场景, 怎么使用就看个人了

编程式路由

编程式路由的使用方式非常简单, 就相当于你无需去写一个Link组件, 而是直接通过函数就能够让页面实现跳转.

  • 如果你使用过小程序或者uniapp就知道, 这个就类似于 navigateTo({})

image.png

  • 这里的使用非常简单, 就不做演示了 添加一个事件对象, 当点击这个按钮我们就实现跳转
<button onClick={() => this.push(item.title, item.id)}>push</button>
push (title, id) {
  this.props.history.push(`/home/cHome/detail`,{title, id})
}

这就完成了一次跳转

push意为, 往浏览器历史栈顶放入一个历史记录, 然后跳转.

replace则是, 替代掉当前的栈顶的一个记录, 换成当前的. 也就是说, 按回退键是回不到跳转前的页面的.

forward 就是浏览器跳转页面的向前键

back 就是浏览器跳转页面的回退键

image.png

路由懒加载(Lazy-load)

与图片懒加载同理, 就是在用到了这个路由组件的时候, 我们才去进行加载, 这是很有必要的优化手段, 可以提高当前页面的渲染速度, 具体案例请查看项目

lazy

import React, { Component, lazy, Suspense } from 'react'
// 通过lazy函数引入 组件
const About = lazy(() => import('./pages/About'))

Suspense

他可以接受一个fallback属性的组件, 用于产生过渡效果

<div className='right'> 
    {/* 路由切換 */} 
    <Suspense fallback={<Loading />}>  {/*过渡效果组件*/}
        <Route path='/about' component={About} /> 
        <Route path='/home' component={Home} /> 
    </Suspense> 
</div>

Redux

全局状态管理, 注册至Redux的状态可以在任何组件内直接获取。Redux的使用相对比较复杂, 我十分建议您学习了概念, 要通过Demo看看整个redux是怎么串起来的 Redux.png

怎么理解Redux

对于这些模块的翻译, 纯粹个人理解, 而非官方翻译

  • Actions(指令)
  • Reducers(执行)
  • Store(存储动作执行的结果)

怎么理解?

可以想象你在银行ATM存钱的过程, 你(Components)按了存款的指令, 并把钱(data: 数据)放入机子中。ATM(Store)接收到你这个指令(Actions)会去在你的存款中加上一笔钱(Reducers), 也就是执行你的指令

  • 你无法直接取钱, 存钱, 需要经过ATM操作(无法直接从store[propName]拿到属性, 必须通过getState()

  • 你可以在全国各地的ATM中拿到你的钱(公共状态)

使用

根据我们在银行存钱的过程, 我们来看看一个完整的Redux流程是怎样的

image.png

Store(存储动作执行的结果)

在使用Redux, 你需要创建一个store实例, 用来分发, 存储数据。 也就是要有一个store.js的入口文件

  • 这一步就好比, 你在预设提款机的操作, countReducer 就是预设文件, 存入 store 中告诉它, 它会有哪些操作。
// store.js 
import { createStore } from "redux"; 
// 需要在一开始就让Redux知道要怎样执行你的指令
import countReducer from './count-reducers' 
export default createStore(countReducer)

APIs

  • store.dispatch(action) 拿到一个动作,通知Reducer执行

Actions我们在介绍该

  • store.subscribe 监听State, 如果State发生变化, 立即执行该函数, 用于通知视图层更新(重新渲染) 由于我们更改状态只改变了, store中的状态, 这可能导致页面中有些视图应该基于数据发生改变, 但是没有发起, 以下的监听方式就十分重要的
// 项目入口文件
import React from 'react'
import ReactDom from 'react-dom'
import App from './App'
import store from './redux/store'


ReactDom.render(<App />, document.getElementById('root'))
store.subscribe( () => {
    ReactDom.render(<App />, document.getElementById('root'))
})
  • store.getState 外部是无法直接获取状态(state)的, 需要通过函数store.getState()获取

通常来说, 在一个企业项目中, store, actions 都是单独写成一个文件的, 所以我们下列的代码, 也仿照这个习惯

Actions(指令)

  • 一个合法的 Action 对象:
action = {
    type: 'whatWillDo',
    payload: data
}

指令文件

// Action.js redux的动作, 单独一个文件管理 
import {INCREMENT, DECREMENT} from './constant' 
export const incrementAction = data => ({type: INCREMENT, data}) 
export const decrementAction = data => ({type: DECREMENT, data})

使用

Actions 需要通过 store 暴露的dispatch([object])进行分发

import React, { Component } from 'react' 
import store from '../../redux/store' 
import { incrementAction, decrementAction} from '../../redux/count_action'

export default class Header extends Component { 
    increament = (value) => { 
        store.dispatch(incrementAction(value))
    } 
}
  • 当然如果你不愿意创建Actions文件, 也可以这样

你传入的Action对象, 只要是合法的就能被接受

export default class Header extends Component { 
    increament = (value) => { 
        store.dispatch({type: 'increament', data: value})
    } 
}

异步的Action

组件需要通过

// 异步
export const addActionAsync = (data, time) => {
    return (dispatch) => {
        setTimeout(() => {
            dispatch({type: 'add', payload: data})
        }, time)
    }
}

这里我们返回了一个函数, 并非常规的Action对象, 正常使用这个Action是会报错的, 因为 Redux 的Reducers, 不能接受一个函数作为Action

  • 怎么解决?

我们需要在store.js中使用一个叫thunk的中间件

$ yarn add redux-thunk

在store中使用中间件, 需要传入一个由 Redux 提供的函数 applyMiddleware()

import { createStore, applyMiddleware } from "redux"; 
import countReducer from './count-reducers' 
import thunk from 'redux-thunk'
export default createStore(countReducer, applyMiddleware(thunk))

Reducers(执行)

Reducers 从名字来看应该翻译成累加器, 它是基于前状态进行加工得到目前的状态 image.png

要求

  • 在store.js文件中,需要通过createStore()传入reducers, 告诉redux,当接收到什么动作指令的时候, 需要做什么事情

  • 要求 reducers 是一个纯函数

使用

  • 定义一个初始状态, 在传入Reucer函数的时候, 项目初始化就会定义一个相关的 state
const initState = 0 

export default function addReudcer (prevState = initState, action) {
    const { type, payload } = action
    switch (type) {
        case 'add':
            return prevState + payload
        default:
            return prevState // (初始化的时候,无指令, 返回默认值)
    }
}
  • store.js中定义
import { createStore, applyMiddleware  } from "redux";
import addReducer from "./reducers/addReducer";
import thunk from 'redux-thunk'

export default createStore(addReducer, applyMiddleware(thunk))

集中暴露

如果你熟悉 VueX-State 你肯定很奇怪, 为什么没有类似于 state.js 管理全局状态的文件

单独使用暴露一个 Reducer 的时候, 其返回值就是当前的state, 因为 state 内就这么一个值.

但是如果我们使用的 Reducer 特别多, 通常就需要集中暴露, 并且给每一个 Reducer 的返回值定义一个键名.

  • 集中 Reducer 的文件
// allReducers.js
import { combineReducers } from "redux";
import addReudcer from "./addReducer";
// 合并所有的reducers
export default combineReducers({
    add: addReudcer
})
  • 以同样的方式传入allReducers
import { createStore, applyMiddleware  } from "redux";
import allReducers from "./reducers/allReducers";
import thunk from 'redux-thunk'
export default createStore(allReducers, applyMiddleware(thunk))

请先看一下 最简单的redux_Demo 的代码, 这里我们直接在UI组件内使用了Redux, 但实际上这并不合规范

connect(规范)

正常情况下使用Redux要求: 我们要将对 store 的 state 做的操作, 剥离出UI组件, 通过prop的形式传入操作函数,以及状态值. UI组件只负责展示

image.png


如果每次都需要自己书写一个父组件,那就太麻烦了, Redux也替我们做了这一步, 通过connect立即执行函数, 将能简写这一步

使用

  • 假设我们有一个 ComponentA 组件
class ComponentA extends Component {
    render() {
        return (
            <div className='C-A'>
                <h1>ComponentA</h1>
            </div>
        )
    }
}
  • 创建一个父容器(无需自己创建), 传入UI组件, 这个立即执行函数执行后会默认返回父组件, 也就是供我们使用的组件, 所以我们要将其暴露出去, 好在外部能够使用
export default connect()(ComponentA)

父组件如何传值给子组件?

connect(mapStateToProps:function, mapDispatchToProps: function) 函数接收两个参数[mapStateToProps] [mapDispatchToProps]

  1. mapStateToProps(state) 这个函数能够默认接收到 store.state 对象, 假设我们需要获得一个叫 add 的 state 这个函数传入connect后会被执行, 并且返回值作为 props 传入UI组件
const mapStateToProps = (state) => { return { add: state.add } }
  1. mapDispatchToProps(dispatch) 接收到一个 store.dispatch 函数, 用于执行动作

这个函数传入connect后会被执行, 并且返回值(函数)作为 props 传入UI组件

const mapDispatchToProps = (dispatch) => {
    return {
        addCount: value => dispatch(addAction(value)),
    }
}
  1. 传递给connect, 这样子一个完整的父组件我们就创建成功了
export default connect( mapStateToProps, mapDispatchToProps )(ComponentA)

看到这里你可能会觉得奇怪, 在[mapStateToProps] [mapDispatchToProps]参数中, 我们都使用了 store对象里面的state, dispatch函数, 那这两个值是从哪里拿到的? 当然是我们自己让 store 作为 prop 传进去的,

  1. 我们来看看怎么使用这个组件, 非常简单.
import React, { Component } from 'react'
import ComponentA from "./containers/ComponentA";
import store from './redux/store'; // 引入 store 对象

export default class App extends Component {
    render() {
        return (
            <div>
                <ComponentA store={store}/> {/* 将store传入 */}
            </div>
        )
    }
  1. 子组件怎么使用?

就像使用prop传递过来的 数据/函数 一样使用, 没有任何差别, 如果你想看完整代码👉 redux_剥离

connect参数的简写

  • mapDispatchToProps其实允许做为一个对象传入
  • 并且,原先需要自己执行的 dispatch 也可以交给 conncet 来做, 我们仅需将 action作为一个对象的键值对丢进去

简写前:

export default connect(
    (state) => {
        return {
            state
        }
    },
    (dispatch) => {
        return {
           addCount: (value) => dispatch(addAction(value)),
           addCountAsync: (value) => dispatch(addActionAsync(value)),
        }
    }
)(ComponentA)

简写后:

export default connect( 
    state => ({ state }), 
    { 
        addCount: addAction, 
        addCountAsync: addActionAsync, 
    } 
)(ComponentA)

无需逐个传入store.js

并不是每个这样的组件, 我们都需要自己传入store

// index.js 入口文件
import React from 'react'
import ReactDom from 'react-dom'
import App from './App'
import store from './redux/store'
import { Provider } from 'react-redux' // 👈

ReactDom.render(
    <Provider store={store}>
        <App />
    </Provider>, 
    document.getElementById('root'))
    
// 也不需要自己监听了
// store.subscribe(()=>{
//     ReactDom.render(<App />, document.getElementById('root'))
// })

Redux可视化插件

更方便地观察数据流动

$ yarn add redux-devtools-extension

image.png

// store.js
import { createStore, applyMiddleware } from "redux";
import thunk from 'redux-thunk'
import { composeWithDevTools } from "redux-devtools-extension";
import allReducer from "./reducers";

export default createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))
  • 谷歌浏览器安装一个插件

image.png

  • 完毕😄

image.png


Hooks

钩子(Hook)勾出React的特性

Hooks.png

理念

hooks为React提供了更加直接, 简单的API.

hook像是一个比起类组件更加底层的概念

stateDiagram-v2

hook --> class组件

React底层 --> hook

解决了什么问题?

  • 函数式组件无法使用像类组件的一些 React 特性,如: State, 生命周期钩子, ref等
  • class式组件对于初学者有一定的学习门槛, 函数组件更加友好 类式组件依然可以使用,两者没有任何高低之分

注意

  • 可以从useState返回的数组中结构出 state, setState()

  • setState([object | (preState)⇒{}])

useState

函数式组件无法获取到react实例, 也就无法获取state与setState()

image.png

import React, { Component, useState } from 'react'

// 函数- 如果以use开头, React 会认为你使用了自定义hook
function StateHook() {
    
    const [count, setCount] = useState(0) // 👈

    return (
        <div>
            <h4>Home Component</h4>
            <h5>count: {count}</h5>
            <button onClick={() => setCount(count + 1)}>count add</button>
            {/* 传函数 */}
            <button onClick={() => setCount(count => count + 1)}>count add</button>
        </div>
    )
}
  • 对比(class组件)
// 对照(类)
class StateHook extends Component{
    state = {
        count: 0
    }
    render() {
        return (
            <div>
                <h4>Home Component</h4>
                <h5>count: {this.state.count}</h5>
                <button onClick={() => this.setState({count: this.state.count + 1})}>count add</button>
            </div>
        )
    }
}

useEffect

函数式组件无法使用生命周期钩子, useEffect就是来解决这个问题的

image.png

import React, { Component, useState, useEffect } from 'react'

// 函数- 如果以use开头, React 会认为你使用了自定义hook
function EffectHook() {
    
    const [count, setCount] = useState(0)

    useEffect(() => {
        // 这个函数相当于 componentDidMount + componentDidUpdate
        console.log('页面初始化完毕/更新数据完毕')
        // 返回的函数就相当于 componentWillUnmount
        return () => {
            // 做一些清理处理, 如清理一个定时器 clearInterval()
            // 严格来讲,仅在清理副作用上与componentWillUnmount功能类似, 但是并非等同于卸载钩子
        }
    })
    function add() {
        setCount(count + 1)
    }
    return (
        <div>
            <h4>Home Component</h4>
            <h5>count: {count}</h5>
            <button onClick={add}>count add</button>
        </div>
    )
}
  • 对比
// 对照(类)
class EffectHook extends Component{

    state = {
        count: 0
    }
    componentDidMount() {
        console.log('页面初始化完毕')
    }
    componentDidUpdate() {
        console.log('更新数据完毕')
    }
    render() {
        return (
            <div>
                <h4>Home Component</h4>
                <h5>count: {this.state.count}</h5>
                <button onClick={() => this.setState({count: this.state.count + 1})}>count add</button>
            </div>
        )
    }
    componentWillUnmount() {
        console.log('组件即将卸载') // 仅演示, effect函数的返回函数跟它还是有一些差别,
    }
}
  • useEffect 相当于三个钩子的整合
React.useEffect(() => {
    let timer =setInterval(() => {
       add()     // 这一部分相当于 componentDidMount 和 componentDidUpdate
    },1000)      // 至于趋进于哪个取决于第二个参数数组[]
    return () => {
        clearInterval(timer) // 这个返回函数
    }
},[])

参数2

用于监听页面的改动

// 不传递第二个参数, 页面中任何改动都会被监听
React.useEffect(() => {
    // .....
})
// 传递一个空数组, 页面中任何改动都不监听
React.useEffect(() => {
    // .....
},[])
// 传递具体的状态名, 则该状态改变会被监听
React.useEffect(() => {
    // .....
},[stateName])

useRef

image.png

function RefHook() {
    
    const [count, setCount] = useState(0)
    const countRef = useRef() //👈
    const headerRef = useRef()
    function showInfo () {
        console.log('countRef', countRef)
        console.log('headerRef', headerRef)
    }
    return (
        <div>
            <h4>Home Component</h4>
            <h5 ref={headerRef}>header</h5>
            <input type="text" ref={countRef} onChange={showInfo}/>
        </div>
    )
}
  • 对比
对照(类)
class RefHook extends Component{

    state = {
        count: 0
    }
    showInfo = () => {
        console.log('countRef', this.refs.countRef)
        console.log('headerRef', this.refs.headerRef)
    }
    render() {
        return (
            <div>
                <h4>Home Component</h4>
                <h5  ref='headerRef' >header</h5>
                <input type="text" ref='countRef' onChange={this.showInfo}/>
            </div>
        )
    }
}

自定义Hook

image.png

  • 以use开头定义我们的hook函数
// 这部分逻辑就可以使用到任何其他组件中去了
function useAllself(initState) {

    const [count, setCount] = useState(initState)

    useEffect(() => {
        console.log('count', count) // 监听
    })
    // 如果值为偶数, 就返回
    return [count, setCount]

}
export default SelfHook
import React, { Component, useState, useEffect } from 'react'


function SelfHook() {
    
    const [count, setCount] = useAllself(0) // 使用我们抽象的hook
    
    return (
        <div>
            <h4>Home Component</h4>
            <h5>count: {count}</h5>
            <button onClick={() => setCount(count + 1)}>count add</button>
        </div>
    )
}

注:更多相关的hookAPI

在hook的加入以后, 函数组件的优势变得极为明显, 现在绝大多数开发用的也都是函数组件, 并且我认为这也是未来的一种趋势


拓展

一些有用的扩展

拓展.png

Fragment

JSX语法要求每一个组件最外层都必须由一个标签包裹, 但是这个标签是多余的, 这时候使用Fragment组件, 可以解决层级多余的问题

image.png

使用

import React, { Component,Fragment } from 'react'

export default class DemoFrag extends Component {
    render() {
        return (
            <Fragment key={index}>
               <p>111</p>
            </Fragment>
        )
    }
}

  • 这两种写法都有同样的效果, 但是空标签不允许你传入任何属性, 所以如需传入属性,则使用Fragment
export default class DemoFrag extends Component {
    render() {
        return (
            <>
               <p>111</p>
            </>
        )
    }
}

Context

在通信章节我们提到的 context, 允许你进行祖组件与孙组件的,跨组件通信

image.png

使用

建议看一下Demo的代码 context通信

假设你希望在不同的文件使用context, 那我建议你将context单独建立为一个文件去暴露

  • 引入context
const MyContext = React.createContext() 
const { 
    Provider,     
    Consumer  
} = MyContext 
  • 使用

为了看起来更加直观, 假设下列代码都在同一个文件内, 如需看多文件的情况, 请看Demo

const MyContext = React.createContext()
const {Provider, Consumer} = MyContext
export default class Grandpa extends Component {
    state = {
        name: 'Link',
        age: 18
    }
    render() {
        const {name, age} = this.state
        return (
            <div className='grand'>
                <h1>我是祖组件</h1>
                <Provider value={{name,age }}> // 👈
                    <Father />  {/* 父组件 */}
                </Provider>
            </div>
        )
    }
}
  • 孙组件接收(类式)

基于 MyContext 定义一个私有属性contextType

class Son extends Component {
    static contextType = MyContext
    render() {
        const {name, age} = this.context
        return (
            <div className='son'>
                <h1>我是孙组件</h1>
                <p>我是:{name}, 今年: {age}</p>
            </div>
        )
    }
}
  • 孙组件接收(函数式)

通过 Context 下的 Consumer 接受到的参数进行传递

function Son() {
    return (
        <div className='son'>
            <h1>我是孙组件</h1>
            <Consumer>
                {
                    value => <p>我是:{value.name}, 今年: {value.age}</p>
                }
            </Consumer>
        </div>
    )
}

PureComponent

对数据进行变更的时候, 无论 setState 是否改变数据, render 函数都一定会执行, 并且如果组件内嵌套有其他组件, 子组件的 render 也会被调用, 即时数据没有任何改变.这就造成了很多无谓的组件重渲染

image.png

解决办法

  1. 重写 shouldComponentUpdate 钩子        shouldComponentUpdate 默认情况下都会返回一个 true, 使得组件接下来的生命周期钩子能够被调用, 如果返回 false像是一个阀门被关闭了一样, 则它以下的钩子不再执行

       根据这一点, 我们就可以重写它, 去判断传入该组件的 state, props 有没有发生改变, 有的话 我们在让其继续执行

shouldComponentUpdate(nextProps,nextState) {
    // 如数据未变化, 阻止render
    console.log('改变前', this.state, this.props);
    console.log('改变后', nextState, nextProps);
    return !this.state.name === nextState
}

这里就不提供Demo了, 因为一般不会这么做, 🤭

  1. 让你的组件继承 PureComponent
import React, { Component, PureComponent } from 'react'
// 改变组件的继承
class index extends PureComponent { // 👈
	// ....
}

注意: PureComponent 内对 shouldComponentUpdate 进行重写, 更好的检测了 setState , props 的变化, 但是其只对 State 进行浅比较, 只判断SetState([object]) 的参数对象object的内存地址是否发生了变化, 也就说, 在不改变State对象地址的情况下,修改值, 依然会导致render调用

RenderProps

从直接传组件变为回调函数的形式 image.png

常规的props传组件

render() {
    return (
        <div className='index' >
            <h1 >index</h1>
            <A B={<B />}/> // 👈
        </div>
    )
}
  • A Component
class A extends Component {
    render() {
        return (
            <div className='A'>
                <h1>AAA</h1>
                {this.props.B} // 👈
            </div>
        )
    }

这种传递方式很方便, 但无法传参数

Render属性传递一个函数

给A组件传递一个接受 name, age 的函数

render() {
    return (
        <div className='index' >
            <h1 >index</h1>
            <A render={(name, age) => <B  name={name} age={age}/>}/> // 👈
        </div>
    )
}
  • A Component

将数据作为 props 传给 B 组件

class A extends Component {
    state = {
        name: '我是A传递给B的数据',
        age: '我也是呢'
    }
    render() {
        const { name, age } = this.state
        return (
            <div className='A'>
                <h1>AAA</h1>
                {this.props.render( name, age )} // 👈
            </div>
        )
    }
}
  • B Component
class B extends Component {
    render() {
        return (
            <div className='B'>
                <h1>BBB</h1>
                {this.props.name}<br /> // 👈
                {this.props.age}
            </div>
        )
    }
}

ErrorBoundary

防止错误的扩散处理, React如果子组件发生错误, 会导致整个页面渲染不出来, 这时候错误的边界处理就十分重要了, 但只能捕获子组件生命周期内发生的错误

image.png

使用

核心钩子: getDerivedStateFromError 这个钩子会返回一个错误对象, 用于捕获子组件是否发生错误

// 从Error中获得一个state状态, 
static getDerivedStateFromError(err) {
    console.log('err', err)
    return {hasErr: err}
}
render() {
    return (
        <div className='index' >
            <h1 >index</h1>
            {/* 错误对象存在,则渲染条件成立的DOM*/}
            {this.state.hasErr ? <h1>这里发生错误了</h1> : <A />}
        </div>
    )
}
  • 记录错误
componentDidCatch(error, errorInfo) { 
    console.log('发生错误咯', error, errorInfo)  // 错误发生完毕后, 可进行记录操作 
}
  • 注意(摘自React官网):

错误边界无法捕获以下场景中产生的错误:

  • 事件处理(了解更多
  • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
  • 服务端渲染
  • 它自身抛出来的错误(并非它的子组件)

(完)

参考资料

1. controlled-vs-uncontrolled 受控组件与非受控组件的区别

2. React React官方文档

3. React-router React-Router文档


感谢😘

这篇文章纯粹是从我个人学习React的角度, 去教自己怎么学习React. 可能很多地方您觉得我一笔带过, 或是讲得不正确, 也希望您能提出来. 这也是我自己可能学习不到位的地方, 十分感谢, 您能看到这里


如果觉得文章内容对你有帮助:

  • ❤️欢迎关注点赞哦! 我会尽最大努力产出高质量的文章
    个人公众号: 前端Link
    联系作者: linkcyd 😁