React使用(常更)
一、React简介
1.1特点
轻量
原生
易扩展
不依赖宿主环境
渐进式
单向数据流
用JS代码声明界面
组件化
1.2使用
1.直接在页面上使用React,引用下面JS
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script>
//React.createElement
const element = React.createElement("h1", {}, "Hello, world");
ReactDOM.render(element, document.getElementById("root"));
</script>
<script type="text/babel">
//JSX
const element = <h1>Hello, world!</h1>;
ReactDOM.render(element, document.getElementById("root"));
</script>
</body>
创建元素用React.createElement或JSX,JSX用babel转义。
2.脚手架
官方:create-react-app
第三方:next.js、umijs
二、React核心概念
2.1JSX
2.2组件和组件属性
创建一个组件
注意: 组件名称必须以大写字母开头。
1.函数组件
返回一个 React 元素
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
对于函数组件,属性会作为一个对象的属性,传递给函数的参数
2.类组件
必须继承React.Component,必须提供render函数,返回一个 React 元素,用于渲染组件
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
对于类组件,属性会作为一个对象的属性,传递给构造函数的参数
注意:组件的属性应该使用小驼峰命名。
组件无法改变自身的属性,React中的数据,自顶而下流动, 单向数据流。
2.3组件的状态
1.什么是组件状态?
- 组件状态是指组件可以自行维护的数据。
- 组件状态仅在类组件中有效。
- 状态本质上是类组件的一个属性,一个对象。
2.状态初始化
2.1在类组件的构造函数中添加state,状态是必须要初始化的,否则为undefined。
constructor(props){
super(props);
this.state = {
n:this.props.number
}
}
2.2在类组件中直接添加state,该方法属于JS Next 语法,目前处于实验阶段。
export default class Tick extends Component {
state = {
left: this.props.number,
n: 123
}
constructor(props) {
super(props);
}
render() {
return (<div></div>)
}
}
3.状态的变化
改变组件状态不能直接用this.state.XXX来改变,React无法监控到状态发生了变化。
改变状态要用this.setState(param),该函数为父类提供的构造函数,一旦调用该函数,组件会重新渲染。
- param是一个对象,函数执行时,param会混入到this.state对象中。
- 即如果param中有属性与this.state中的属性相同,那么this.state中的属性会被覆盖。
- 如果属性不相同,则param中的属性会添加到this.state中
// 组件Tick
import React, { Component } from 'react';
export default class Tick extends Component {
constructor(props){
super(props);
this.state = {
n:this.props.number
}
this.timer = setInterval(()=>{
this.setState({
n:this.state.n-1
})
},1000);
}
render() {
return (
<>
<h1>{this.state.n}</h1>
</>
)
}
}
4.案例解析
组件Test.js中,组件A中的状态数据n数据流向依次是A->B->C
import React, { Component } from 'react'
export default class A extends Component {
state = {
n: 123
}
constructor(props) {
super(props);
setInterval(() => {
this.setState({
n: this.state.n - 1
})
}, 1000);
}
render() {
console.log("A组件重新渲染了")
return (
<div>
<B n={this.state.n} />
</div>
)
}
}
function B(props) {
return <div>
B组件:{props.n}
<C n={props.n} />
</div>
}
function C(props) {
return <div>
C组件:{props.n}
</div>
}
- 该案例中组件A数据变化导致组件B组件C数据依次变化。
- 这说明数据属于谁,谁就有权利更改,该案例中A自己更改了数据,假如组件A还有一个父组件,但是父组件是不能修改A数据的。
- 该案例同样说明了React数据流是自顶向下流动的。
5.组件的数据来源
目前组件中数据的来源有如下两种途径。
- props:该数据是由组件的使用者传递的数据,所有权不属于自身,所以组件无法改变数据。
- state:该数组是由组件自身创建的,所有权属于组件自身,因此组件有权改变该数据。
2.4组件事件
在React中,组件的事件,本质上就是一个属性
按照之前React对组件的约定,由于事件本质上是一个属性,因此也需要使用小驼峰命名法
1、内置组件事件
import React from 'react';
import ReactDOM from 'react-dom';
function handleClick(e){
console.log("点击了!");
}
const btn = <button onClick={handleClick} onMouseEnter={(e)=>{
console.log("鼠标进入了!",e);
}}>按钮</button>
ReactDOM.render(btn, document.getElementById('root'));
2、自定义组件事件
//TickControl.js
import React, { Component } from 'react'
import MyComp from './MyComp'
export default class TickControl extends Component {
state = {
isOver: false
}
render() {
let status = "正在倒计时";
if(this.state.isOver){
status = "倒计时完成";
}
return (
<div>
<MyComp onOver={()=>{
this.setState({
isOver: true
})
}} time={6} />
<h2>{status}</h2>
</div>
)
}
}
//MyComp.js
import React, { Component } from 'react'
export default class MyComp extends Component {
constructor(props){
super(props);
this.state = {
time: props.time
}
const timer = setInterval(()=>{
this.setState({
time: this.state.time-1
})
if(this.state.time === 0){
clearInterval(timer);
this.props.onOver && this.props.onOver();
}
},800);
}
render() {
return (
<div>
倒计时: {this.state.time}
</div>
)
}
}
如果一个函数逻辑比较复杂的话,我们推荐先将函数封装好,直接绑定在属性上。
//TickControl.js
export default class TickControl extends Component {
state = {
isOver: false
}
handleOver(){
this.setState({
isOver: true
})
}
render() {
let status = "正在倒计时";
if(this.state.isOver){
status = "倒计时完成";
}
return (
<div>
<MyComp onOver={this.handleOver} time={6} />
<h2>{status}</h2>
</div>
)
}
}
但这里就有问题了(this的指向问题,在类组件中出现)。
此时handleOver是在当前对象原型上
react中,如果没有特殊处理,在事件处理函数中,this指向undefined
解决方法:
1.使用bind函数,绑定this,将方法绑定在当前对象上。
constructor(props){
super(props);
this.handleOver = this.handleOver.bind(this);
}
handleOver(){
this.setState({
isOver: true
})
}
或者
render() {
let status = "正在倒计时";
if(this.state.isOver){
status = "倒计时完成";
}
return (
<div>
<MyComp onOver={this.handleOver.bind(this)} time={6} />
<h2>{status}</h2>
</div>
)
}
2.箭头函数
我们前面写的就是,只是如果函数过于复杂,不方便阅读。那么我们进行改进。
handleOver = () => {
console.log(this);
this.setState({
isOver: true
})
}
2.5深入setState
Vue 的渲染是异步执行的,但是React的对状态的改变,可能是异步的也可能是同步的。
如果改变状态的代码处于某个HTML元素的事件中,则其是异步的,否则是同步
开发的时候始终当成异步的,然后使用回调函数进行处理。
this.setState(() => {
//改变状态
n: xxx,
}, () => {
//状态改变完成之后触发,改回调运行在render之后
xxxx
})
若遇到某个事件,需要同步调用多次,需要使用函数的方式得到最新状态。
this.setState(cur => {
//改变状态
return {
n: xxx
}
}, () => {
//状态改变完成之后触发,改回调运行在render之后
xxxx
})
React会对异步的setState进行优化,将多次setState进行合并(将多次状态改变完成后,再统一对state进行改变,然后触发render)
最佳实践
- 把所有的setState当作是异步的
- 永远不要信任setState调用之后的状态
- 如果要使用改变之后的状态,需要使用回调函数(setState的第二个参数)
- 如果新的状态要根据之前的状态进行运算,使用函数的方式改变状态(setState的第一个函数)
2.6生命周期
生命周期仅存在于类组件中,函数组件每次调用都是重新运行函数,旧的组件即刻被销毁
React16.3版本以前的生命周期
React16.3版本以后的生命周期
注意:componentWillMount()、componentWillReceiveProps()、componentWillUpdate()在React16.3新版本中都被移除;
旧版生命周期
1、constructor
- 同一个组件对象只会创建一次
- 不能在第一次挂载到页面之前,调用setState,为了避免问题,构造函数中严禁使用setState
2、componentWillMount(新版被移除)
- 正常情况下,和构造函数一样,它只会运行一次
- 可以使用setState,但是为了避免bug,不允许使用,因为在某些特殊情况下,该函数可能被调用多次
3、render
- 返回一个虚拟DOM,会被挂载到虚拟DOM树中,最终渲染到页面的真实DOM中
- render可能不只运行一次,只要需要重新渲染,就会重新运行
- 严禁使用setState,因为可能会导致无限递归渲染
4、componentDidMount
- 只会执行一次
- 可以使用setState
- 通常情况下,会将网络请求、启动计时器、订阅消息等一开始需要的操作,书写到该函数中
组件进入活跃状态
5、componentWillReceiveProps(新版被移除)
- 即将接收新的属性值
- 参数为新的属性对象
- 该函数可能会导致一些bug,所以不推荐使用
6、shouldComponentUpdate
- 指示React是否要重新渲染该组件,通过返回true和false来指定
- 默认情况下,会直接返回true
- 可以做性能优化
shouldComponentUpdate(nextProps, nextState) {
if (this.props.n === nextProps.n && this.state.n === nextState.n) {
return false;
}
return true;
}
7、componentWillUpdate(新版被移除)
组件即将被重新渲染
8、componentDidUpdate
往往在该函数中使用dom操作,改变元素
9、componentWillUnmount
通常在该函数中销毁一些组件依赖的资源,比如计时器、取消订阅
新版生命周期
版增加了两个钩子:
getDerivedStateFromProps
通过参数可以获取新的属性和状态
该函数是静态的
该函数的返回值会覆盖掉组件状态
该函数几乎是没有什么用
getSnapshotBeforeUpdate
真实的DOM构建完成,但还未实际渲染到页面中。
在该函数中,通常用于实现一些附加的dom操作
该函数的返回值,会作为componentDidUpdate的第三个参数
React官方为什么要改它的生命周期?
React16.3之前,用户在使用React的时候有一种反模式,组件的props和state同时控制数据(实现原理是在componentWillReceiveProps里监听props的变化,props是变化时使用this.setSate同步props和state的数据),数据来源不再单一。
React官方认为,某个数据的来源必须是单一的,否则很容易出现Bug。
componentWillReceiveProps生命周期里用户可以操作this,所以在React16.3之后官方移除了componentWillReceiveProps,使用静态属性getDerivedStateFromProps(该属性几乎没什么用)来替代。
2.7传递元素内容
1.传递属性
import React from 'react';
import ReactDOM from 'react-dom';
import Comp from "./Comp"
ReactDOM.render(<Comp html={<h1>传递的元素</h1>} />, document.getElementById('root'));
//Comp组件
export default function Comp(props) {
return (
<div className="comp">
<h1>组件自身的内容</h1>
{props.html || <h1>默认值</h1>}
</div>
)
}
2.语法糖
import React from 'react';
import ReactDOM from 'react-dom';
import Comp from "./Comp"
ReactDOM.render((<Comp>
<h1>传递的元素</h1>
</Comp>), document.getElementById('root'));
//Comp
export default function Comp(props) {
return (
<div className="comp">
<h1>组件自身的内容</h1>
{props.children || <h1>默认值</h1>}
</div>
)
}
2.8表单
三、React高级指引
3.1属性默认值和类型校验
属性默认值:
通过一个静态属性defaultProps告知react属性默认值
static defaultProps = {
xxx: xxx
}
属性类型检查:
使用库:prop-types
对组件使用静态属性propTypes告知react如何检查属性
static propTypes = {
xxx: xxx
}
3.2HOC高阶组件
HOF:高阶函数,以函数作为参数,并返回一个函数,例如(compose、bind、防抖节流)
HOC: 高阶组件,以组件作为参数,并返回一个组件。
react-redux 也是使用 HOC, connect 将应用 store 的值传递到“已连接” 的组件。它还会执行一些错误检查和组件生命周期优化,如果手动完成将导致编写大量重复代码。
3.3ref
使用场景:直接使用dom元素中的某个方法,或者直接使用自定义组件中的某个方法
1.ref作用于内置的html组件,得到的将是真实的dom对象
2.ref作用于类组件,得到的将是类的实例
3.ref不能作用于函数组件,函数组件里面的react 元素才可以使用的。
ref不再推荐使用字符串赋值,字符串赋值的方式将来可能会被移出
对象
export default class MyRef extends Component {
constructor(props) {
super(props);
this.test = React.createRef(); //创建一个 ref 对象
// 这个对象 也就是 { current : null} 那么就可以通过 { current : null} 替换 React.createRef() 也是可以的。
}
getFac= (e)=> {
this.test.current.focus();
}
render(){
return (
<div>
<input type="text" ref={this.test} />
<button onClick={this.getFac}>聚焦</button>
</div>
)
}
}
函数
函数的调用时间:
- componentDidMount的时候会调用该函数,在componentDidMount事件中可以使用ref
- 如果ref的值发生了变动(旧的函数被新的函数替代),分别调用旧的函数以及新的函数,时间点出现在componentDidUpdate之前
旧的函数被调用时,传递null
新的函数被调用时,传递对象 - 如果ref所在的组件被卸载,会调用函数
谨慎使用
能够使用属性和状态进行控制,就不要使用ref。
使用场景:
1.调用真实的DOM对象中的方法 (如聚焦、播放器、画布)
2.某个时候需要调用类组件的方法
ref转发
使用forwardRef方法
在做高阶函数组件的时候有用
3.4Context上下文
react中的上下文的特点:
- 当某个组件创建了上下文后,上下文中的数据,会被所有后代组件共享,如概念所说,子组件与父组件中都可以使用上下文的数据。
- 如果某个组件依赖了上下文,会导致该组件不再纯粹(外部数据仅来源于属性props),毕竟组件的数据都是都是一层一层往下传的,如果突然组件的数据来自祖先组件,这会给组件维护起来带来一定的麻烦。
- 一般情况下,用于第三方组件(通用组件)
四、HOOK
4.1HOOK简介
HOOK是React16.8.0之后出现
组件:无状态组件(函数组件)、类组件
类组件中的麻烦:
1.this指向问题
2.繁琐的生命周期
3.其他问题
HOOK专门用于增强函数组件的功能(HOOK在类组件中是不能使用的),使之理论上可以成为类组件的替代品。
官方强调:没有必要更改已经完成的类组件,官方目前没有计划取消类组件,只是鼓励使用函数组件
HOOK(钩子)本质上是一个函数(命名上总是以use开头),该函数可以挂载任何功能
HOOK种类:
useState
useEffect
其他…
4.2State Hook
State Hook是一个在函数组件中使用的函数(useState),用于在函数组件中使用状态。
- 函数有一个参数,这个参数的值表示状态的默认值
- 函数的返回值是一个数组,该数组一定包含两项
第一项:当前状态的值
第二项:改变状态的函数
一个函数组件中可以有多个状态,即可以多次使用useState,这种做法非常有利于横向切分关注点。
注意的细节
-
useState最好写到函数的起始位置,便于阅读
错误示范:
import React, { useState } from 'react' // 代码写在function外部,是不行的 const [data, setData] = useState(0) export default function TestStateHook() { return ( <div> <button onClick={() => { setData(data - 1) }}> - 1 </button> <span> {data} </span> <button onClick={() => { setData(data + 1) }}> + 1 </button> </div> ) }
-
useState严禁出现在代码块(判断、循环)中
-
useState返回的函数(数组的第二项),引用不变(节约内存空间)
import React, { useState } from 'react'
export default function TestStateHook() {
const [data, setData] = useState(0);
//定义一个数组来装setData 函数
const arr = [setData]
return (
<div>
<button onClick={() => {
setData(data - 1)
arr.push(setData)
console.log( arr[0] === arr[1]); //打印true
}}> - 1 </button>
<span> {data} </span>
<button onClick={() => {
setData(data + 1)
arr.push(setData)
}}> + 1 </button>
</div>
)
}
-
使用函数改变数据,若数据和之前的数据完全相等(使用Object.is比较),不会导致重新渲染,以达到优化效率的目的。
-
使用函数改变数据,传入的值不会和原来的数据进行合并,而是直接替换。
import React, { useState } from 'react'
interface obj {
a?: number,
b?: number
}
export default function TestStateHook() {
const [data, setData] = useState<obj>({ a: 123, b: 321 });
console.log('函数初始化')
return (
<div>
<p>数字A: {data.a} </p>
<p>数字B: {data.b} </p>
<button onClick={() => {
setData({ b: 42344 })
}}>改变B </button>
</div>
)
}
-
如果要实现强制刷新组件的情况:
类组件:使用forceUpdate函数, vue2里面也有一个$foreUpdate来强制刷新组件
函数组件:使用一个空对象的useState,如: setData({}). 因为传入的值是一个引用值,传入一个空对象的引用地址和原来的对象都是不一样的,传入一个空的数组或者函数也是可以做到的。
-
如果某些状态之间没有必然的联系,应该分化为不同的状态,而不要合并成一个对象。
-
和类组件的状态一样,函数组件中改变状态可能是异步的(在DOM事件中),多个状态变化会合并以提高效率,此时,不能信任之前的状态,而应该使用回调函数的方式改变状态。
如果状态变化要使用到之前的状态,尽量传递函数。
练习: 1. Base state: 0 Queue: [1, 1, 1] Expected result: 1 2. Base state: 0 Queue: [n => n+1, n => n+1, n => n+1] Expected result: 3 3. Base state: 0 Queue: [5, n => n+1] Expected result: 6 4. Base state: 0 Queue: [5, n => n+1, 42] Expected result: 42
4.3Effect Hook
Effect Hook:用于在函数组件中处理副作用。
副作用:
- ajax请求
- 计时器
- 其它异步操作
- 更改真实DOM对象
- 本地存储
- 其它会对外部产生影响的操作
函数:useEffect,该函数接收一个函数作为参数,接收的函数就是需要进行副作用操作的函数。
常见的useEffect使用方式,三种
//每次更新之后都要执行 相当于componentDidUpdate
// 如果是请求,会一直请求,需要注意一下
useEffect(() => {
// 副作用函数的内容
})
//初始化页面时 只执行第一次 相当于componentDidMount
useEffect(() => {
// 副作用函数的内容
}, [])
//1.初始执行一次; 2.每次依赖项的值变化时执行
useEffect(() => {
// 副作用函数的内容
}, [依赖项]) //依赖项可以有多个
细节
-
副作用函数的运行时间点,是在页面完成真实的UI渲染之后。因此它的执行是异步的,并且不会阻塞浏览器。
与类组件中componentDidMount和componentDidUpdate的区别:
-
componentDidMount和componentDidUpdate,更改了真实DOM,但是用户还没有看到UI更新,同步的。
-
useEffect中的副作用函数,更改了真实DOM,并且用户已经看到了UI更新,异步的。
-
每个函数组件中,可以多次使用useEffect,但不要放入判断或循环等代码块中。
-
useEffect中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数。
该函数运行时间点:
1.首次渲染组件不会运行
2.在每次运行副作用函数之前(在下次运行效果之前清理上一次渲染中的效果)
3.组件被销毁时一定会运行
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Specify how to clean up after this effect:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
-
useEffect函数,可以传递第二个参数
第二个参数是一个数组数组中记录该副作用的依赖数据
当组件重新渲染后,只有依赖数据与上一次不一样的时,才会执行副作用
所以,当传递了依赖数据之后,如果数据没有发生变化
副作用函数仅在第一次渲染后运行
清理函数仅在卸载组件后运行 -
副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化。
import React,{useState,useEffect} from 'react'
export default function App() {
const [n,setN] = useState(0);
useEffect(() => {
setTimeout(()=>{
console.log(n);//每次渲染都会执行
},5000)
})
return (
<div>
<span>{n}</span>
<button onClick={()=>{
setN(n+1)
}}>+</button>
</div>
)
}
- 副作用函数在每次注册时,会覆盖掉之前的副作用函数,因此,尽量保持副作用函数稳定,否则控制起来会比较复杂。
4.4自定义Hook
State Hook: useState
Effect Hook:useEffect
自定义Hook:将一些常用的、跨越多个组件的Hook功能,抽离出去形成一个函数,该函数就是自定义Hook,自定义Hook,由于其内部需要使用Hook功能,所以它本身也需要按照Hook的规则实现:
- 函数名必须以use开头
- 调用自定义Hook函数时,应该放到顶层
4.5Context Hook
用于获取上下文数据
使用context:
import React from 'react'
const context = React.createContext()
const Provider = context.Provider
const Consumer = context.Consumer
function Test() {
return (
<Consumer>
{ value => <h1>Test, 上下文的值:{ value }</h1> }
</Consumer>
)
}
export default function () {
return (
<div>
<Provider value="hello context hook">
<Test />
</Provider>
</div>
)
}
使用Context Hook:
import React, { useContext } from 'react'
const context = React.createContext()
const Provider = context.Provider
function Test() {
// 直接获取
const value = useContext(context)
return <h1>Test, 上下文的值:{ value }</h1>
}
export default function () {
return (
<div>
<Provider value="hello context hook">
<Test />
</Provider>
</div>
)
}
4.6Ref Hook
用以下三种方式创建 Ref 都可以
const myRef = React.createRef()
const myRef2 = React.useRef() // Ref Hook 的方式
const myRef3 = {current: undefined}
(1)除了可以进行ref操作以外,还能进行数据存储的操作,即相当于在函数式组件中拥有了this
(2)ref的改变不会引起组件的重新渲染
1、引入
import React, { Component,useRef } from ‘react’;
2、使用ref
const xx=useRef(任意内容);
在标签上: ref={xx}
3、操作ref
xx.current.dom操作
4、若使用ref容器存储数据,即当作this来使用
const xx=useRef(初始数据);
改变数据:xx.current=数据;
获取数据:xx.current
Ref Hook用于对ref的缓存,通常用于对某个函数组件节点中某个局部引用进行缓存
五、Router
1.可以根据不同的地址,展示不同的组件
2.可以无刷新的跳转页面
具备上面两个功能的可以称之为路由
React Router
1.react-router:路由核心库,包含诸多和路由相关的核心代码
2.react-router-dom:利用路由核心库,结合实际页面,实现跟页面路由密切相关的功能
两种路由模式
- Hash Router 哈希路由
根据url地址中的哈希值来确定显示的组件(原因:哈希值改变,不会刷新页面)
这种模式兼容新最好
- Broswer History Router 浏览器历史记录路由
HTML5出现后,新增了History Api 后,浏览器拥有了改变路径二不刷新页面的方式
History表示浏览器的历史纪录,它使用栈的方式存储
history.length:获取栈中的数据量
history.pushState()
参数1:附加的数据,自定义的数据,可以是任何类型
参数2:页面标题,目前大部分浏览器不支持
参数3:新的地址
history.replaceState()
参数1:附加的数据,自定义的数据,可以是任何类型
参数2:页面标题,目前大部分浏览器不支持
参数3:新的地址
根据页面的路径来决定渲染哪个组件
路由组件
- Router组件
它本身不会做任何展示,仅提供路由模式配置,另外,该组件会产生一个上下文,上下文中会提供一些使用的对象和方法,供其他相关组件使用
1.HashRouter :该组件,使用hash模式匹配
2.BrowserRouter:该组件使用了BrowserHistory模式匹配
通常情况下,Router组件只有一个,该组件包裹整个页面
- Route组件
根据不同的地址,展示不同的组件
两个重要属性
1.path:匹配的路径
默认情况下不区分大小写, 可以添加boolean属性sensitive来控制是否区分大小写
默认情况下,只匹配根目录,加上boolean属性exact进行精确匹配
如果不写path的话,会匹配所有路径
2.component:匹配成功后显示的组件
3.children:
传递React元素,无论是否匹配,一定会显示children,并且会忽略component属性
传递一个函数,该函数有多个参数,这些参数来自于上下文,该函数返回React元素,则一定会显示返回的元素,并且忽略component属性
Route组件可以写在任何地方,只要保证他是Router组件的后代元素
六、Redux
https://www.redux.org.cn/