马上到了又一年的秋招, 还不会React? 没关系! 我这里有最精简的入门指南😄
通过6张脑图带你入门React, React文档相对于刚入门的新人来说, 其跳跃性强, 案例的综合性之高让人读起来都费劲. 为此我整理了一份脑图+超简单的Demo的笔记, 让你迅速明白ReactAPI的作用, 具体的Demo地址👉react-study-guide, 欢迎star⭐。 所有的脑图, Demo都在仓库里可以直接获取
并且在许多演示片段, 我都录制了GIF图, 让你更直观的体会到代码的实现与Demo的样式
总览
放心看起来虽然多, 但是内容都不复杂, 每个分支我都配备了对应的讲解
JSX语法
最重要的一点, 大括号 {} 内写的是
JavaScript
的语法
- 这个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最核心的内容, 我会将脑图拆分成一个部分一个部分来看
- 如果你不理解
组件化
这个词, 没关系继续往下看, 稍后你就会慢慢明白了
函数式与类式组件
函数式组件是目前使用最多, 并且是最受欢迎得用法, 但是两者并非谁更加优秀, 用法看个人即可。
函数式
使用简单,没有烦人的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'))
- 注 如果你对类的写法不熟悉, 推荐你看一下这篇文章底部对类的简单介绍原型与原型链: 如何自己实现 call, bind, new?
事件对象
就是各种事件(点击等), 使用起来除了书写差异, 其实跟原生没有太大区别。
书写规范
使用 小驼峰 的形式,取代传统的写法即可
- 传统写法
<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组件应该长什么样
state(状态)
组件本身的状态,是一个挂载在当前组件实例上的一个属性
this.setState(): 有两种使用方式:
- 对象式
// 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
// 根据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元素
-
类似于document.getElementById('id') 只是这件事由React帮你做了
-
使用方式
注意: ref的使用方式有三种, 但目前为止最被官方推荐的为第三种
字符串形式❌(不推荐)
// 这是一个原生的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
- 效果
也就是说, 在全局主组件中有一个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的官网为例:
- 我们就可以把头部的红色部分看成一个头部组件.
- 底部的橙色看成一个组件, 我们仅需写一个橙色组件, 然后将标题以及内容, 作为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>
)
}
一个橙色的框框就是我们一个"信息"组件. 这样我们就复用了这个组件, 而不是自己去复制三个. 当然组件化远远比我形容的要强大, 剩下的就靠你的创造力了.
注: 在循环处我加入了一个key
属性, 他的作用是给每一个组件绑定一个唯一的标识, 这有助于react识别组件.但是该标识并不是要求在全局下唯一,而是在兄弟节点之间,也就是非兄弟节点(不同组件),是可以使用一个数据中的id作为标识的. 目前只需要知道这一点就可以了. 有兴趣可以深入了解, 它的作用与原理比想象的复杂
组件通信
从这里开始, 将会详细的书写一个React组件, 并且这些Demo都在react-study-guide项目中有对应Demo, 如果看不明白, 可以clone一下项目自己调试一下, 并且强烈建议你自己手敲一下这些Demo
父子组件
根据上面的例子 父传子 应该非常显而易见了, 就是通过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'"
}
- 这里我仅提供思路, 因为它和上面那个案例的实现是一致的, 尝试自己实现一下吧!
非亲组件
- 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下来调试下面输出的例子,
新版
- 页面初次渲染时
- 数据更新时
旧版
旧版的生命周期在Demo中有演示, 这里就不做展示了
注意
即便父组件更新与子组件无任何关系, 也会导致子组件刷新, 可以通过继承PureComponent解决, 但是shouldComponentUpdate不可用
通过这张图可以看出, 尽管我传入的childName没有发生任何改变, 但是子组件还是发生了重新渲染
新旧生命周期对比
受控与非受控组件
原本表单元素是自己维护自己的值, 并且只能通过用户的输入进行值的修改. 而在React中可变状态的值只能保存于state中, 并且基于setState去修改. 两者结合, 使得用户输入的值保存至state中, 并在事件中基于setState去改变. 组件内部的state就成了唯一的数据源(值都保存于此). 基于这种形式控制的组件就成为 "受控组件"
看不懂? 看gif图
- 上为受控组件, 下为非受控组件
发现区别在哪里没? 受控组件的数据会实时的渲染到页面上, 同步更新页面与数据。而非受控则不会.他们两者用的都是change事件, 受控组件用的是React做过修改的onChange, 而下方的是原生的change事件
- 这就是受控与非受控的区别, 简单吧。当然这只是从视图层面解释, 请仔细看上方的定义
请思考一下, 如上方的受控组件, 我要求他们只能通过一个处理器函数去处理 event.target.value值,怎样才能保证两者的值不会覆盖到同一个state状态上?
- 答案地址:React组件化
可被创建为受控组件表
React-Router
使用
$ yarn add react-router
- 入口文件引入即可
import { Route,Link} from 'react-router-dom'
HashRouter与BrowserRouter
一般情况下, 与服务器做交互,使用的都是 BrowserRouter, 而使用静态文件的服务器的情况才用 HashRouter,并且这两者都要求包裹在使用路由的外部
- 使用
// 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组件, 我们再来看一个完整的路由匹配
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属性中传的路径相对应的组件.
- 这是一个完整的路由匹配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的跳转, 是会被记录到历史栈中的
- 如果我们把Link换成 NavLink它就会根据类名高亮被激活的路由
渲染方式
Route的组件有三种传递方式,
component
,render
,children
- 注意看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路径匹配与否, 如不匹配, 则展示默认内容, 匹配则展示我们书写的内容
-
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>
- 给
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>
模糊与严格匹配
默认情况下为模糊匹配, 也就是路径如果为多层的,仅匹配到第一层就会渲染
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组件也会被匹配到
<div className='right'>
{/* 路由切換 */}
<Route path='/about' component={About} />
<Route path='/home' component={Home} exact /> {/*要求严格匹配*/}
<Route path='/home/a' component={HomeA} /> {/*home子路由*/}
</div>
- 如果加上 给对应的路由加上 exact 属性, 就能预防这种情况
传值
总的来说, 通过params的形式, React会从路径中解析出对应参数
params
明文传输, 但是刷新参数不消失
- 将要传递的值写到路径上
<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讲到如何预防子组件的错误蔓延到父组件
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({})
- 这里的使用非常简单, 就不做演示了 添加一个事件对象, 当点击这个按钮我们就实现跳转
<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
就是浏览器跳转页面的回退键
路由懒加载(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
对于这些模块的翻译, 纯粹个人理解, 而非官方翻译
- Actions(指令)
- Reducers(执行)
- Store(存储动作执行的结果)
怎么理解?
可以想象你在银行ATM存钱的过程, 你(
Components
)按了存款的指令, 并把钱(data: 数据
)放入机子中。ATM(Store
)接收到你这个指令(Actions
)会去在你的存款中加上一笔钱(Reducers
), 也就是执行你的指令
-
你无法直接取钱, 存钱, 需要经过ATM操作(无法直接从store[propName]拿到属性, 必须通过
getState()
-
你可以在全国各地的ATM中拿到你的钱(公共状态)
使用
根据我们在银行存钱的过程, 我们来看看一个完整的Redux流程是怎样的
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 从名字来看应该翻译成累加器, 它是基于前状态进行加工得到目前的状态
要求
-
在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组件只负责展示
如果每次都需要自己书写一个父组件,那就太麻烦了, 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
]
- mapStateToProps(state) 这个函数能够默认接收到
store.state
对象, 假设我们需要获得一个叫 add 的 state 这个函数传入connect后会被执行, 并且返回值作为 props 传入UI组件
const mapStateToProps = (state) => { return { add: state.add } }
- mapDispatchToProps(dispatch) 接收到一个
store.dispatch
函数, 用于执行动作
这个函数传入connect后会被执行, 并且返回值(函数)作为 props 传入UI组件
const mapDispatchToProps = (dispatch) => {
return {
addCount: value => dispatch(addAction(value)),
}
}
- 传递给connect, 这样子一个完整的父组件我们就创建成功了
export default connect( mapStateToProps, mapDispatchToProps )(ComponentA)
看到这里你可能会觉得奇怪, 在[
mapStateToProps
] [mapDispatchToProps
]参数中, 我们都使用了 store对象里面的state, dispatch函数, 那这两个值是从哪里拿到的? 当然是我们自己让 store 作为 prop 传进去的,
- 我们来看看怎么使用这个组件, 非常简单.
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>
)
}
- 子组件怎么使用?
就像使用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
// 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)))
- 谷歌浏览器安装一个插件
- 完毕😄
Hooks
钩子(Hook)勾出React的特性
理念
hooks为React提供了更加直接, 简单的API.
hook
像是一个比起类组件更加底层的概念
stateDiagram-v2
hook --> class组件
React底层 --> hook
解决了什么问题?
- 函数式组件无法使用像类组件的一些 React 特性,如: State, 生命周期钩子, ref等
- class式组件对于初学者有一定的学习门槛, 函数组件更加友好 类式组件依然可以使用,两者没有任何高低之分
注意
-
可以从useState返回的数组中结构出 state, setState()
-
setState([object | (preState)⇒{}])
useState
函数式组件无法获取到react实例, 也就无法获取state与setState()
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就是来解决这个问题的
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
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
- 以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>
)
}
在hook的加入以后, 函数组件的优势变得极为明显, 现在绝大多数开发用的也都是函数组件, 并且我认为这也是未来的一种趋势
拓展
一些有用的扩展
Fragment
JSX语法要求每一个组件最外层都必须由一个标签包裹, 但是这个标签是多余的, 这时候使用Fragment组件, 可以解决层级多余的问题
使用
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, 允许你进行祖组件与孙组件的,跨组件通信
使用
建议看一下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
也会被调用, 即时数据没有任何改变.这就造成了很多无谓的组件重渲染
解决办法
- 重写 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了, 因为一般不会这么做, 🤭
- 让你的组件继承 PureComponent
import React, { Component, PureComponent } from 'react'
// 改变组件的继承
class index extends PureComponent { // 👈
// ....
}
注意: PureComponent 内对 shouldComponentUpdate 进行重写, 更好的检测了
setState
,props
的变化, 但是其只对State
进行浅比较, 只判断SetState([object])
的参数对象object的内存地址是否发生了变化, 也就说, 在不改变State对象地址的情况下,修改值, 依然会导致render调用
RenderProps
从直接传组件变为回调函数的形式
常规的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如果子组件发生错误, 会导致整个页面渲染不出来, 这时候错误的边界处理就十分重要了, 但只能捕获子组件生命周期内发生的错误
使用
核心钩子: 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 😁