Hi~我是Tom,这是我整理的React学习路线,文章许多地方对Vue进行的类比,阅读之前我希望你已经十分熟悉Vue.js。
这将是掘金上最全的React指南,里面涉及到React.js,Redux,路由,cli工具,指南主要是以使用步骤为主,告诉你如何直接实现需求,本指南主要是针对于刚入门React的同学,所以一些不必要的步骤或者不必要的说明我都会直接忽略,以防混淆降低学习的效率。
让我们开始吧!
安装
和Vue一样,React先从标签引入形式进行开发学习
-
引入三个文件
<!-- babel,用于编译JSX语法 --> <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> <!-- react --> <script src="https://unpkg.com/react@16/umd/react.development.js"></script> <!-- react-dom --> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
-
创建一个script标签
//script标签的type要为text/babel,因为这里需要babel来解析JSX语法 <script type="text/babel"> function MyFirstComponent(props){ return( <h1>我是tom,这是我创建的第一个react组件</h1> ) } //调用ReactDOM.render方法,渲染组件 ReactDOM.render(<MyFirstComponent/>,document.getElementById('root')) </script>
-
创建一个用来渲染react组件的html标签
<div id="root"></div>
-
运行即可,下面贴出根据上面的步骤写的完整HTML代码
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>StudyReact</title> </head> <body> <div id="root"></div> <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> <script src="https://unpkg.com/react@16/umd/react.development.js"></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"> </script> <script type="text/babel"> function MyFirstComponent(props){ return( <h1>我是tom,这是我创建的第一个react组件</h1> ) } //调用 ReactDOM.render 方法 ReactDOM.render(<MyFirstComponent/>,document.getElementById('root')) </script> </body> </html>
JSX语法
和vue的单文件.vue有点不同,jsx语法只是将组件的结构(html)和行为(js)整合到一起,没有将表现(css)整合到一起来,css还是采用引入的形式
-
可以直接写HTML语法:
-
let ele_obj = <div>hello world</div> //这里比较离谱的事情是,这里的结构代码不需要加''引号 //以前我们常用的的做法是将html结构写成字符串的形式,再使用dom对象的innerHTML进行解析。
-
-
JSX插值表达式使用单花括号,区别于vue使用胡子双花括号
-
let name_str = 'ahreal' let ele_obj = <div>my name is <span>{name_str}</span> </div>
-
-
使用单标签一定要加 /
-
let ele_obj = <img src="..." />
-
-
使用class要使用className
-
let ele_obj = <p className="name">ahreal</p>
-
-
使用行内样式style直接用对象的形式插入
-
let ele_obj = <div style={{'color':'red','fontSize':'20px'}}>hello,world!</div> //1.这里插入的是以对象的形式进行插入,所以这里用了双大括号,第一个括号是插值语法,第二个括号是对象的括号 //2.写样式的时候,如果遇到连接符号font-size这类应该改成驼峰形式fontSize //3.中间使用逗号,隔开 ,注意区分html里面是用分号;隔开 //4.强调外层不要有''引号这些东西,直接两个大括号{{}}
-
组件定义方式
组件名首字母需要大写
通过函数定义
function person(props){
return <div>hello,{props.name}</div> {/* 函数定义组件返回的必须是一个jsx对象 */}
}
通过类定义
如果通过类的方式去定义组件,那么组件必须继承于React.Component这个类
class person extends React.Component{
//必须定义一个render方法,
render(){
{/* 也是必须返回一个jsx对象 */}
return <h1>hello,{this.props.name}</h1>
}
//也可以定义组件的类的方法
sayName(){
console.log('我的名字是'+this.props.name)
}
}
类定义不需要写props这个形参,因为props是继承React.Component来的,只需要this.props即可访问
事件绑定
-
直接在渲染的jsx对象标签上进行绑定,需要写成驼峰的形式
比如:onclik 写成 onClick
-
事件处理的function直接写成类的方法
-
事件处理funciton内部如果需要访问this(组件本身)的话,直接访问是undefined
解决方案:
-
在绑定事件处理函数的时候使用bind明确this指向
class MyCom extends React.Component{ render(){ //这里在调用的时候使用bind return <div onClick={this.handleClick.bind(this)}>这是一个自定义组件</div> } handleClick(){ console.log('我被点击了'+this.props.message) } } ReactDOM.render(<MyCom message="ahreal"/>,document.getElementById('root'))
-
使用箭头函数
class MyCom extends React.Component{ render(){ return <div onClick={this.handleClick}>这是一个自定义组件</div> } //使用箭头函数去定义事件回调 handleClick=()=>{ console.log('我被点击了'+this.props.message) } }
-
-
事件传参
-
使用bind传参(不可直接传参,否则会立即调用)
onClick = {this.handleClick.bind(this,arg1,arg2...)}
-
如果不使用bind,那么就需要再包一层箭头函数
onClick = { ()=>{this.handleClick(arg1,arg2)} }
-
注意点:
- 无法通过事件处理函数return false来阻止默认行为,必须使用e.preventDefault()
组件状态state
创建state
如果需要定义组件的自定义属性,在组件的 constructor 构造函数里面去定义state
class Mycom extends React.Component {
constructor(props){
super(props)
//给this.state赋值一个对象,对象的属性就是组件的自定义属性
this.state = {
iNum:10
}
}
}
修改state
不能直接去修改state的值,否则数据无法驱动关联,需要使用 setState
setState方法接收一个参数,参数为一个对象,类似于小程序原生的setData。
setState方法是一个异步的方法
this.setState({
iNum:12
})
setState方法也可以接收参数为一个函数,函数的第一个参数为state,第二个参数为props
this.setState(function(state,props){
return {iNum:state.iNum+1}
})
//可用箭头函数简写成
this.setState(state=>({iNum:state.iNum+1}))
//补充一下箭头函数
function(state){
return {iNum:state.iNum+1}
}
//先去掉function关键字,加上go's to =>
(state)=>{
return {iNum:state.iNum+1}
}
//因为只有一个参数,接收参数的()可以省略
state=>{
return {iNum:state.iNum+1}
}
//因为函数内部直接是返回值,所以函数体可以省略return 并且省略{}
state=>{iNum:state.iNum+1}
//因为返回值是一个对象,对象本身的{}会被理解成函数体的{},所以需要再包一层()
state=> ({iNum:state.iNum+1})
当使用函数的形式去传参的时候,可以解决一些由于异步所带来的麻烦的问题
注意:
setState不要依赖this.state或者 this.props等值,因为react会将多个state和props值的变化合并成一起去执行来提高性能,所以如果我们在setState里面具有依赖的话,那么会带来一系列麻烦。
我们使用setState传递一个函数,函数默认有state和props两个形参,利用这两个形参去访问的话,就不会产生问题。
另外,setState还可以接收第二个参数,第二个参数为一个回调函数
经过上面的介绍大家都知道setState设置值是一个异步操作,如果我们需要在设置完state之后做一些逻辑操作,我们可以给setState传递第二个回调形式的参数。
像下面这样。
this.setState({
name:'ahreal'
},()=>{
console.log('state值修改成功,现在的name值为'+this.state.name)
})
概念:有状态组件和无状态组件
- 通过有无state来判断是否是有状态组件
- 通过function定义的组件都是无状态组件,通过class定义的组件可以是有状态组件,也可以是无状态组件
列表渲染
在React中,列表渲染使用数组的map方法,列表渲染需要使用key
let myArr = ['jack','allen','ahreal']
class MyCom extends React.Component{
render(){
return (
<ul>
{
myArr.map((item,index)=>{
return <li key={index}>{item}</li>
})
}
</ul>
)
}
}
条件渲染
两种方式做条件渲染
-
使用 if-else
-
直接返回jsx对象
class LoginState extends React.Component{ render(){ { if(this.props.isLogin){ return <p>已经登录</p> }else{ return <p>未登录</p> } } } }
-
返回不同的子组件
function Login(){ return <p>已经登录</p> } function Loginout(){ return <p>未登录</p> } class LoginState extends React.Component{ render(){ { if(this.props.isLogin){ return <Login></Login> }else{ return <Loginout></Loginout> } } } }
-
-
使用 &&
class Notice extends React.Component{
render(){
let {username,message} = this.props
{
return (
<div>
<p>欢迎您,{username}</p>
{message.length&&<p>您有{message.length}条信息</p>}
</div>
)
}
}
}
如果你想行为和结构区分得更彻底,那么你可以使用变量去保存jsx对象
比如:
function Login(){
return (
<button>登录</button>
)
}
function LoginOut(){
return(
<button>注销</button>
)
}
class LoginButton extends React.Component {
render(){
let isLogin = false
let btn
if(isLogin){
btn = <LoginOut></LoginOut>
}else{
btn = <Login></Login>
}
return (
<div>
<h1>登录状态</h1>
<btn></btn>
</div>
)
}
}
如果你想阻止一整个组件的渲染,那么你return null即可
双向绑定
React中没有帮我们封装类似vue中的V-model
所以我们需要自己去封装双向绑定
React对于绑定了state数据的表单组件称为受控组件
简单的实现步骤:
-
初始化默认的数据
constructor(props){ super(props) this.state = { //初始化了一个message,message作为双向绑定的数据媒介 message:this.props.message } }
-
给输出绑定message
render(){ return ( <div> <p>{this.state.message}</p> </div>) }
-
给输入绑定message,并且绑定change事件
render(){ return ( <div> <p>{this.state.message}</p> <input value={this.state.message} onChange={this.changeMessage.bind(this)}/> </div>) }
-
声明changeMessage方法,去动态修改state
changeMessage(e){ let newValue = e.currentTarget.value this.setState(()=>({message:newValue})) }
Ref的使用
和vue一样,React可以使用ref去标记某个dom对象,然后进行操作
使用步骤:
-
在constructor构造函数中创建一个ref对象
constructor(props){ super(props) this.myRef = React.createRef() }
-
在render中使用ref标记dom
render(){ return( <input ref={this.myRef}/> ) }
如此便可讲我们需要的dom使用ref标记了
当我们需要使用的时候可以通过
this.myRef.current
如此去拿到对应的ref所标记的dom
概念:受控组件和非受控组件
-
受控组件
组件的数据有和state进行关联的叫受控组件
-
非受控组件
组件数据没有和state进行关联的叫非受控组件
生命周期
图示

文字说明
constructor
构造函数,在组件初始化的时候会执行
render
这个方法会在componentWillMount方法之后执行,也会在state和props的数据发生变化的时候执行,所以这个方法在组件开始会执行,组件中途也会执行
componentDidMount
方法在组件挂载在页面之后执行
componentDidUpdate
组件更新之后执行
componentWillUnmount
在组件销毁之前执行
shouldComponentUpdate
在组件更新之前执行,这个方法可以决定组件是否更新,renturn true更新 false不更新。
如果返回true 组件会进入componentWillUpdate,然后进入render方法,接着进入componentDidUpdate方法
My-React-Cli
让我们来diy一个简单的react脚手架吧~
核心流程
-
初始化项目文件夹
npm init -y
-
下载react 和 react-dom
npm i react react-dom -S
-
下载webpack 和 webpack-cli
npm i webpack webpack-cli -D
-
下载babel的loader 和 babel核心
npm i babel-loader @babel/core -D
-
下载babel的react的preset
npm i @babel/preset-react -D
-
创建webpack.config.js文件,对loader进行配置
module:{ rules:[ { test:/\.js$/, //匹配到node_modules文件夹下面的就跳过loader的处理 exclude:/node_modules/, //使用的loader loader:'babel-loader', } ] }
-
根目录下创建babel配置文件 .babelrc ,配置react的babel-preset
{ "presets":[ "@babel/preset-react" ] }
至此,react简易脚手架已经搭建完成,当然也只是实现对react的js文件进行基本编译,当然一般来说还需配置css-loader,file-loader,dev-server等等,因为这些不在react脚手架的核心里面,所以就不过多赘述。
Router
web开发用的是react-router-dom
react-router-dom和react-router的区别: router-dom是对router的一种补充,新增了类似borswerRouter,Link等组件,安装了react-router-dom就无需再安装react-router
安装
-
下包
npm i react-router-dom -S
-
导包
import {HashRouter,Link,Route,Switch,Redirect} from 'react-router-dom'
其中:
- HashRouter定义哈希路由整体的容器组件
- Link单个标签,定义路由的链接,通过to属性来定义链接的地址
- Route单个标签,定义组件的容器标签,通过path定义和Link的to属性对应的地址,component属性定义链接对应的组件
- Switch多个Route标签外面的容器标签,如果需要定义404跳转和重定向跳转,需要用此标签包裹Route标签
- Redirect定义路由的重定向,通过from属性定义原始路由,通过to属性定义重定向路由
基本用法
react的路由直接在需要写router-view的页面引入即可
import {HashRouter,Route,Link} from 'react-router-dom'
class MyRouter extends React.Component {
render(){
return(
//HashRouter包裹在最外层作为路由组件的根组件
<HashRouter>
//Link组件就是一个a标签,to定义链接的地址
<Link to="/">router1</Link>
<Link to="/router2">router2</Link>
<Link to="/router3">router3</Link>
<hr/>
//Route就是一个表明链接对应的组件,同时表示路由组件渲染的位置,exact字段表示精准匹配
<Route path="/" exact component={router1}></Route>
<Route path="/router2" component={router2}></Route>
<Route path="/router3" component={router3}></Route>
</HashRouter>
)
}
}
exact表示这个Route是精准匹配的,react的路由默认是模糊匹配。
404页面
两步走
-
最末尾写一个Route标签,不写path属性,表示任意路径都能匹配上
<Route path="/" exact component={router1}></Route> <Route path="/router2" component={router2}></Route> <Route path="/router3" component={router3}></Route> <Route component={Page404}></Route>
-
外部包裹一层Switch组件,表示从上到下匹配,只能匹配一个路由
<Switch> <Route path="/" exact component={router1}></Route> <Route path="/router2" component={router2}></Route> <Route path="/router3" component={router3}></Route> <Route component={Page404}></Route> </Switch>
Redirect重定向组件
接收一个to和一个from
<Redirect from="/" to="/page1"></Redirect>
路由嵌套
直接在路由里面写路由子组件即可,当路由里面嵌套路由的时候,最外层不需要再包HashRouter
动态路由传值与取值
和Vue-router的动态路由一样,在定义路由path的时候使用**: 冒号** 。
步骤:
-
实现在路由的path声明参数,类似于函数的形参
import { BrowserRouter as Router, Switch, Route, useParams } from "react-router-dom"; function MyComponent(props){ return ( <Router> <Switch> //这里跳转的形参 <Route path="/:id" component={<Child/>}></Route> </Switch> </Router> ) }
-
跳转的时候在url对应位置写值
import { BrowserRouter as Router, Switch, Route, useParams } from "react-router-dom"; function MyComponent(props){ return ( <Router> //跳转的url直接在对应位置写参数 <Link to="/ahreal">点我传递ahreal</Link> <Link to="/allen">点我传递allen</Link> <Switch> //这里跳转的形参 <Route path="/:name" component={<Child/>}></Route> </Switch> </Router> ) }
-
对应的组件内通过useParasm获取参数
import { BrowserRouter as Router, Switch, Route, useParams } from "react-router-dom"; function Child(props){ //对应组件里面使用useParams取参数 let {name} = useParams return ( <div>传递过来的参数是:{name}</div> ) }
编程式导航
首先要知道,组件分为路由组件和非路由组件
- 路由组件,包裹在Router里并且经过匹配渲染出来的,称为路由组件
- 非路由组件,不是Router匹配渲染的。
区别
- 路由组件可以直接从this.props.history上拿到history
- 非路由组件无法直接拿到history,需要配合withRouter
实现步骤
-
路由组件:
//组件方法内部直接获取history jump(){ this.props.history.push(url) }
-
非路由组件:
//引入withRouter import { withRouter } from 'react-router-dom' //正常定义组件 class tabBar extends React.Component{ render(){ return ( <ul> <li onClick={this.handleClick.bind(this,'/home')}>首页</li> <li onClick={this.handleClick.bind(this,'/cate')}>分类</li> <li onClick={this.handleClick.bind(this,'/home')}>个人中心</li> </ul> ) } handleClick(url){ //如路由组件一样直接使用this.props.history this.props.history.push(url) } } //暴露的时候需要使用装饰者设计模式使用withRouter包裹一下组件,注意,一定要是最外层 export default withRouter(tabBar)
自定义路由(路由守卫)
我们上面说到Route组件可以接收一个Component组件,当path匹配上的时候,这个Route组件就会被渲染出来。我们还可以在路径匹配之后做一点事情,这一点类似于Vue中的路由守卫。
用到的还是Route这个组件,只不过这次组件不通过Component去传递数据,通过 render 属性。
import {Route} from 'react-router-dom'
function Custom(){
return (
<Route path="/index" Render={()=>{
//isLogin判断用户是否登录,如果登录了渲染首页,没有登录渲染登录
if(isLogin){
return <Index></Index>
}else{
return <Login></Login>
}
}}/>
)
}
还有一种常见的情况,使用 children 属性去帮我们做匹配渲染
匹配渲染
假设我们这里有个需求,我们要做一个按钮,处于index页面的时候,首页按钮高亮
import {Route} from 'react-router-dom'
function Custom(){
return (
//match为一个布尔值,由Route默认传入,告知匹配结果。
<Route path="/index" children={({match})=>{
return(
//当匹配成功的时候,添加类名active高亮,否则移除active
<Button className={ match ?'active':'' }>首页</Button>
)
}}></Route>
)
}
传值
父子之间
-
父传子
父组件在引用子组件的时候通过行内属性的形式直接传递,子组件通过this.props.属性名去获取传递过来的值
注意,这里和Vue一样,具有单向数据流的概念,不可以直接修改由父组件传递过来的props属性
-
子传父
父组件在自身声明一个接收数据的方法,将这个方法通过行内属性的形式传递给子组件,子组件接收这个方法调用,通过方法传参的形式传值
兄弟之间(bus模式)
- 使用react内部模块events中的EventEmitter类实例化一个bus对象,通过发布订阅的模式传值,组件中通过bus.emit来触发自定义事件,接收数据的组件通过bus.on来监听事件。
Redux
react中统一的状态管理
基本流程

**React Component:**react组件,是redux的使用者,获取redux值或者修改redux的值
**Store:**Store对象,数据仓库,组件需要获取或者修改数据需要通过store对象。
**Reducers:**负责数据的操作,定义Action的type对应的操作,定义数据仓库默认的初始值。
**Action Creators:**数据操作action,类似一个工单,组件如果需要修改redux的数据,需要创建一个action,action声明type和value,交给store去操作数据
安装部署redux
-
下包
npm install redux -S
-
创建store文件夹
-
创建index.js
import {createStore} from 'redux' import reducer from './reducer.js' let store = createStore(reducer) export default store
-
创建reducer.js
let defaultData = { } function reducer(state=defaultData,action){ return state } export default reducer
使用流程
**取值:**组件引入store对象,调用store对象的getState方法获取当前的数据仓库。
let storeData = store.getData()
**改值:**事先在reducer文件声明相应action type对应的处理方式,组件创建一个Action工单对象,调用store对象dispatch方法处理,如果我们需要监听值的变化的话,我们需要调用一下subscribe并且传递一个回调
-
在reducer文件里面声明处理方式
let reducer = (state=defaultData,action)=>{ if(action.type==='addNum'){ //深拷贝一个state,在拷贝的state上面对值进行操作 let newState = JSON.parse(JSON.stringify(state)) newState.Num += action.value //最后要返回这个新的state return newState } return state } export default reducer
-
在component组件里面制作一个工单,并且指派给store去处理
import store from '...' addNum(){ //创建一个action工单 let action = { type:'addNum', value:10 } //调用store的dispatch方法提交工单 store.dispatch(action) }
-
一般在constructor调用store的subscribe方法,监听store的变化
import store from '...' constructor(props){ super(props) //订阅redux的数据变化,当数据变化的时候会执行传递的回调 store.subscribe(this.handleStoreChange.bind(this)) } handleStoreChange(){ //一旦被执行了,就意味着redux数据变化了,需要重新获取一下 let storeData = store.getState() ... }
订阅以及退订
**订阅:store.subscribe(fn):**订阅store数据变化,当store的数据发生改变的时候,自动执行传入的回调。
**退订:**当调用store.subscribe去订阅store数据变化的时候,会返回一个退订的方法,我们只需要保存这个方法,在需要退订的时候调用一下即可。
**退订操作一定要在组件销毁前执行一下,**因为React中路由组件的切换是组件不断的构建-销毁的过程,这时候如果没有退订的话,每次切换来回一次组件,就会创建一次组件,执行一次constructor方法,就会订阅一次(我们一般把store.subscribe方法的调用放置在constructor中),**一直重复订阅的话会造成内存泄漏,**最后导致内存溢出
正常订阅以及退订的操作
import store from '...'
class Mycomponent extends React.Component {
constructor(porps){
super(props)
//调用store.subscribe订阅数据变化,同时将返回的退订方法保存在this上
this.unSubscribe = store.subscribe(handleChange)
}
...
componentWillUnmount(){
//调用一下退订的方法
this.unSubscribe()
}
}
React-Redux
首先你要明白的事情是,redux并不是专门为react服务的,他的运作完全不需要react,redux和其他第三方框架配合一样可以使用。
react-redux是对redux的一种扩充,能提高redux在react的性能,并且能够使代码更加的优雅。
react-redux依赖于react以及redux,所以使用react-redux之前,是基于你react以及redux已经安装完成。
安装以及使用
首先你应该先完成redux的安装并且部署好store以及reducer,参考笔记上面redux的安装部署。
-
下载 react-redux
npm install react-redux -S
-
在main.js使用react的context特性注入store
import store from './store/store.js' //导入react-redux的Provider import {Provider} from 'react-redux' //注入store ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
-
在组件里面导入connect
import { connect } from 'react-redux'
-
声明两个注入props的属性和方法
//这里的参数state是个默认参数,这里的state指的是store的state,而不是组件自身的state let mapStateToProps = state=>{ return{ //写属性 } } let mapDispatchToProps = dispatch=>{ return{ //写方法 } }
-
使用connect修饰组件并导出组件
//connect接收两个参数,是上面定义的两个方法,connect返回一个方法,方法接收组件本身作为参数 export default connect(mapStateToProps,mapDispatchToProps)(MyComponent)
与只使用redux的区别以及优点
- 无需在组件文件里面导入store
- 无需调用subscribe来监听数据变化,自动监听
- 如果组件是无状态组件(没有自己的state),那么组件可以直接改成使用function声明(之前只使用redux,由于redux的state需要在组件的constructor里面去获取,所以只能使用class的形式去定义组件),改用function声明组件,能提高性能
- 理由同上一点,使得代码更加简洁优雅
react及redux的chrome开发者工具
使用起来和vue dev-tools一毛一样,不过多赘述。
下载链接:pan.baidu.com/s/1Iq-J3u-2… 提取码:f7dg
安装流程:
- 网盘里面一共有两个文件夹,分别是react开发者工具和redux开发者工具,下载。
- 下载完成之后,打开chrom菜单,更多工具,扩展程序。
- 右上角又一个开发者模式,请打开。
- 打开开发者模式以后,选择左上角加载已解压的扩展程序,选择刚才下载的两个文件夹就OK。
注意,redux的开发者工具不是开箱即用,需要在代码里面,创建store的时候加上一句
//原本只传递一个参数reducer,现在多传递一个参数,复制过去即可。
let store = createStore(reducer,window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
插槽
官方不叫他插槽,官方叫他组合。
我认为他和效果和Vue中插槽的概念非常接近,而且插槽的话对于这一块的知识点的描述更加形象。
用法
React的props可以传递一个JSX对象,通过{this.props.XX}去使用外层传递进来的JSX对象
//定义一个我们即将传递进来的子组件
function hello(props){
return <h1>Hello world</h1>
}
//定义一个props接受子组件的组件
class MyComponent extends React.Component{
render(){
return(
<div>
<h1>你好世界</h1>
{this.props.children}
</div>
)
}
}
//使用props将子组件传递进去
ReactDOM.render(<MyComponent children={<hello/>} />, document.getElementById('root'));
概念:React中的副作用操作
数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。
一般说到函数的副作用指的是,函数体内部做了一些和参数返回值无关的操作,比如说访问全局变量,修改全局变量等等一些和本函数无关的操作称为函数的副作用。
Hook
Hook是React16.8新推出的一个特性,可以在使用function定义组件的时候,使用state等其他react的特性。
Hook是一种渐进策略,也就是说即使你的项目之前没有使用hook,你完全可以选择使用或者不使用hook,
Hook不可以在class组件中使用。
useState
这就是一个hook,能够帮助你在function组件定义State。
像这样:
这是一个计步器,每次点击button的时候count这个state属性++
//导入React使用默认导入,useState使用的是具名导入,这个和react封装的暴露策略有关。
import React,{ useState } from 'react'
function Example() {
// useState本身是一个方法,方法接收一个参数,方法返回一个数组,使用解构赋值的形式接收并且声明。
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
我们来具体讲一下 useState
useState本身是一个方法,方法接收一个参数,这个参数作为我们要定义的 state的初始值。
比如说我们:
//这里是es6的解构赋值特性
const [count, setCount] = useState(0);
等价于你在class组件中:
constructor(props){
super(props)
this.state:{
count:0
}
}
那么这个setCount是什么呢?setCount是用来修改Count这个State的方法。
比如说我们这样:
setCount(20)
等价于你在class组件中:
this.setState({
count:20
})
我们使用hook的setCount和React自带的setState区别在于,setState是合并state,和setCount是替换值,毕竟setCount只为一个state服务。
这个设置state初始值,和调用方法修改state并不难。
当然如果你有多个state,那么你只需调用多次useState即可。
useEffect
useEffect这个Hook使你的function组件具有生命周期的能力!能够使用componentDidMount,componentDidUpdate,componentWillUnmount这三个生命周期函数。
最基本的使用useEffect
我们在上一个例子的计步器的基础上,将Count绑定到浏览器title。
import React,{ useState,useEffect } from 'react'
function Example() {
//这里最好使用const来做声明关键字,防止我们意外直接修改state而没有通过set方法去设置。
const [count, setCount] = useState(0);
//useEffect方法接收一个参数,参数为一个函数,这个函数会在Dom渲染的时候调用,包括第一次渲染。
//这里useEffect接收的函数在react中称为副作用函数。
useEffect(()=>{
document.title = `你点击了${count}次!`
})
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
下面我们再进行与Class组件类比:
比如你这样去写useEffect这个hook
useEffect(()=>{
document.title = `你点击了${count}次!`
})
在Class组件中等价于:
//组件挂载到页面上后的生命周期钩子
componentDidMount(){
document.title = `你点击了${this.state.count}次!`
}
//组件State更新之后的生命周期钩子
componentDidUpDate(){
document.title = `你点击了${this.state.count}次!`
}
这里useEffect将两个生命生命周期合二为一了,简化了代码,但同时也限制了逻辑代码的灵活性。
对于此,React官方文档给到的解释是:
注意,在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。
这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。
使用useEffect实现componentWillUnmount
上面只介绍了如何实现componentDidMount,componentDidUpdate,那如何实现componentWillUnmount这个生命周期呢?
在副作用函数中返回一个函数即可,返回的这个函数就是componentWillUnmount。
useEffect(()=>{
document.title = `你点击了${count}次!`
let UnmountFn = ()=>{
alert('组件被卸载了!')
}
return UnmountFn
})
返回的这个函数只有在组件卸载的时候才会被执行。
性能优化
在class组件中,componentDidUpdate在每次数据发生改变的时候都会被执行,我们常用一个判断需要的值的改变来节约性能,就像这样:
我们需要在name值发生改变的时候重新赋值给fullname
componentDidUpdate(prevProps,prevState){
//当发生改变的值是state的name的时候,才继续往下执行
if(prevState.name != this.state.name){
this.setState((state)=>{
return {
fullname:'黄'+ state.name
}
})
}
}
在effect中,我们可以通过第二次参数的形式决定这个hook执行与否
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]); // 仅在 props.friend.id 发生变化时,重新订阅
更加准确的理解
我们上面类比class组件的生命周期来解释useEffect的作用,方便理解,但是我个人认为还不够准确。
- class组件的componentWillUnmount是在组件卸载的时候执行,而上文将其类比的useEffect返回值函数是在state每次发生改变的时候都会执行,只不过是在Hook挂载的时候不会执行。
- 观察的点不同,组件挂载我认为和Hook挂载不同,即使componentDidMount和useEffect在看起来貌似是同一时刻执行,componentDidMount观察的是组件的DOM挂载,而useEffect观察的是State创建或者更新(从另外一个维度理解,state创建和更新本质上都是赋值。)
Hook的使用规则
从官方文档搬来的,我觉得总结的很好。
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。(还有一个地方可以调用 Hook —— 就是自定义的 Hook 中,我们稍后会学习到。)
自定义Hook
我们可以将一些相同的逻辑抽离出来,注入组件中。
自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。
一个自定义的Hook例如:
import React, { useState, useEffect } from 'react';
//这是一个可以通过friendID去判断是否在线的自定义Hook
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
//这里只是返回了isOnline这个属性,当然你连setIsOnline也可以返回出去,可以让组件调用的时候接收这个方法,在必要的时候自行修改Hook的State
return isOnline;
}
自定义Hook看起来特别的像一个函数的使用,传递一个参数,得到一个结果。
定义Hook和函数不同的是,Hook可以利用起生命周期钩子,以及有Hook内部自己的数据。
注意:自定义Hook每次调用的时候会创建一个新的State作用域(如果你有使用useState的话),所以不用担心多次调用Hook导致State互相影响
自定义Hook服用逻辑,但不服用State,作用域还是独立的
如何在组件中使用这个自定义Hook呢?
function FriendStatus(props) {
const isOnline = useFriendStatus(props.friend.id);
if (isOnline === null) {
return 'Loading...';
}
return isOnline ? 'Online' : 'Offline';
}
用一个变量保存即可,无需Hook关心内部的逻辑,内部帮你绑定了生命周期,帮你创建State作用域,这些你都不用关心,直接用即可。
代码分割(待补充)
React的懒加载
- 动态 import( )
- React.lazy( )
- 基于路由
Context
Context,翻译过来就是上下文的意思,我们有时候会出现这么一个情况,某一些特定的状态,我只想在一个组件树上共享,我既不想一层一层向下传值,又不想他是一个全局变量(毕竟其他组件树不需要这些状态),那么Context就是为了解决这个情况设计。
挂载(生产)
我们把需要传递下去的值挂载在组件树的最顶端,挂载完以后就可以在挂载点的子孙组件访问
使用(消费)
分为两种情况:函数组件和类组件
函数组件:
函数组件需通过Context.Consumer获取。
类组件:
类组件重写自身类的contextType,将其指向需要获取值得Context即可。
使用流程
-
我们先来创建一个Context对象,这里 MyContext 就是一个Context对象
const MyContext = React.createContext('ahreal')
-
接着我们把Provider挂在我们需要传递特性的组件树最顶端。(挂载)
<MyContext.Provider> <Father> <Son></Son> </Father> </MyContext.Provider>
-
我们需要取值的时候分两种。(取值)
-
类组件(假设son组件是一个类组件)
class Son extends React.Component{ render(){ let value = this.context } } //需要重写类的contextType,当然这个MyContext如果有需要的话要从定义的地方导入 Son.contextType = MyContext
-
函数组件
function Son(props){ return( <MyContext.Consumer> { value = > ( <div> ... </div> ) } </MyContext.Consumer> ) }
-
Cli(待补充)
...