React
crossorigin
-
提示跨域错误的详细信息(不写默认没有详细信息)
-
console.dir()可以显示一个对象的所有属性和方法
React.createElement
- 创建一个 React 元素,称作虚拟 DOM,本质上是一个对象
- 参数 1:元素类型,如果是字符串,一个普通的 HTML 元素
- 参数 2:元素的属性,一个对象
- 后续参数:元素的子节点
JSX
- JS 的扩展语法,需要使用 babel 进行转义。
yarn
yarn 最常用最基础的命令和 npm 对比
npm init / yarn init 初始化
mkdir 文件名 / md 文件名
npm install / yarn 或 yarn install 安装依赖
npm install package –save-dev / yarn add package –dev 安装某个依赖
npm uninstall package –save-dev / yarn remove package –dev 卸载某个依赖
npm run dev 或 npm start / yarn run start 或 yarn start 运行
react 脚手架
- create-react-app 脚手架(yarn create 会把脚手架名称 react-app 补全,后面接的是自定义名称)
-
yarn create react-app react-learn
-
npm install -g create-react-app
-
create-react-app my-app
-
启动工程 yarn start
- 凡是使用 JSX 的文件,必须导入 React(注意:React 工程里面的.js 也可以写成.jsx)
JSX
- 每一个 jsx 的表达多需要一个根节点,可以用<>代替根节点
const h1 = (
<>
<div>123</div>
</>
);
ReactDOM.render(h1, document.getElementById('root'));
//其中<></>是一个语法糖,是<React.Fragment></<React.Fragment>
const a = 1234,
b = 4321;
const div =
(
<div>
{a} * {b} = {a * b}
</div>
) - 上面的jsx语法就相当于下面直接用React.createElement;
const a = 1234,
b = 4321;
const div = React.createElement('div', {}, `${a} * ${b} = ${a * b}`);
ReactDOM.render(div, document.getElementById('root'));
复制代码
什么是 JSX
- Facebook 起草的 JS 扩展语法
- 本质是一个 JS 对象,会被 bable 编译,最终会被转化为 createElement
- 每个 JSX 表达式有且仅有一个根节点
- React.Fragment
- 每个 JSX 元素必须结束(XML 规范)
在 JSX 中嵌入表达式
- 将表达式作为内容的一部分
- null,undefined,false,true 不会显示
- 普通对象,不可以作为子元素
- 可以放置 React 元素对象(var span = 一个 span 元素)
- 如果是数组,会把数组元素当作子元素渲染出来
- 将表达式作为元素属性(
- {i}
- 属性使用小驼峰命名法(let cls = “image” <li key={i} className={cls} style={{marginLeft:’250px’}}>{i}
- jsx 中 className 相当于 css 中 class
- 防止注入攻击
- 自动编码
- dangerouslySetInnerHTML
)
) //{{}} 外面的{}表示 js 表达式 里面的表示对象
//默认情况下jsx有保护机制,及不能将字符串转化html代码,如果需要转换,需要在元素上添加dangerouslySetInnerHTML
const content = '<h1>I Love you very much <p>Do you like me?</p></h1>';
const h1 = (
<li>
<div dangerouslySetInnerHTML>{content}</div>
</li>
);
ReactDOM.render(h1, document.getElementById('root'));
//具体使用如下dangerouslySetInnerHTML={{__html:content}}
const h1 = (
<li>
<div
dangerouslySetInnerHTML={{
__html: content,
}}
></div>
</li>
);
复制代码
元素的不可变性
- 虽然 JSX 元素是一个对象,但是该对象中的所有属性不可更改
let num = 0;
const div = <div title="asd">{num}</div>;
div.props.title = 'twd'; //报错属性不能更改
其实是Object.freeze(div); //冻结了这个属性
复制代码
- 如果确实需要更改元素的属性,需要重新创建 JSX 元素
setInterval(() => {
num++;
const h1 = <div title="asd">{num}</div>;
ReactDOM.render(h1, document.getElementById('root'));
}, 1000);
复制代码
demo 图片切换
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import demo1 from './assets/demo1.jpg';
import demo2 from './assets/demo2.jpg';
import demo3 from './assets/demo3.jpg';
let index = 0;
let arr = [demo1, demo2, demo3]; // console.log(demo1); ///static/media/demo1.e7e78cf4.jpg
const container = document.getElementById('root');
let timer;
function start() {
stop();
timer = setInterval(() => {
index = (index + 1) % 3;
const div = <img src={arr[index]} alt=""></img>;
render(div);
}, 1000);
}
function render(div) {
ReactDOM.render(div, container);
}
function stop() {
clearInterval(timer);
}
start();
container.onmouseenter = function () {
stop();
};
container.onmouseleave = function () {
start();
};
复制代码
组件和组件属性
- 组件: 包含内容 样式和功能的 UI 单元
创建一个组件
特别注意:组件的名称首字母必须大写
否则不会识别为 React 组件元素(也就是不会进行函数调用),而是普通元素
- 函数组件
返回一个 React 元素
function MyFuncComp(){
return <h1>组件内容</h1>
}
ReactDOM.render(
<div> { MyFuncComp() }</div>, // 第一种(不常用,调试器中不显示组件,不利于调试)
<MyFuncComp number="123"></MyFuncComp> //一般这这样子写
document.getElementById('root')
);
复制代码
- 类组件
必须继承 React.Component
import React from 'react';
export default class MyClassComp extends React.Component {
/**
*必须有render方法 该方法必须返回React元素
*该方法是在MyClassComp原型上面的,因此可以使用this.props
*/
render() {
//return 123;
return <h1>类组件</h1>;
}
}
复制代码
必须提供 render 函数,用于渲染组件
组件属性
- 对于函数组件,属性会作为一个对象的属性,传递给函数的参数
<MyFuncComp number="123"></MyFuncComp>;
export default function (props) {
console.log(props); //{number: "123"}
return <h1>组件内容 {props.number}</h1>;
}
复制代码
- 对于类组件,属性会作为一个对象的属性,传递给构造函数的参数
ReactDOM.render(
<div>
{comp}
<MyFuncComp number="123"></MyFuncComp>
<MyClassComp pittle="love" obj={{ name: 'pittle', age: 18 }}></MyClassComp>
<MyClassComp
pittle="love"
obj={{ name: 'pittle', age: 18 }}
cmp={<h1>I love you</h1>}
></MyClassComp>
</div>,
document.getElementById('root')
);
export default class MyClassComp extends React.Component {
constructor(props) {
super(props); //React.Component的constructor就接受参数props实现this.props = props
//super(props)相当于 React.Component.prototype.constructor.call(this)
console.log(this.props, props); //{pittle: "love"}
}
/**
* 该方法必须返回React元素,该方法由React自动调用
*/
render() {
//return this.props.pittle;
return this.props.cmp;
}
}
复制代码
注意:组件的属性,应该使用小驼峰命名法
组件无法改变自身的属性。
之前学习的 React 元素,本质上,就是一个组件(内置组件)
React 中的哲学:数据属于谁,谁才有权力改动(单向数据流 自顶而下)
Demo 显示学生列表
// 注意: await fetch()会返回的一个带返回值结果(resp.data)的promise对象(可以在浏览器上测试上面那行代码)
**index.js**
import React from 'react';
import ReactDOM from 'react-dom';
import StudentList from './components/StudentList';
const appkey = "demo13_1545210570249"
async function FetchData(){
var stus = await fetch("http://open.duyiedu.com/api/student/findAll?appkey=" + appkey)
.then(res => res.json()).then(resp => resp.data); //复习Promise
return stus;
}
async function render(){
ReactDOM.render(
<div>
"正在加载中..."
</div>,
document.getElementById('root')
);
const stus = await FetchData();
ReactDOM.render(
<div>
<StudentList stus={stus}></StudentList>
</div>,
document.getElementById('root')
);
}
render();
**Student.js**
import React from 'react'
export default function Student(props) {
// 假设所有的学生信息,都是分开你传递的
return (
<li>
{/* 显示一个学生的所有数据 */}
[姓名]:{props.name}
[email]:{props.email}
[性别]:{props.sex === 1 ? 'man' : 'woman'}
[出生年月]:{props.birth}
</li>
)
}
** StudentList.js **
import React, { Component } from 'react'
import Student from "./Student"
export default class StudentList extends Component {
render() {
// 约定 props.stus传递的是学生的数组
// 获取组件数组
const students = this.props.stus.map(item => <Student key={item.id} {...item} />);
console.log(students);
return (
<div>
{students}
</div>
)
}
}
复制代码
组件状态
组件状态: 组件可以自行维护的数据
组件状态仅在类组件中有效
状态(state),本质上是类组件的一个属性,是一个对象
状态初始化
状态变化
不能直接改变状态:因为 React 无法监控到状态发生了变化
必须使用 this.setState({})改变状态
一旦调用 this.setState({}) 会导致组件重新渲染
组件中的数据
- props: 该数据是由组件的使用者传递的数组,所有权不属于组件自身,因此组件无法改变该数据
- state: 该数据是由组件自身创建的,所有权属于组件自身,因此组件有权改变该数据
ReactDOM.render(
<div>
<Tick number={10} />
</div>,
document.getElementById('root')
);
export default class Tick extends Component {
// 初始化状态: JS Next语法,目前处于试验阶段
// this.state = {
// number:this.props.number
// }
constructor(props) {
super(props);
//初始化状态
this.state = {
number: this.props.number,
};
this.timer = setInterval(() => {
this.setState({
number: this.state.number - 1,
});
if (this.state.number === 0) {
clearInterval(this.timer);
}
}, 1000);
}
render() {
return <div>倒计时: {this.state.number}</div>;
}
}
复制代码
Demo 自动移动的小球
ReactDOM.render(<BallList />, document.getElementById('root'));
import React, { Component } from 'react';
export default class Ball extends Component {
constructor(props) {
super(props);
//初始化状态
// 属性中需要分别传递横纵坐标的速度 每秒移动的像素值
// props.xSpeed props.ySpeed
// 需要传递背景颜色 如果没有传递,则使用红色
this.state = {
left: props.left || 0,
top: props.top || 0,
xSpeed: props.xSpeed,
ySpeed: props.ySpeed,
};
const duration = 16;
this.timer = setInterval(() => {
const xSpeed = (this.state.xSpeed * duration) / 1000;
const ySpeed = (this.state.ySpeed * duration) / 1000;
let newLeft = this.state.left + xSpeed;
let newTop = this.state.top + ySpeed;
if (newLeft <= 0) {
newLeft = 0;
this.setState({
xSpeed: -this.state.xSpeed, //横坐标反向
});
} else if (newLeft >= document.documentElement.clientWidth - 100) {
newLeft = document.documentElement.clientWidth - 100;
this.setState({
xSpeed: -this.state.xSpeed,
});
}
if (newTop <= 0) {
newTop = 0;
this.setState({
ySpeed: -this.state.ySpeed, //纵坐标反向
});
} else if (newTop >= document.documentElement.clientHeight - 100) {
newTop = document.documentElement.clientHeight - 100;
this.setState({
ySpeed: -this.state.ySpeed,
});
}
this.setState({
left: newLeft,
top: newTop,
});
if (this.state.number === 0) {
clearInterval(this.timer);
}
}, duration);
}
render() {
return (
<div
className="ball"
style={{
left: this.state.left,
top: this.state.top,
background: this.props.bg || '#f40',
}}
></div>
);
}
}
复制代码
事件
在 React 中,组件的事件,本质是一个属性
按照之前 React 对组件的约定,由于事件本质上是一个属性,因此也需要使用小驼峰命名法
function handClick() {
console.log('点击');
}
//const btn = <button onClick={handClick}>点击</button>;
const btn = (
<button
onClick={() => {
console.log('hello world');
}}
onMouseEnter={(e) => {
console.log(e);
}}
>
点击
</button>
);
ReactDOM.render(btn, document.getElementById('root'));
复制代码
handleClick 中的 this 指向 undefined
如果没有特殊处理,在事件处理函数中,this 指向 undefined
//解决办法一:使用bind函数,绑定this
constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleOver = this.handleOver.bind(this);
}
handleClick(){}
handleOver(){}
<Tick onClick={this.handleClick.bind(this)} />
//解决办法二:使用箭头函数(handleClick不在原型上,而在对象(实例)上,与之前写state一样是next语法)
handleClick = () => {}
handleOver = () => {}
render() {
return (
<Tick onClick={this.handleClick()} />
)
}
复制代码
制作分页组件
深入认识 setState
setState,它对状态的改变,可能是异步的(这里是 html 事件)
如果改变状态的代码处于某个 HTML 元素的事件中,则其是异步,否则是同步(上面定时器的例子就是同步)
如果遇到某个事件中,需要同步调用多次,需要使用函数的方式得到最新状态
最佳实践:
- 把所有的 setState 当作是异步的
- 永远不要信任 setState 调用之后的状态
- 如果要使用改变之后的状态,需要使用回调函数(setState 的第二个参数是回调函数)
- 如果新的状态要根据之前的状态进行运算,使用函数的方式改变状态(setState 第一个参数是函数)
state = {
n:0
}
handleClick = () => {
this.setState({
n:this.state.n + 1
});
console.log(this.state.n); //先输出this.state.n 再执行render重新渲染
// this.setState({
// n:this.state.n + 1
// },() => {
// //状态改变之后触发,该函数运行在render函数之后
// console.log('compelete');
// });
//同时执行setState三次,最后渲染出来还是1 //事件中setState是异步的,不会立即改变state,所以每次都到的state.n多是0
//因为每次拿到的this.state.n是一样的还是0
// 所以执行三次都还是0+1
// this.setState({
// n:this.state.n + 1
// });
// this.setState({
// n:this.state.n + 1
// });
//下面这种就可渲染三次,不过太恶心了
// this.setState({
// n:this.state.n + 1
// },() => {
// this.setState({
// n:this.state.n + 1
// },() => {
// this.setState({
// n:this.state.n + 1
// });
// });
// });
// setState的第一个参数还可以是函数
// 这样写上三个this.setState,cur会保存上一个的状态
// 源码可能是把setState的每个函数放到一个队列当中,每次执行一个,当所有的函数调用
// 完之后,再改变最终的状态
this.setState(cur => { //cur = this.state
//参数cur表示当前state状态
//该函数的返回结果 会混合(覆盖)掉之前的状态
//该函数也是异步执行的
return {
n:cur.n + 1
}
},() => {
//回调还是一样的
//回调函数式在所有状态全部更新完成并且重新渲染后执行
console.log('state更新完成', this.state.n); //3
})
this.setState(cur => ({n:cur.n + 1}),() => {
//回调还是一样的
})
this.setState(cur => ({n:cur.n + 1}),() => {
//回调还是一样的
})
}
render(){
console.log('render');
return (<div>
<h1>
{this.state.n}
</h1>
<p><button onClick={this.handleClick}>+</button></p>
</div>)
}
复制代码
React 会对异步的 setState 进行优化,将多次 setState 进行合并(将多次状态改变完成后,统一对 state 进行改变,触发一次 render)
constructor() {
super();
this.state = {
val: 0,
};
}
componentDidMount() {
this.setState({ val: this.state.val + 1 });
console.log(this.state.val); //第一次结果0
this.setState({ val: this.state.val + 1 });
console.log(this.state.val);//第二次结果0
setTimeout(() => {
console.log(this.state.val); //第三次结果1
this.setState({ val: this.state.val + 1 });
console.log(this.state.val);//第四次结果2
this.setState({ val: this.state.val + 1 }); //第五次结果3
console.log(this.state.val);
}, 0);
}
//结果是0 0 1 2 3
//说明this.setState在componentDidMount函数中是统一触发一次
//进入到setTimeout函数中是同步执行
复制代码
生命周期
生命周期:组件从诞生到销毁会经历一系列的过程,该过程就叫做生命周期。React 在组件的生命周期中提供了一系列的钩子函数(类似于事件),可以让开发者在函数中注入代码,这些代码会在适当的时候运行.
生命周期仅存在于类组件中,函数组件每次调用都是重新运行函数,旧的组件即刻被销毁
旧版生命周期
-
constructor
同一个组件对象只会创建一次
不能在第一次挂载到页面之前,调用 setState,为了避免问题,构造函数中严禁使用 setState -
componentWillMount
正常情况下,和构造函数一样,它只会运行一次
可以使用 setState,但是为了避免 bug,不允许使用,因为在某些特殊情况下,该函数可能被调用多次 -
render
返回一个虚拟 DOM,会被挂载到虚拟 DOM 树中,最终渲染到页面的真实 DOM 中
render 可能不只运行一次,只要需要重新渲染,就会重新运行
严禁使用 setState,因为可能会导致无限递归渲染 -
componentDidMount
只会执行一次
可以使用 setState
通常情况下,会将网络请求、启动计时器等一开始需要的操作,书写到该函数中
组件进入活跃状态 -
componentWillReceiveProps
即将接收新的属性值
参数为新的属性对象
该函数可能会导致一些 bug,所以不推荐使用
(只要属性重新被赋值就会触发,属性值不一定是变化的,可能两次被赋同一个值) -
shouldComponentUpdate
指示 React 是否要重新渲染该组件,通过返回 true 和 false 来指定
默认情况下,会直接返回 true -
componentWillUpdate
组件即将被重新渲染 -
componentDidUpdate
往往在该函数中使用 dom 操作,改变元素 -
componentWillUnmount
通常在该函数中销毁一些组件依赖的资源,比如计时器
注意
如果组件自身的 state 更新了,那么会依次执行 shouldComponentUpdate、componentWillUpdate
render 和 componentDidUpdate
DOM 真正被添加到 HTML 中的生命周期方法是 componentDidMount 和 componentDidUpdate 方法。在这两个方法中,我们可以获取真正的 DOM 元素。在这两个方法中,我们可以获取真正的 DOM 元
素。React 提供的获取 DOM 元素的方法有两种,其中一种就是 ReactDOM 提供的 findDOMNode
componentDidMount() {
console.log(ReactDOM.findDOMNode(this));
}
复制代码
新版生命周期
React >= 16.0.0
React 官方认为,某个数据的来源必须是单一的
- getDerivedStateFromProps
通过参数可以获取新的属性和状态
该函数是静态的
该函数的返回值会覆盖掉组件状态
该函数几乎是没有什么用 - getSnapshotBeforeUpdate
真实的 DOM 构建完成,但还未实际渲染到页面中。
在该函数中,通常用于实现一些附加的 dom 操作
该函数的返回值,会作为 componentDidUpdate 的第三个参数
export default class OldLifeCycle extends Component {
constructor(props) {
super(props);
this.state = {
n: 0,
};
console.log('constructor', '一个新的组件诞生了!!!');
}
componentWillMount() {
console.log('componentWillMount', '组件即将被挂载');
}
componentDidMount() {
console.log('componentDidMount', '挂载完成');
}
componentWillReceiveProps(nextProps) {
console.log(
'componentWillReceiveProps',
'接收到新的属性值',
this.props,
nextProps
);
}
shouldComponentUpdate(nextProps, nextState) {
console.log(
'shouldComponentUpdate',
'是否应该重新渲染',
this.props,
nextProps,
this.state,
nextState
);
if (this.props.n === nextProps.n && this.state.n === nextState.n) {
return false;
}
return true;
// return false;
}
componentWillUpdate(nextProps, nextState) {
console.log('componentWillUpdate', '组件即将被重新渲染');
}
componentDidUpdate(prevProps, prevState) {
console.log(
'componentDidUpdate',
'组件已完成重新渲染',
prevProps,
prevState
);
}
componentWillUnmount() {
console.log('componentWillUnmount', '组件被销毁');
}
render() {
console.log('render', '渲染,返回的React元素会被挂载到虚拟DOM树中');
return (
<div>
<h1>旧版生命周期组件</h1>
<h2>属性n: {this.props.n}</h2>
<h2>状态n:{this.state.n}</h2>
<button
onClick={() => {
this.setState({
n: this.state.n + 1,
});
}}
>
状态n+1
</button>
</div>
);
}
}
复制代码
传递元素内容
内置组件:div、h1、p…
<div>asdfafasfafasdfasdf</div>
复制代码
//index.js
import React from 'react';
import ReactDOM from 'react-dom';
import Comp from './Comp';
ReactDOM.render(
<Comp content1={<h2>第2组元素内容</h2>} content2={<h2>第3组元素内容</h2>}>
<h2>第1组元素内容</h2>
</Comp>,
document.getElementById('root')
);
//Comp.js
import React from 'react';
export default function Comp(props) {
console.log(props);
return (
<div className="comp">
<h1>组件自身的内容</h1>
{/* {props.children || <h1>默认值</h1>} */}
{props.children}
{props.content1}
{props.content2}
</div>
);
}
复制代码
如果给自定义组件传递元素内容,则 React 会将元素内容作为 children 属性传递过去。
制作蒙层组件
// Test.js
import React, { Component } from 'react';
import Model from './common/Model';
export default class Test extends Component {
state = {
showModel: false,
};
showModel = () => {
this.setState({
showModel: true,
});
};
hideModel = () => {
this.setState({
showModel: false,
});
};
render() {
return (
<div>
<img
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0xMS41IC0xMC4yMzE3NCAyMyAyMC40NjM0OCI+CiAgPHRpdGxlPlJlYWN0IExvZ288L3RpdGxlPgogIDxjaXJjbGUgY3g9IjAiIGN5PSIwIiByPSIyLjA1IiBmaWxsPSIjNjFkYWZiIi8+CiAgPGcgc3Ryb2tlPSIjNjFkYWZiIiBzdHJva2Utd2lkdGg9IjEiIGZpbGw9Im5vbmUiPgogICAgPGVsbGlwc2Ugcng9IjExIiByeT0iNC4yIi8+CiAgICA8ZWxsaXBzZSByeD0iMTEiIHJ5PSI0LjIiIHRyYW5zZm9ybT0icm90YXRlKDYwKSIvPgogICAgPGVsbGlwc2Ugcng9IjExIiByeT0iNC4yIiB0cmFuc2Zvcm09InJvdGF0ZSgxMjApIi8+CiAgPC9nPgo8L3N2Zz4K"
alt=""
width="500"
height="500"
/>
{this.state.showModel ? (
<Model onClose={this.hideModel}>
<div
style={{
background: '#fff',
}}
>
<h1>asdfasfasfasfasdfasdf</h1>
<button onClick={this.hideModel}>关闭朦层</button>
</div>
</Model>
) : null}
<button onClick={this.showModel}>显示朦层</button>
</div>
);
}
}
// Model.js
import React from 'react';
import './index.css';
export default function Model(props) {
var defaultProps = {
//默认属性
bg: 'rgba(0,0,0,.5)',
};
var datas = Object.assign({}, defaultProps, props);
return (
<div
onClick={(e) => {
if (e.target.className === 'model') {
//防止事件冒泡,点击弹窗的时候冒泡也会执行onClick
datas.onClose();
}
}}
className="model"
style={{
background: datas.bg,
}}
>
<div className="model-center">{datas.children}</div>
</div>
);
}
复制代码
表单
受控组件和非受控组件
受控组件:组件的使用者,有能力完全控制该组件的行为和内容。通常情况下,受控组件往往没有自身的状态,其内容完全受到属性的控制。
非受控组件:组件的使用者,没有能力控制该组件的行为和内容,组件的行为和内容完全自行控制。
表单组件,默认情况下是非受控组件,一旦设置了表单组件的 value 属性,则其变为受控组件(单选和多选框需要设置 checked)
{/* 默认情况下,它是一个非受控组件 */}
{/* <input type="text" /> */}
//受控组件
<input
type="text"
value={this.state.val}
onChange={(e) => {
this.setState({
val: e.target.value,
});
}}
/>
state = {
val: '123',
// checked: true
loves: ['足球', '篮球', '音乐', '其他'],
chooseLoves: ['篮球', '音乐'],
selVal: 'beijing',
};
const checkboxes = this.state.loves.map((it) => (
<label key={it}>
<input
type="checkbox"
checked={this.state.chooseLoves.includes(it)}
onChange={(e) => {
if (e.target.checked) {
this.setState({
chooseLoves: [...this.state.chooseLoves, it], //添加
});
} else {
this.setState({
chooseLoves: this.state.chooseLoves.filter((l) => l !== it), //移除
});
}
}}
/>
{it}
</label>
复制代码
封装表单组件
属性
- datas:一个数组,数组每一项是一个对象,对应一个多选框
- 对象具有 value 和 text 属性
- value:多选框的值
- text:多选框的文本
例如:
datas = [
{ value: 'football', text: '足球' },
{ value: 'basketball', text: '篮球' },
{ value: 'movie', text: '电影' },
];
复制代码
- name:每一个多选框的 name 属性值
- value: 当前选中的 value 值
- onChange:当选中项发生改变时的事件
属性默认值 和 类型检查
属性默认值
通过一个静态属性 defaultProps 告知 react 属性默认值
属性类型检查
使用库:prop-types
//commonTypes.js
import PropTypes from "prop-types"
export default {
children: PropTypes.node,
groupDatas: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string.isRequired,
text: PropTypes.string.isRequired
})), //多选框组、单选框组、下拉列表的数据源
chooseDatas: PropTypes.arrayOf(PropTypes.string),
}
import types from "../../../utils/commonTypes"
/**
* 默认属性值
*/
static defaulProps = {
datas: [],
value: ""
}
static propTypes = {
datas: types.groupDatas.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func
}
复制代码
对组件使用静态属性 propTypes 告知 react 如何检查属性
PropTypes.any://任意类型
PropTypes.array://数组类型
PropTypes.bool://布尔类型
PropTypes.func://函数类型
PropTypes.number://数字类型
PropTypes.object://对象类型
PropTypes.string://字符串类型
PropTypes.symbol://符号类型
a:PropTypes.number.isRequired//表示a属性必填不能为null和undefined
//PropTypes.number其实是一个函数 可以用console.dir打印
// PropTypes.number.isRequired也是一个函数
PropTypes.node://任何可以被渲染的内容,字符串、数字、React 元素
PropTypes.element://react 元素
PropTypes.elementType://react 元素类型
PropTypes.instanceOf(构造函数)://必须是指定构造函数的实例
PropTypes.oneOf([xxx, xxx])://枚举
PropTypes.oneOfType([xxx, xxx]); //属性类型必须是数组中的其中一个
PropTypes.arrayOf(PropTypes.XXX)://必须是某一类型组成的数组
PropTypes.objectOf(PropTypes.XXX)://对象由某一类型的值组成
PropTypes.shape(对象): //属性必须是对象,并且满足指定的对象要求
PropTypes.exact({...})://对象必须精确匹配传递的数据
//自定义属性检查,如果有错误,返回错误对象即可
属性: function(props, propName, componentName) {
//...
}
复制代码
HOC 高阶组件
HOF:Higher-Order Function 高阶函数 以函数作为参数 并返回一个函数
HOC:Higher-Order Component 高阶组件 以组件作为参数 并返回一个组件
function Comp(props){
return <h1>aaaa</h1>
}
class Comp{}
<Comp /> React元素
<h1></h1> React Html Element
复制代码
ref
reference: 引用
场景:希望直接使用 dom 元素中的某个方法,或者希望直接使用自定义组件中的某个方法
- ref 作用于内置的 html 组件,得到的将是真实的 dom 对象
- ref 作用于类组件,得到的将是类的实例
- ref 不能作用于函数组件
- ref 不再推荐使用字符串赋值,字符串赋值的方式将来可能会被移出
字符串
import React, { Component } from 'react';
class A extends Component {
method() {
console.log('调用了组件A的方法');
}
render() {
return <h1>组件A</h1>;
}
}
// function B(){
// return <h1>组件B</h1>
// }
export default class Comp extends Component {
handleClick = () => {
console.log(this);
this.refs.txt.focus();
this.refs.compA.method();
};
render() {
return (
<div>
<input ref="txt" type="text" />
<A ref="compA" />
{/* <B ref="compB" /> */}
<button onClick={this.handleClick}>聚焦</button>
</div>
);
}
}
复制代码
目前,ref 推荐使用对象或者是函数
对象
export default class Comp extends Component {
constructor(props) {
super(props);
this.txt = React.createRef(); //相当于this.txt = {current:null}
//这里直接使用this.txt = {current:null}也是可以的
// React.createRef()只不过是做了效率的优化,保持current不变
//执行render函数的时候会自动给current属性赋值
}
handleclick = () => {
console.log(this);
console.log(this.txt); //{current: input}
this.txt.current.focus();
};
render() {
return (
<div>
<input ref={this.txt} type="text" />
<button onClick={this.handleclick}>聚焦</button>
</div>
);
}
}
复制代码
通过 React.createRef 函数创建
函数
export default class Comp extends Component {
constructor(props) {
super(props);
}
handleclick = () => {
console.log(this);
this.setState({});
this.txt.focus();
};
componentDidMount() {
console.log(this.txt, 'didmount');
}
render() {
return (
<div>
<input
ref={(el) => {
//刚开始渲染的时候就会执行这个函数,并且是在componentDidMount之前执行
console.log(el, '111'); //每次执行this.setState({});这里会执行两次 第一次el为null,第二次el为input对象
this.txt = el;
}}
type="text"
/>
<button onClick={this.handleclick}>聚焦</button>
</div>
);
}
}
复制代码
export default class Comp extends Component {
constructor(props) {
super(props);
// this.txt = React.createRef();
}
handleclick = () => {
console.log(this);
this.setState({});
this.txt.focus();
};
componentDidMount() {
console.log(this.txt, 'didmount');
}
getRef = (el) => {
//这里ref就没有变动所以只有在初次渲染执行render函数的时候执行一次,其他时候不会执行的
console.log(el, '111');
this.txt = el;
};
render() {
return (
<div>
<input ref={this.getRef} type="text" />
<button onClick={this.handleclick}>聚焦</button>
</div>
);
}
}
复制代码
函数的调用时间:
componentDidMount 的时候会调用该函数
在 componentDidMount 事件中可以使用 ref
如果 ref 的值发生了变动(旧的函数被新的函数替代),分别调用旧的函数以及新的函数,时间点出现在 componentDidUpdate 之前
旧的函数被调用时,传递 null
新的函数被调用时,传递对象
如果 ref 所在的组件被卸载,会调用函数
谨慎使用 ref
能够使用属性和状态进行控制,就不要使用 ref。
调用真实的 DOM 对象中的方法
某个时候需要调用类组件的方法
import React, { Component } from 'react';
export default class Comp extends Component {
state = {
show: true,
};
handleClick = () => {
// this.txt.focus();
this.setState({
show: !this.state.show,
});
};
componentDidMount() {
console.log('didMount', this.txt);
}
getRef = (el) => {
console.log('函数被调用了', el);
this.txt = el;
};
render() {
return (
<div>
{this.state.show && <input ref={this.getRef} type="text" />}
<button onClick={this.handleClick}>显示/隐藏</button>
</div>
);
}
}
复制代码
ref 转发
forwardRef
forwardRef 方法:
参数,传递的是函数组件,不能是类组件,并且,函数组件需要有第二个参数来得到 ref
返回值,返回一个新的组件
refs
- 值得注意的是,在 React 中使用 DOM 原生事件时,一定要在组件卸载时手动移除,否则很
可能出现内存泄漏的问题。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
export default class NativeEventDemo extends Component {
componentDidMount() {
this.refs.button.addEventListener('click', (e) => {
this.handleClick(e);
});
}
handleClick(e) {
console.log(e);
}
componentWillUnmount() {
this.refs.button.removeEventListener('click');
}
render() {
return <button ref="button">Test</button>;
}
}
复制代码
import React, { Component } from 'react';
function A(props, ref) {
return (
<h1 ref={ref}>
组件A
<span>{props.word}</span>
</h1>
);
}
// 传递函数组件A 得到一个新的组件NewA
const NewA = React.forwardRef(A);
export default class App extends React.Component {
ARef = React.createRef();
componentDidMount() {
console.log(this.ARef);
}
render() {
return (
<div>
<NewA ref={this.ARef} word="props属性" />
</div>
);
}
}
复制代码
类组件传递 ref 通过普通属性
//方法1
class A extends React.Component {
render() {
return (
<h1 ref={this.props.ref1}>
组件A
<span>{this.props.word}</span>
</h1>
);
}
}
export default class App extends React.Component {
ARef = React.createRef();
componentDidMount() {
console.log(this.ARef);
}
render() {
return (
<div>
<A ref1={this.ARef} word="props属性" />
</div>
);
}
}
// 方法2
class A extends React.Component {
render() {
return (
<h1 ref={this.props.ref1}>
组件A
<span>{this.props.word}</span>
</h1>
);
}
}
// 传递函数组件A 得到一个新的组件NewA
const NewA = React.forwardRef((props, ref) => {
return <A {...props} ref1={ref} />;
});
export default class App extends React.Component {
ARef = React.createRef();
componentDidMount() {
console.log(this.ARef);
}
render() {
return (
<div>
<NewA ref={this.ARef} word="props属性" />
</div>
);
}
}
复制代码
Context
上下文:Context,表示做某一些事情的环境
React 中的上下文特点:
当某个组件创建了上下文后,上下文中的数据,会被所有后代组件共享
如果某个组件依赖了上下文,会导致该组件不再纯粹(外部数据仅来源于属性 props)
一般情况下,用于第三方组件(通用组件)
旧的 API
创建上下文
只有类组件才可以创建上下文
给类组件书写静态属性 childContextTypes,使用该属性对上下文中的数据类型进行约束
添加实例方法 getChildContext,该方法返回的对象,即为上下文中的数据,该数据必须满足类型约束,该方法会在每次 render 之后运行。
使用上下文中的数据
要求:如果要使用上下文中的数据,组件必须有一个静态属性 contextTypes,该属性描述了需要获取的上下文中的数据类型
可以在组件的构造函数中,通过第二个参数,获取上下文数据
从组件的 context 属性中获取
在函数组件中,通过第二个参数,获取上下文数据
上下文的数据变化
上下文中的数据不可以直接变化,最终都是通过状态改变
在上下文中加入一个处理函数,可以用于后代组件更改上下文的数据
import React, { Component } from 'react';
import PropTypes from 'prop-types';
const types = {
a: PropTypes.number,
b: PropTypes.string.isRequired,
onChangeA: PropTypes.func,
};
class ChildA extends Component {
static contextTypes = types;
static childContextTypes = {
a: PropTypes.number,
c: PropTypes.string,
};
getChildContext() {
return {
a: 789,
c: 'hello',
};
}
render() {
return (
<div>
<h1>ChildA</h1>
<h2>
a:{this.context.a},b:{this.context.b}
</h2>
<ChildB />
</div>
);
}
}
class ChildB extends React.Component {
/**
* 声明需要使用哪些上下文中的数据
*/
static contextTypes = {
...types,
c: PropTypes.string,
};
render() {
return (
<p>
ChildB,来自于上下文的数据:a: {this.context.a}, b:{this.context.b}
,c: {this.context.c}
<button
onClick={() => {
this.context.onChangeA(this.context.a + 2);
}}
>
子组件的按钮,a+2
</button>
</p>
);
}
}
export default class OldContext extends Component {
/**
* 约束上下文中数据的类型
*/
static childContextTypes = types;
state = {
a: 123,
b: 'abc',
};
/**
* 得到上下文中的数据
*/
getChildContext() {
console.log('获取新的上下文');
return {
a: this.state.a,
b: this.state.b,
onChangeA: (newA) => {
this.setState({
a: newA,
});
},
};
}
render() {
return (
<div>
<ChildA />
<button
onClick={() => {
this.setState({
a: this.state.a + 1,
});
}}
>
a加1
</button>
</div>
);
}
}
复制代码
新版 API
旧版 API 存在严重的效率问题,并且容易导致滥用
创建上下文
上下文是一个独立于组件的对象,该对象通过 React.createContext(默认值)创建
返回的是一个包含两个属性的对象
Provider 属性:生产者。一个组件,该组件会创建一个上下文,该组件有一个 value 属性,通过该属性,可以为其数据赋值
同一个 Provider,不要用到多个组件中,如果需要在其他组件中使用该数据,应该考虑将数据提升到更高的层次
Consumer 属性:后续讲解
使用上下文中的数据
在类组件中,直接使用 this.context 获取上下文数据
要求:必须拥有静态属性 contextType , 应赋值为创建的上下文对象
在函数组件中,需要使用 Consumer 来获取上下文数据
Consumer 是一个组件
它的子节点,是一个函数(它的 props.children 需要传递一个函数)
注意细节
如果,上下文提供者(Context.Provider)中的 value 属性发生变化(Object.is 比较),会导致该上下文提供的所有后代元素全部重新渲染,无论该子元素是否有优化(无论 shouldComponentUpdate 函数返回什么结果),会跳过 shouldComponentUpdate 进行强制重新渲染
eg1:
import React, { Component } from 'react';
const ctx = React.createContext();
class ChildB extends React.Component {
//写法一:
// static contextType = ctx;
// render() {
// return (
// <h1>
// a:{this.context.a},b:{this.context.b}
// <button
// onClick={() => {
// this.context.changeA(this.context.a + 2);
// }}
// >
// 后代组件的按钮,点击a+2
// </button>
// </h1>
// );
// }
//写法二:
render() {
return (
<ctx.Consumer>
{(value) => (
<h1>
a:{this.context.a},b:{value.b}
<button
onClick={() => {
this.context.changeA(value.a + 2);
}}
>
后代组件的按钮,点击a+2
</button>
</h1>
)}
</ctx.Consumer>
);
}
}
function ChildA(props) {
return (
<div>
<h1>childA</h1>
<h2>
<ctx.Consumer>
{(value) => (
<>
{value.a},{value.b}
</>
)}
</ctx.Consumer>
</h2>
<div>
<ChildB />
</div>
</div>
);
}
export default class NewContext extends Component {
state = {
a: 0,
b: 'abc',
changeA: (newA) => {
this.setState({
a: newA,
});
},
};
render() {
console.log(this.state);
return (
<ctx.Provider value={this.state}>
<div>
<ChildA />
<button
onClick={() => {
this.setState({
a: this.state.a + 1,
});
}}
>
父组件的按钮,a加1
</button>
</div>
</ctx.Provider>
);
}
}
复制代码
eg2:如果 ctx 是对象,改变 ctx.a 需要重新改变这个对象(下面的例子是憨批写法,以后别看了)
import React, { Component } from 'react';
const ctx = React.createContext();
class ChildB extends React.Component {
static contextType = ctx;
// shouldComponentUpdate(nextProps, nextState) {
// console.log('运行了优化');
// return false;
// }
render() {
console.log(this.context, 'context');
return (
<h1>
a:{this.context.a},b:{this.context.b}
<button
onClick={() => {
this.context.changeA(this.context.a + 2);
}}
>
后代组件的按钮,点击a+2
</button>
</h1>
);
}
}
export default class NewContext extends Component {
state = {
ctx: {
a: 0,
b: 'abc',
changeA: (newA) => {
this.setState({
ctx: {
a: newA,
b: this.state.ctx.b,
changeA: (newA) => {
this.setState({
ctx: {
a: newA,
b: this.state.ctx.b,
changeA: this.state.ctx.changeA,
},
});
},
},
});
},
},
};
render() {
console.log(this.state);
return (
<ctx.Provider value={this.state.ctx}>
<div>
<ChildB />
<button
onClick={() => {
this.setState({
ctx: {
a: this.state.ctx.a + 1,
b: this.state.ctx.b,
},
});
}}
>
父组件的按钮,a加1
</button>
</div>
</ctx.Provider>
);
}
}
复制代码
eg3:每次 this.setState({})会给 state 设置一个新的对象,这个新的对象会继承更新前的 state 属性,并且同名新的属性会覆盖旧的,可能就是 Object.assign(新的 state,旧的 state)
<ctx.Provider value={this.state}>
的 value 属性比较的是 this.state 的引用,所以 state 的引用变化会导致上下文刷新
import React, { Component } from 'react';
const ctx = React.createContext();
function ChildA(props) {
return (
<div>
<h1>childA</h1>
<h2>
<ctx.Consumer>
{(value) => (
<>
{value.a},{value.b}
</>
)}
</ctx.Consumer>
</h2>
</div>
);
}
export default class NewContext extends Component {
datas = [
{
a: 0,
b: 'abc',
changeA: (newA) => {
this.setState({
a: newA,
});
},
},
];
state = this.datas[0];
render() {
console.log(this.state);
return (
<ctx.Provider value={this.state}>
<div>
<ChildA />
<button
onClick={() => {
this.setState({}, () => {
this.datas.push(this.state);
console.log(this.datas[0] === this.datas[1]);
});
}}
>
父组件的按钮,a加1
</button>
</div>
</ctx.Provider>
);
}
}
复制代码
PureComponent
纯组件,用于避免不必要的渲染(运行 render 函数)
优化:如果一个组件的属性和状态都没有发生变化,重新渲染该组件是没有必要的
PureComponent 是一个组件,如果某个组件继承改组件,则该组件的 shouldComponentUpdate 会进行优化,对属性和状态进行钱比较,如果相等则不会重新渲染
相当于下面的代码
shouldComponentUpdate(nextProps, nextState) {
//参数:新的state和新的props。由于每次重新渲染,prop都会被赋上新的对象
// 所以shouldComponentUpdate的这个函数是在重新渲染之前比较一下,新的state/props和旧的state/props的区别
if (
ObjectEqual(nextProps, this.props) &&
ObjectEqual(this.state, nextState)
) {
return false;
}
return true;
}
// 浅比较
// export function ObjectEqual(obj1, obj2) {
// for (let prop in obj1) {
// if (!Object.is(obj1[prop], obj2[prop])) {
// return false;
// }
// }
// return true;
// }
复制代码
注意
- PureComponent 是浅比较
为了效率 应该尽量使用 PureComponent
要求不要改动之前的状态 永远是创建新的状态覆盖之前的状态(Immutable,不可变对象)
有一个第三方 js 库,Immutable.js,它专门用于制作不可变对象 - 函数组件, 使用 React.memo 函数制作纯组件
function Task(props) {
console.log('Task');
return <li className={props.isFinish ? 'isFinish' : ''}>{props.name}</li>;
}
Task.propTypes = {
name: propTypes.string.isRequired,
isFinish: propTypes.bool.isRequired,
};
export default React.memo(Task);
复制代码
React.memo 原理(就是一个高阶组件)
function memo(FunComp) {
return class Memo extends PureComponent {
// render(){
// return <FunComp {...this.props}/>;
// }
render() {
return <>{FunComp(this.props)}</>;
}
};
}
复制代码
注意:避免下面这种写法
this.state.tasks.push(newTask)会在之前的 tasks 数组中添加一项,然后把之前的数组重新渲染
所以,数组的引用是没有变化的,如果把这个 tasks 作为属性传给 TaskList 这个 PureComponent 组件,
由于 tasks 引用没有变化,所以 TaskList 组件就不会重新渲染
state = {
tasks: [],
};
handleAdd = (newTask) => {
this.state.tasks.push(newTask);
this.setState({
tasks: this.state.tasks,
});
};
<TaskList tasks={this.state.tasks} />;
复制代码
handleAdd = (newTask) => {
this.setState({
tasks: [...this.state.tasks, newTask],
});
};
<AddTask onAdd={this.handleAdd} />
//下面这种写法和上面写法的区别在于,下面的写法每次重新渲染属性onAdd函数是不同的引用,上面的写法每次都是同一个函数引用
<AddTask onAdd={(newTask) => {
this.setState({
tasks: [...this.state.tasks, newTask],
});
}} />
复制代码
render props
有时候,某些组件的各种功能及其处理逻辑几乎完全相同,只是显示的界面不一样,建议下面的方式任选其一来解决重复代码的问题(横切关注点)
- render props
某个组件,需要某个属性
该属性是一个函数,函数的返回值用于渲染
函数的参数会传递为需要的数据
注意
纯组件的属性(尽量避免每次传递的 render props 的地址不一致)
通常该属性的名字叫做 render - HOC
import MouseListener from './MouseListener';
import React from 'react';
const renderPoint = (mouse) => (
<>
横坐标:{mouse.x},纵坐标:{mouse.y}
</>
);
const renderDiv = (mouse) => (
<>
<div
style={{
width: 100,
height: 100,
background: '#008c8c',
position: 'absolute',
left: mouse.x - 50,
top: mouse.y - 50,
}}
></div>
</>
);
export default function Test() {
return (
<div>
<MouseListener render={renderPoint} />
<MouseListener render={renderDiv} />
</div>
);
}
复制代码
import React, { PureComponent } from 'react';
import './style.css';
/**
* 该组件用于监听鼠标的变化
*/
export default class MouseListener extends PureComponent {
state = {
x: 0,
y: 0,
};
divRef = React.createRef();
handleMouseMove = (e) => {
//更新x和y的值
const { left, top } = this.divRef.current.getBoundingClientRect();
const x = e.clientX - left;
const y = e.clientY - top;
this.setState({
x,
y,
});
};
render() {
return (
<div
ref={this.divRef}
className="point"
onMouseMove={this.handleMouseMove}
>
{this.props.render ? this.props.render(this.state) : '默认值'}
</div>
);
}
}
复制代码
Portals
插槽:将一个 React 元素渲染到指定 DOM 容器中
ReactDOM.createPortal(React 元素, 真实的 DOM 容器),该函数返回一个 React 元素
import React from 'react';
import ReactDOM from 'react-dom';
function ChildA() {
return ReactDOM.createPortal(
<div
className="child-a"
style={{
marginTop: 200,
}}
>
<h1>ChildA</h1>
<ChildB />
</div>,
document.querySelector('.modal')
);
}
function ChildB() {
return (
<div className="child-b">
<h1>ChildB</h1>
</div>
);
}
export default function App() {
return (
<div
className="app"
onClick={(e) => {
console.log('App被点击了', e.target);
}}
>
<h1>App</h1>
<ChildA />
</div>
);
}
复制代码
注意事件冒泡
React 中的事件是包装过的
它的事件冒泡是根据虚拟 DOM 树来冒泡的,与真实的 DOM 树无关。
错误边界
默认情况下,若一个组件在渲染期间(render)发生错误,会导致整个组件树全部被卸载
错误边界:是一个组件,该组件会捕获到渲染期间(render)子组件发生的错误,并有能力阻止错误继续传播
让某个组件捕获错误
编写生命周期函数 getDerivedStateFromError (DOM has not yet been updated)
静态函数
运行时间点:渲染子组件的过程中,发生错误之后,在更新页面之前
注意:只有子组件发生错误,才会运行该函数
该函数返回一个对象,React 会将该对象的属性覆盖掉当前组件的 state
参数:错误对象
通常,该函数用于改变状态
编写生命周期函数 componentDidCatch (DOM has already been updated)
参数:错误对象,错误信息
实例方法
运行时间点:渲染子组件的过程中,发生错误,更新页面之后,由于其运行时间点比较靠后,因此不太会在该函数中改变状态
通常,该函数用于记录错误消息(该函数是卸载所有组件后再重新构造组件重新渲染)
getDerivedStateFromError:渲染备用 UI
componentDidCatch:打印错误信息
import React, { PureComponent } from 'react';
export default class ErrorBound extends PureComponent {
state = {
hasError: false,
};
static getDerivedStateFromError(error) {
console.log('发生错误了', error);
return {
hasError: true,
};
}
// componentDidCatch(error, info) {
// console.log('记录错误信息');
// }
render() {
// setTimeout(() => {
// throw new Error("asfasdfasf");
// }, 1000);
if (this.state.hasError) {
return <h1>出现错误了!</h1>;
}
return this.props.children;
}
}
复制代码
细节
某些错误,错误边界组件无法捕获
自身的错误
异步的错误
事件中的错误
服务端渲染
总结:仅处理渲染子组件期间的同步错误
React 中的事件
这里的事件:React 内置的 DOM 组件中的事件
给 document 注册事件
几乎所有的元素的事件处理,均在 document 的事件中处理
一些不冒泡的事件,是直接在元素上监听
一些 document 上面没有的事件,直接在元素上监听
在 document 的事件处理,React 会根据虚拟 DOM 树的完成事件函数的调用
React 的事件参数,并非真实的 DOM 事件参数,是 React 合成的一个对象,该对象类似于真实 DOM 的事件参数
stopPropagation,阻止事件在虚拟 DOM 树中冒泡
nativeEvent,可以得到真实的 DOM 事件对象
为了提高执行效率,React 使用事件对象池来处理事件对象
import React from 'react';
export default function App() {
return (
<div>
<button
onClick={(e) => {
console.log('react1: 按钮被点击了', e.nativeEvent);
e.nativeEvent.stopPropagation();
}}
>
按钮
</button>
</div>
);
}
document.querySelector('#root').onclick = function (e) {
console.log('root: 阻止onclick事件冒泡');
e.stopPropagation();
};
document.onclick = function (e) {
console.log('document: 阻止onclick事件冒泡');
// e.stopPropagation();
};
复制代码
注意事项
如果给真实的 DOM 注册事件,阻止了事件冒泡,则会导致 react 的相应事件无法触发
如果给真实的 DOM 注册事件,事件会先于 React 事件运行
通过 React 的事件中阻止事件冒泡,无法阻止真实的 DOM 事件冒泡
可以通过 nativeEvent.stopImmediatePropagation(),阻止 document 上剩余事件的执行
在事件处理程序中,不要异步的使用事件对象,如果一定要使用,需要调用 persist 函数
import React from 'react';
var prev;
export default function App() {
return (
<div
onClick={(e) => {
console.log(prev === e);
console.log('react: div被点击了');
}}
>
<input
type="text"
onFocus={(e) => {
console.log('react:文本获得了焦点');
}}
/>
<button
onClick={(e) => {
console.log('react: 按钮被点击了');
prev = e;
e.persist();
setTimeout(() => {
console.log(e.type);
}, 1000);
// e.nativeEvent.stopImmediatePropagation()
// console.log(e.isPropagationStopped());
// e.stopPropagation();
// console.log(e.isPropagationStopped());
}}
>
按钮
</button>
</div>
);
}
document.querySelector('#root').onFocus = function (e) {
console.log('阻止focus事件冒泡');
e.stopPropagation();
};
复制代码
渲染过程
渲染:生成用于显示的对象,以及将这些对象形成真实的 DOM 对象
- React 元素:React Element,通过 React.createElement 创建(语法糖:JSX)
例如:
<div><h1>标题</h1></div>
<App />
复制代码
-
React 节点:专门用于渲染到 UI 界面的对象,React 会通过 React 元素,创建 React 节点,ReactDOM 一定是通过 React 节点来进行渲染的
-
节点类型:
React DOM 节点:创建该节点的 React 元素类型是一个字符串(“h1″,”p”)
React 组件节点:创建该节点的 React 元素类型是一个函数或是一个类
React 文本节点:由字符串、数字创建的
React 空节点:由 null、undefined、false、true
React 数组节点:该节点由一个数组创建
真实 DOM:通过 document.createElement 创建的 dom 元素
import React from 'react';
class CompA extends React.Component {
state = {
a: 123,
b: 'abc',
};
componentDidUpdate(prevProps, prevState) {
console.log('CompA', prevProps, prevState, this.state);
console.log('CompA componentDidUpdate');
}
render() {
return (
<div>
<h1>{this.state.a}</h1>
<CompB n={this.state.b} />
<button
onClick={() => {
this.setState({
a: 321,
b: 'cba',
});
}}
>
点击
</button>
</div>
);
}
}
function CompB(props) {
return (
<div>
<h1 id="title">{props.n}</h1>
<CompC n={props.n} />
</div>
);
}
class CompC extends React.Component {
componentDidUpdate(prevProps, prevState) {
console.log('CompC', prevProps, prevState, this.props);
console.log('CompC componentDidUpdate');
}
render() {
var title = document.getElementById('title');
if (title) {
console.log(title.innerHTML);
} else {
console.log(title);
}
return (
<div>
<h1>{this.props.n}</h1>
</div>
);
}
}
export default class App extends React.Component {
render() {
return (
<div>
<CompA />
</div>
);
}
}
复制代码
首次渲染(新节点渲染)
- 通过参数的值创建节点
- 根据不同的节点,做不同的事情
- 文本节点:通过 document.createTextNode 创建真实的文本节点
- 空节点:什么都不做
- 数组节点:遍历数组,将数组每一项递归创建节点(回到第 1 步进行反复操作,直到遍历结束)
- DOM 节点:通过 document.createElement 创建真实的 DOM 对象,然后立即设置该真实 DOM 元素的各种属性,然后遍历对应 React 元素的 children 属性,递归操作(回到第 1 步进行反复操作,直到遍历结束)
- 组件节点
a. 函数组件:调用函数(该函数必须返回一个可以生成节点的内容),将该函数的返回结果递归生成节点(回到第 1 步进行反复操作,直到遍历结束)
b. 类组件:- 创建该类的实例
- 立即调用对象的生命周期方法:static getDerivedStateFromProps
- 运行该对象的 render 方法,拿到节点对象(将该节点递归操作,回到第 1 步进行反复操作)
- 将该组件的 componentDidMount 加入到执行队列(先进先出,先进先执行),当整个虚拟 DOM 树全部构建完毕,并且将真实的 DOM 对象加入到容器中后,执行该队列
- 生成出虚拟 DOM 树之后,将该树保存起来,以便后续使用
- 将之前生成的真实的 DOM 对象,加入到容器中。
更新节点
更新的场景:
重新调用 ReactDOM.render,触发根节点更新
在类组件的实例对象中调用 setState,会导致该实例所在的节点更新
节点的更新
如果调用的是 ReactDOM.render,进入根节点的对比(diff)更新
如果调用的是 setState
运行生命周期函数,static getDerivedStateFromProps
运行 shouldComponentUpdate,如果该函数返回 false,终止当前流程
运行 render,得到一个新的节点,进入该新的节点的对比更新
将生命周期函数 getSnapshotBeforeUpdate 加入执行队列,以待将来执行
将生命周期函数 componentDidUpdate 加入执行队列,以待将来执行
后续步骤:
更新虚拟 DOM 树
完成真实的 DOM 更新
依次调用执行队列中的 componentDidMount
依次调用执行队列中的 getSnapshotBeforeUpdate
依次调用执行队列中的 componentDidUpdate
对比更新
将新产生的节点,对比之前虚拟 DOM 中的节点,发现差异,完成更新
问题:对比之前 DOM 树中哪个节点
React 为了提高对比效率,做出以下假设
-
假设节点不会出现层次的移动(对比时,直接找到旧树中对应位置的节点进行对比)
-
不同的节点类型会生成不同的结构
相同的节点类型:节点本身类型相同,如果是由 React 元素生成,type 值还必须一致
其他的,都属于不相同的节点类型 -
多个兄弟通过唯一标识(key)来确定对比的新节点
key 值的作用:用于通过旧节点,寻找对应的新节点,如果某个旧节点有 key 值,则其更新时,会寻找相同层级中的相同 key 值的节点,进行对比。
key 值应该在一个范围内唯一(兄弟节点中),并且应该保持稳定
找到了对比的目标
判断节点类型是否一致
一致
根据不同的节点类型,做不同的事情
空节点:不做任何事情
DOM 节点:
直接重用之前的真实 DOM 对象
将其属性的变化记录下来,以待将来统一完成更新(现在不会真正的变化)
遍历该新的 React 元素的子元素,递归对比更新
文本节点:
直接重用之前的真实 DOM 对象
将新的文本变化记录下来,将来统一完成更新
组件节点:
函数组件:重新调用函数,得到一个节点对象,进入递归对比更新
类组件:
重用之前的实例
调用生命周期方法 getDerivedStateFromProps
调用生命周期方法 shouldComponentUpdate,若该方法返回 false,终止
运行 render,得到新的节点对象,进入递归对比更新
将该对象的 getSnapshotBeforeUpdate 加入队列
将该对象的 componentDidUpdate 加入队列
数组节点:遍历数组进行递归对比更新
不一致
整体上,卸载旧的节点,全新创建新的节点
创建新节点
进入新节点的挂载流程
卸载旧节点
文本节点、DOM 节点、数组节点、空节点、函数组件节点:直接放弃该节点,如果节点有子节点,递归卸载节点
类组件节点:
直接放弃该节点
调用该节点的 componentWillUnMount 函数
递归卸载子节点
import React, { Component } from 'react';
class CompA extends React.Component {
state = {
a: 123,
b: 'abc',
};
componentDidUpdate(prevProps, prevState) {
console.log('CompA componentDidUpdate');
}
render() {
return (
<div>
<h1>{this.state.a}</h1>
<CompB n={this.state.b} />
<button
onClick={() => {
this.setState({
a: 321,
b: 'cba',
});
}}
>
点击
</button>
</div>
);
}
}
function CompB(props) {
return (
<div>
<h1 id="title">{props.n}</h1>
<CompC n={props.n} />
</div>
);
}
class CompC extends React.Component {
componentDidUpdate(prevProps, prevState) {
console.log('CompC componentDidUpdate');
}
render() {
var title = document.getElementById('title');
if (title) {
console.log(title.innerHTML);
} else {
console.log(title);
}
return (
<div>
<h1>{this.props.n}</h1>
</div>
);
}
}
export default class App extends Component {
render() {
return (
<div>
<CompA />
</div>
);
}
}
复制代码
没有找到对比的目标
新的 DOM 树中有节点被删除
新的 DOM 树中有节点添加
创建新加入的节点
卸载多余的旧节点
工具
严格模式
StrictMode(React.StrictMode),本质是一个组件,该组件不进行 UI 渲染(React.Fragment <> </>),它的作用是,在渲染内部组件时,发现不合适的代码。
识别不安全的生命周期
关于使用过时字符串 ref API 的警告
关于使用废弃的 findDOMNode 方法的警告
检测意外的副作用
React 要求,副作用代码仅出现在以下生命周期函数中
ComponentDidMount
ComponentDidUpdate
ComponentWillUnMount
副作用:一个函数中,做了一些会影响函数外部数据的事情,例如:
异步处理
改变参数值
setState
本地存储
改变函数外部的变量
相反的,如果一个函数没有副作用,则可以认为该函数是一个纯函数
在严格模式下,虽然不能监控到具体的副作用代码,但它会将不能具有副作用的函数调用两遍,以便发现问题。(这种情况,仅在开发模式下有效)
检测过时的 context API
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
复制代码
Profiler
性能分析工具
分析某一次或多次提交(更新),涉及到的组件的渲染时间
火焰图:得到某一次提交,每个组件总的渲染时间以及自身的渲染时间
排序图:得到某一次提交,每个组件自身渲染时间的排序
组件图:某一个组件,在多次提交中,自身渲染花费的时间
HOOK 简介
HOOK 是 React16.8.0 之后出现
组件:无状态组件(函数组件)、类组件
类组件中的麻烦:
this 指向问题
繁琐的生命周期
其他问题
HOOK 专门用于增强函数组件的功能(HOOK 在类组件中是不能使用的),使之理论上可以成为类组件的替代品
官方强调:没有必要更改已经完成的类组件,官方目前没有计划取消类组件,只是鼓励使用函数组件
HOOK(钩子)本质上是一个函数(命名上总是以 use 开头),该函数可以挂载任何功能
HOOK 种类:
useState
useEffect
其他…
State Hook
State Hook 是一个在函数组件中使用的函数(useState),用于在函数组件中使用状态
useState
- 第 N 次调用 useState
- 检查该节点的状态数组是否存在下标 N
- 不存在
使用默认值创建一个状态
将该状态加入到状态数组中 下标为 N - 存在
忽略掉默认值
直接得到状态值
import React, { useState } from 'react';
export default function App() {
//uses 快捷键
const [n, setN] = useState(0);
return (
<div>
<button
onClick={() => {
setN(n - 1);
}}
>
-
</button>
<span>{n}</span>
<button
onClick={() => {
setN(n + 1);
}}
>
+
</button>
</div>
);
}
复制代码
函数有一个参数,这个参数的值表示状态的默认值
函数的返回值是一个数组,该数组一定包含两项
第一项:当前状态的值
第二项:改变状态的函数(调用这个函数会导致重新渲染)
一个函数组件中可以有多个状态,这种做法非常有利于横向切分关注点。
注意的细节
- useState 最好写到函数的起始位置,便于阅读
- useState 严禁出现在代码块(判断、循环)中
- useState 返回的函数(数组的第二项),引用不变(节约内存空间)
import React, { useState } from 'react';
window.arr = [];
export default function App() {
const [visible, setVisible] = useState(true);
const [n, setN] = useState(0);
window.arr.push(setN); //在浏览器上比较arr[0] === arr[1]结果是true
return (
<div>
<p style={{ display: visible ? 'block' : 'none' }}>
<button
onClick={() => {
setN(n - 1);
}}
>
-
</button>
<span>{n}</span>
<button
onClick={() => {
setN(n + 1);
}}
>
+
</button>
</p>
<button
onClick={() => {
setVisible(!visible);
}}
>
显示/隐藏
</button>
</div>
);
}
复制代码
- 使用函数改变数据,若数据和之前的数据完全相等(使用 Object.is 比较),不会导致重新渲染,以达到优化效率的目的。
import React, { useState } from 'react';
export default function App() {
console.log('App Render');
const [visible, setVisible] = useState(true);
const [n, setN] = useState(0);
return (
<div>
<p style={{ display: visible ? 'block' : 'none' }}>
<button
onClick={() => {
setN(n - 1);
}}
>
-
</button>
<span>{n}</span>
<button
onClick={() => {
setN(n + 1);
}}
>
+
</button>
</p>
<button
onClick={() => {
// setVisible(!visible); //这样是会重新渲染的
setVisible(visible); //这里没有改变visible的值 就不会重新渲染
}}
>
显示/隐藏
</button>
</div>
);
}
复制代码
- 使用函数改变数据,传入的值不会和原来的数据进行合并,而是直接替换。
import React, { useState } from 'react';
export default function App() {
console.log('App Render');
const [data, setData] = useState({
x: 1,
y: 2,
});
return (
<div>
<p>
x: {data.x},y:{data.y}
<button
onClick={() => {
setData({
...data,
x: data.x + 1,
});
//下面的这种写法会把{x: 1,y: 2}直接替换成{x: data.x + 1}
// setData({
// x: data.x + 1
// })
}}
>
x+1
</button>
</p>
</div>
);
}
复制代码
- 如果要实现强制刷新组件
类组件:使用 forceUpdate 函数
import React, { Component } from 'react';
export default class App extends Component {
render() {
return (
<div>
<button
onClick={() => {
//不会运行shouldComponentUpdate
this.forceUpdate(); //强制重新渲染
}}
>
强制刷新
</button>
</div>
);
}
}
复制代码
函数组件:使用一个空对象的 useState
import React, { useState } from 'react';
export default function App() {
console.log('App Render');
const [, forceUpdate] = useState({});
return (
<div>
<p>
<button
onClick={() => {
forceUpdate({});
}}
>
强制刷新
</button>
</p>
</div>
);
}
复制代码
-
如果某些状态之间没有必然的联系,应该分化为不同的状态,而不要合并成一个对象
-
和类组件的状态一样,函数组件中改变状态可能是异步的(在 DOM 事件中),多个状态变化会合并以提高效率,此时,不能信任之前的状态,而应该使用回调函数的方式改变状态。如果状态变化要使用到之前的状态,尽量传递函数。
import React, { useState } from 'react';
export default function App() {
console.log('App render');
const [n, setN] = useState(0); //使用一个状态,该状态的默认值是0
return (
<div>
<button
onClick={() => {
// setN(n - 1);
// setN(n - 1);
setN((prevN) => prevN - 1); //传入的函数,在事件完成之后统一运行
setN((prevN) => prevN - 1);
}}
>
-
</button>
<span>{n}</span>
<button
onClick={() => {
// setN(n + 1) //不会立即改变,事件运行完成之后一起改变
// setN(n + 1) //此时,n的值仍然是0
setN((prevN) => prevN + 1); //传入的函数,在事件完成之后统一运行
setN((prevN) => prevN + 1); //使用函数会依次执行每个函数所以结果是2
}}
>
+
</button>
</div>
);
}
复制代码
Effect Hook
Effect Hook:用于在函数组件中处理副作用
副作用:
ajax 请求
计时器
其他异步操作
更改真实 DOM 对象
本地存储
其他会对外部产生影响的操作
函数:useEffect,该函数接收一个函数作为参数,接收的函数就是需要进行副作用操作的函数
下面例子表示 卸载该函数组件 useEffect 是不会执行的,但是清理函数会执行
import React, { useState, useEffect } from 'react';
const ref = React.createRef();
window.timer = null; //计时器ID
function stop() {
clearInterval(window.timer); //清空之前的计时器
window.timer = null;
}
/**
* 一个可移动的块,该组件每次渲染完成后,始终从0,0坐标在10秒钟内,移动到目标点坐标
* @param {*} props
* props.left,要移动到的目标点横坐标
* props.top,要移动到的目标点纵坐标
*/
function MovableBlock(props) {
useEffect(() => {
console.log('render');
//渲染完成后
const div = ref.current;
let curTimes = 0; //当前移动的次数
const disX = props.left / 1000; //横坐标上每次移动的距离
const disY = props.top / 1000; //纵坐标上每次移动的距离
window.timer = setInterval(() => {
curTimes++; //移动次数+1
const newLeft = curTimes * disX;
const newTop = curTimes * disY;
div.style.left = newLeft + 'px';
div.style.top = newTop + 'px';
if (curTimes === 1000) {
stop();
}
}, 10);
return stop; //直接使用stop作为清理函数
});
return (
<div
ref={ref}
style={{
width: 100,
height: 100,
left: 0,
top: 0,
position: 'fixed',
background: '#f40',
}}
></div>
);
}
export default class App extends React.Component {
state = { x: 0, y: 0, visible: true };
render() {
console.log('App render');
return (
<div
style={{
paddingTop: 200,
}}
>
{this.state.visible && (
<div>
x:{' '}
<input
type="number"
value={this.state.x}
onChange={(e) => {
this.setState({
x: parseInt(e.target.value),
});
}}
/>
y: <input
type="number"
value={this.state.y}
onChange={(e) => {
this.setState({
y: parseInt(e.target.value),
});
}}
/>
<MovableBlock left={this.state.x} top={this.state.y} />
</div>
)}
<button
onClick={() => {
this.setState({
visible: !this.state.visible,
});
console.log(window.timer);
}}
>
显示/隐藏
</button>
</div>
);
}
}
复制代码
useEffect()的作用就是指定一个副效应函数,组件每渲染一次,该函数就自动执行一次。组件首次在网页 DOM 加载后,副效应函数也会执行。
如果第二个参数是一个空数组,就表明副效应参数没有任何依赖项。因此,副效应函数这时只会在组件加载进入 DOM 后执行一次,后面组件重新渲染,就不会再次执行。这很合理,由于副效应不依赖任何变量,所以那些变量无论怎么变,副效应函数的执行结果都不会改变,所以运行一次就够了。
细节
- 副作用函数的运行时间点,是在页面完成真实的 UI 渲染之后。因此它的执行是异步的,并且不会阻塞浏览器
与类组件中 componentDidMount 和 componentDidUpdate 的区别
componentDidMount 和 componentDidUpdate,更改了真实 DOM,但是用户还没有看到 UI 更新,同步的。
useEffect 中的副作用函数,更改了真实 DOM,并且用户已经看到了 UI 更新,异步的。 - 每个函数组件中,可以多次使用 useEffect,但不要放入判断或循环等代码块中。
- useEffect 中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数
该函数运行时间点,在每次运行副作用函数之前
首次渲染组件不会运行
组件被销毁时一定会运行 - useEffect 函数,可以传递第二个参数
第二个参数是一个数组
数组中记录该副作用的依赖数据
当组件重新渲染后,只有依赖数据与上一次不一样的时,才会执行副作用
所以,当传递了依赖数据之后,如果数据没有发生变化
副作用函数仅在第一次渲染后运行
清理函数仅在卸载组件后运行
实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。
import React, { useState, useEffect } from 'react';
function Test() {
useEffect(() => {
console.log('副作用函数,仅挂载时运行一次');
return () => {
console.log('清理函数,仅卸载时运行一次');
};
}, []); //使用空数组作为依赖项,则副作用函数仅在挂载的时候运行,如果不写第二个参数,每次渲染都会执行useEffect
console.log('渲染组件');
const [, forceUpdate] = useState({});
return (
<h1>
Test组件{' '}
<button
onClick={() => {
forceUpdate({});
}}
>
刷新组件
</button>
</h1>
);
}
export default function App() {
const [visible, setVisible] = useState(true);
return (
<div>
{visible && <Test />}
<button
onClick={() => {
setVisible(!visible);
}}
>
显示/隐藏
</button>
</div>
);
}
复制代码
- 副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化。
import React, { useState, useEffect } from 'react';
export default function App() {
const [n, setN] = useState(0);
useEffect(() => {
setTimeout(() => {
console.log(n); //n指向,当前App函数调用时的n
}, 5000);
});
return (
<div>
<h1>{n}</h1>
<button
onClick={() => {
setN(n + 1);
}}
>
n+1
</button>
</div>
);
}
复制代码
错误写法
import React, { useState, useEffect } from 'react';
//以下是错误的做法
export default function App() {
const [n, setN] = useState(10);
useEffect(() => {
//仅挂载后运行
const timer = setInterval(() => {
const newN = n - 1;
console.log(newN); //这里会一直打印9,说明这里的n并不是实时的n,每次拿到的n都是闭包10
setN(newN);
if (newN === 0) {
clearInterval(timer);
}
}, 1000);
return () => {
//函数卸载时运行
clearInterval(timer);
};
}, []); //没有依赖,说明只会执行一次
return (
<div>
<h1>{n}</h1>
<button
onClick={() => {
setN(n + 1);
}}
>
n+1
</button>
</div>
);
}
复制代码
正确写法
import React, { useState, useEffect } from 'react';
let timer;
export default function App() {
const [n, setN] = useState(10);
useEffect(() => {
if (n === 0) {
return;
}
//某一次渲染完成后,需要根据当前n的值,1秒后重新渲染
timer = setTimeout(() => {
setN(n - 1);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [n]);
return (
<div>
<h1>{n}</h1>
<button
onClick={() => {
setN(n + 1);
}}
>
n+1
</button>
</div>
);
}
复制代码
- 副作用函数在每次注册时,会覆盖掉之前的副作用函数,因此,尽量保持副作用函数稳定,否则控制起来会比较复杂。
import React, { useState, useEffect } from 'react';
let n = 1;
function func1() {
console.log('odd 副作用函数');
return () => {
console.log('odd 清理函数');
};
}
function func2() {
console.log('even 副作用函数');
return () => {
console.log('even 清理函数');
};
}
export default function App() {
const [, forceUpdate] = useState({});
useEffect(n % 2 === 0 ? func2 : func1);
n++;
return (
<div>
<button
onClick={() => {
forceUpdate({});
}}
>
强制刷新
</button>
</div>
);
}
复制代码
自定义 HOOK
State Hook: useState Effect Hook:useEffect
自定义 Hook:将一些常用的、跨越多个组件的 Hook 功能,抽离出去形成一个函数,该函数就是自定义 Hook,自定义 Hook,由于其内部需要使用 Hook 功能,所以它本身也需要按照 Hook 的规则实现:
- 函数名必须以 use 开头
- 调用自定义 Hook 函数时,应该放到顶层
例如:
- 很多组件都需要在第一次加载完成后,获取所有学生数据
// App.js
import React from 'react';
import useAllStudents from './components/common/CustomizeUse/useAllStudents';
function Test() {
const stus = useAllStudents();
const list = stus.map((it) => <li key={it.id}>{it.name}</li>);
return <ul>{list}</ul>;
}
export default function App() {
return (
<div>
<Test />
</div>
);
}
// useAllStudents.js
import { useEffect, useState } from 'react';
import { getAllStudents } from '../../service/index';
/**
* 当组件首次加载完成后,获取所有学生数据
*/
export default function useAllStudents() {
const [students, setStudents] = useState([]);
useEffect(() => {
(async () => {
const stus = await getAllStudents();
setStudents(stus);
})();
}, []);
return students;
}
复制代码
- 很多组件都需要在第一次加载完成后,启动一个计时器,然后在组件销毁时卸载
使用 Hook 的时候,如果没有严格按照 Hook 的规则进行,eslint 的一个插件(eslint-plugin-react-hooks)会报出警告
/* eslint "react-hooks/exhaustive-deps": "off" */
// App.js
import React, { useState } from 'react';
import useTimer from './components/common/CustomizeUse/useTimer';
function Test(props) {
useTimer(() => {
console.log('Test组件的一些副作用操作');
}, 1000);
return <h1>Test组件</h1>;
}
export default function App() {
const [visible, setVisible] = useState(true);
return (
<div>
{visible && <Test />}
<button
onClick={() => {
setVisible(!visible);
}}
>
隐藏/显示
</button>
</div>
);
}
//useTimer.js
/* eslint "react-hooks/exhaustive-deps": "off" */
import { useEffect } from 'react';
/**
* 组件首次渲染后,启动一个Interval计时器
* 组件卸载后,清除该计时器
*/
export default (func, duration) => {
useEffect(() => {
const timer = setInterval(func, duration);
return () => {
clearInterval(timer);
};
}, []);
};
复制代码
- 利用高阶组件
import React from 'react';
import { getAllStudents } from './components/service/index';
function withAllStudents(Comp) {
return class AllStudentsWrapper extends React.Component {
state = {
stus: [],
};
async componentDidMount() {
const stus = await getAllStudents();
this.setState({
stus,
});
}
render() {
return <Comp {...this.props} stus={this.state.stus} />;
}
};
}
function Test(props) {
const list = props.stus.map((it) => <li key={it.id}>{it.name}</li>);
return <ul>{list}</ul>;
}
const TestStudents = withAllStudents(Test);
export default function App() {
return (
<div>
<TestStudents />
</div>
);
}
复制代码
- 利用 prop render
import React from 'react';
import { getAllStudents } from './components/service/index';
class AllStudents extends React.Component {
state = {
stus: [],
};
async componentDidMount() {
const stus = await getAllStudents();
this.setState({
stus,
});
}
render() {
if (typeof this.props.render === 'function') {
return this.props.render(this.state.stus);
}
return null;
}
}
function Test(props) {
const list = props.stus.map((it) => <li key={it.id}>{it.name}</li>);
return <ul>{list}</ul>;
}
export default function App() {
return (
<div>
<AllStudents render={(stus) => <Test stus={stus} />} />
</div>
);
}
复制代码
- 练习 usePageStudent
//App.js
import React, { useState } from 'react';
import usePageStudents from './components/common/CustomizeUse/usePageStudents';
function Test() {
const [page, setPage] = useState(1);
const resp = usePageStudents(page, 10);
if (resp) {
const list = resp.findByPage.map((it) => <li key={it.id}>{it.name}</li>);
return (
<div>
<h1>数据总数:{resp.cont}</h1>
<ul>{list}</ul>
<input
type="number"
value={page}
onChange={(e) => {
setPage(parseInt(e.target.value));
}}
/>
</div>
);
}
return null;
}
export default function App() {
return (
<div>
<Test />
</div>
);
}
// usePageStudent.js
import { useEffect, useState } from 'react';
import { getStudents } from '../../service/index';
/**
* 根据页码和页容量获取学生数据,得到一个响应结果
* 并且,当页码和页容量变化时,将重新获取学生数据
*/
export default function useAllStudents(page = 1, limit = 10) {
const [resp, setResp] = useState();
useEffect(() => {
(async () => {
const resp = await getStudents(page, limit);
setResp(resp);
})();
}, [page, limit]);
return resp;
}
复制代码
Reducer Hook
Reducer Hook
Flux:Facebook 出品的一个数据流框架
- 规定了数据是单向流动的
- 数据存储在数据仓库中(目前,可以认为 state 就是一个存储数据的仓库)
- action 是改变数据的唯一原因(本质上就是一个对象,action 有两个属性)
type:字符串,动作的类型
payload:任意类型,动作发生后的附加信息
例如,如果是添加一个学生,action 可以描述为:
{ type:”addStudent”, payload: {学生对象的各种信息} }
例如,如果要删除一个学生,action 可以描述为:
{ type:”deleteStudent”, payload: 学生 id } - 具体改变数据的是一个函数,该函数叫做 reducer
该函数接收两个参数
state:表示当前数据仓库中的数据
action:描述了如何去改变数据,以及改变数据的一些附加信息
该函数必须有一个返回结果,用于表示数据仓库变化之后的数据
Flux 要求,对象是不可变的,如果返回对象,必须创建新的对象
reducer 必须是纯函数,不能有任何副作用
如果要触发 reducer,不可以直接调用,而是应该调用一个辅助函数 dispatch
该函数仅接收一个参数:action
该函数会间接去调用 reducer,以达到改变数据的目的
window.arr = [];
import { useState } from 'react';
/**
* 通用的useReducer函数
* @param {function} reducer reducer函数,标准格式
* @param {any} initialState 初始状态
* @param {function} initFunc 用于计算初始值的函数
*/
export default function useReducer(reducer, initialState, initFunc) {
const [state, setState] = useState(
initFunc ? initFunc(initialState) : initialState
);
function dispatch(action) {
const newState = reducer(state, action);
console.log(`日志:n的值 ${state}->${newState}`);
setState(newState);
}
return [state, dispatch];
}
复制代码
// import React, { useReducer } from 'react'
import React from 'react';
import useReducer from './components/other/useReducer';
/**
* 该函数,根据当前的数据,已经action,生成一个新的数据
* @param {*} state
* @param {*} action
*/
function reducer(state, action) {
switch (action.type) {
case 'increase':
return state + 1;
case 'decrease':
if (state === 0) {
return 0;
}
return state - 1;
default:
return state;
}
}
export default function App() {
console.log('重新渲染'); //useReducer中每次 setState(newState);都会直接导致重新执行App函数重新渲染(也就是依赖State Hook的组件会重新渲染)
const [n, dispatch] = useReducer(reducer, 10, (args) => {
console.log(args); //第三个参数是函数,该函数将useReducer的第二个参数作为参数
return 100;
});
//window.arr.push(dispatch); //arr[0]===arr[1] 结果是false,说明每次重新渲染后的dispatch不是同一个
return (
<div>
<button
onClick={() => {
dispatch({ type: 'decrease' });
}}
>
-
</button>
<span>{n}</span>
<button
onClick={() => {
dispatch({ type: 'increase' });
}}
>
+
</button>
</div>
);
}
复制代码
Context Hook
用于获取上下文数据
import React, { useContext } from 'react';
const ctx = React.createContext();
// function Test() {
// return <ctx.Consumer>
// {value => <h1>Test,上下文的值:{value}</h1>}
// </ctx.Consumer>
// }
function Test() {
const value = useContext(ctx);
return <h1>Test,上下文的值:{value}</h1>;
}
export default function App() {
return (
<div>
<ctx.Provider value="abc">
<Test />
</ctx.Provider>
</div>
);
}
复制代码
Callback Hook
函数名:useCallback
用于得到一个固定引用值的函数,通常用它进行性能优化
useCallback:
该函数有两个参数:
函数,useCallback 会固定该函数的引用,只要依赖项没有发生变化,则始终返回之前函数的地址
数组,记录依赖项
该函数返回:引用相对固定的函数地址
import React, { useState } from 'react';
class Test extends React.PureComponent {
// 由于每次传过来的props.onClick都是一个新的函数,所以每次父组件刷新就会导致子组件重新渲染
render() {
console.log('Test Render');
return (
<div>
<h1>{this.props.text}</h1>
<button onClick={this.props.onClick}>改变文本</button>
</div>
);
}
}
function Parent() {
console.log('Parent Render');
const [txt, setTxt] = useState(123);
const [n, setN] = useState(0);
return (
<div>
{/* 函数的地址每次渲染都发生了变化,导致了子组件跟着重新渲染,若子组件是经过优化的组件,则可能导致优化失效 */}
<Test
text={txt}
onClick={() => {
setTxt(Math.random());
}}
/>
<input
type="number"
value={n}
onChange={(e) => {
setN(parseInt(e.target.value));
}}
/>
</div>
);
}
export default function App() {
return (
<div>
<Parent />
</div>
);
}
复制代码
下面这种写法,直接也是返回一个新的函数,还是会导致重新渲染
import React, { useState } from 'react';
class Test extends React.PureComponent {
// 由于每次传过来的props.onClick每次都是一个新的函数,所以每次父组件刷新就会导致子组件重新渲染
render() {
console.log('Test Render');
return (
<div>
<h1>{this.props.text}</h1>
<button onClick={this.props.onClick}>改变文本</button>
</div>
);
}
}
function handleClick(setN) {
//返回的是新的函数
return function (e) {
setN(parseInt(e.target.value));
};
}
function Parent() {
console.log('Parent Render');
const [txt, setTxt] = useState(123);
const [n, setN] = useState(0);
return (
<div>
{/* 函数的地址每次渲染都发生了变化,导致了子组件跟着重新渲染,若子组件是经过优化的组件,则可能导致优化失效 */}
<Test
text={txt}
onClick={() => {
setTxt(Math.random());
}}
/>
<input type="number" value={n} onChange={handleClick(setN)} />
</div>
);
}
export default function App() {
return (
<div>
<Parent />
</div>
);
}
复制代码
解决办法:使用 useCallback
import React, { useState, useCallback } from 'react';
class Test extends React.PureComponent {
render() {
console.log('Test Render');
return (
<div>
<h1>{this.props.text}</h1>
<button onClick={this.props.onClick}>改变文本</button>
</div>
);
}
}
function Parent() {
console.log('Parent Render');
const [txt, setTxt] = useState(1);
const [n, setN] = useState(0);
const handleClick = useCallback(() => {
setTxt(txt + 1);
}, [txt]);
return (
<div>
{/* 函数的地址每次渲染都发生了变化,导致了子组件跟着重新渲染,若子组件是经过优化的组件,则可能导致优化失效 */}
<Test text={txt} onClick={handleClick} />
<input
type="number"
value={n}
onChange={(e) => {
setN(parseInt(e.target.value));
}}
/>
</div>
);
}
export default function App() {
return (
<div>
<Parent />
</div>
);
}
复制代码
Memo Hook
用于保持一些比较稳定的数据,通常用于性能优化
如果 React 元素本身的引用没有发生变化,一定不会重新渲染
import React, { useState, useMemo } from 'react';
class Test extends React.PureComponent {
render() {
console.log('Test Render');
return (
<div>
<h1>{this.props.text}</h1>
<button onClick={this.props.onClick}>改变文本</button>
</div>
);
}
}
function Parent() {
console.log('Parent Render');
const [txt, setTxt] = useState(1);
const [n, setN] = useState(0);
//useMemo与useCallback的区别是
// useMemo是基于返回值的,只要是依赖的数据不变(这里是txt),它的返回值总是指向同一个引用,不会发生变化(也就是handleClick),返回值可以是任何类型
// useCallback只能绑定的是一个函数,依赖的数据不变,函数引用就不变
const handleClick = useMemo(() => {
return () => {
setTxt(txt + 1);
};
}, [txt]);
return (
<div>
{/* 函数的地址每次渲染都发生了变化,导致了子组件跟着重新渲染,若子组件是经过优化的组件,则可能导致优化失效 */}
<Test text={txt} onClick={handleClick} />
<input
type="number"
value={n}
onChange={(e) => {
setN(parseInt(e.target.value));
}}
/>
</div>
);
}
export default function App() {
return (
<div>
<Parent />
</div>
);
}
复制代码
Ref Hook
useRef 函数:
一个参数:默认值
返回一个固定的对象,{current: 值}
使用 createRef 每次都会创建一个新的 ref
//arr[0] == arr[1] //false
import React, { useState, useRef } from 'react';
window.arr = [];
export default function App() {
const inpRef = React.createRef();
window.arr.push(inpRef); //这里和之前ref是函数的场景一样的,旧的传null,新的传input组件
//因此点击重新渲染三次后window.arr结果如下
// 0: {current: null}
// 1: {current: null}
// 2: {current: input}
const [n, setN] = useState(0);
return (
<div>
<input ref={inpRef} type="text" />
<button
onClick={() => {
console.log(inpRef.current.value);
}}
>
得到input的值
</button>
<input
type="number"
value={n}
onChange={(e) => {
setN(e.target.value); //每次点击重新渲染
}}
></input>
</div>
);
}
复制代码
使用 useRef 后每次都是同一个 ref
// arr[0] == arr[1] //true
import React, { useState, useRef } from 'react';
window.arr = [];
export default function App() {
const inpRef = useRef();
window.arr.push(inpRef);
const [n, setN] = useState(0);
return (
<div>
<input ref={inpRef} type="text" />
<button
onClick={() => {
console.log(inpRef.current.value);
}}
>
得到input的值
</button>
<input
type="number"
value={n}
onChange={(e) => {
setN(e.target.value);
}}
></input>
</div>
);
}
复制代码
使用 setInterval 定时器(差不多就相当于 setTimeout,和直接写 setTimeout 没有区别)
import React, { useState, useEffect } from 'react';
let timer = null;
export default function App() {
const [n, setN] = useState(10);
useEffect(() => {
//仅挂载后运行
if (n === 0) {
clearInterval(timer);
return;
}
timer = setInterval(() => {
//这里的setInterval可换成setTimeout
console.log(n - 1);
setN(n - 1);
}, 1000);
return () => {
//函数卸载时运行
clearInterval(timer);
};
}, [n]);
return (
<div>
<h1>{n}</h1>
<button
onClick={() => {
setN(n + 1);
}}
>
n+1
</button>
</div>
);
}
复制代码
使用 useRef 将 timer 放入每个 App 组件中(这样可以避免多个 App 组件的时候使用同一个 timer 的情况)
import React, { useState, useRef, useEffect } from 'react';
export default function App() {
const [n, setN] = useState(10);
const timerRef = useRef(); //这里如果你写let timer; 请看下一个js代码
useEffect(() => {
if (n === 0) {
return;
}
timerRef.current = setTimeout(() => {
console.log(n);
setN(n - 1);
}, 1000);
return () => {
clearTimeout(timerRef.current);
};
}, [n]);
return (
<div>
<h1>{n}</h1>
</div>
);
}
复制代码
import React, { useState, useRef, useEffect } from 'react';
export default function App() {
const [n, setN] = useState(10);
// const timerRef = useRef(); //这里如果你写let timer;好像也没有问题
let timer;
useEffect(() => {
if (n === 0) {
return;
}
timer = setTimeout(() => {
console.log(n);
setN(n - 1);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [n]);
return (
<div>
<h1>{n}</h1>
</div>
);
}
复制代码
使用 useRef 中的属性(每个组件中的 useRef 是不同的)
import React, { useState, useRef, useEffect } from 'react';
export default function App() {
const [n, setN] = useState(10);
const nRef = useRef(n); // {current:10}
//这里可以直接把nRef理解成一个属性 flag = n 然后再下面控制flag(请看下面代码)
useEffect(() => {
const timer = setInterval(() => {
nRef.current--;
setN(nRef.current);
if (nRef.current === 0) {
clearInterval(timer);
}
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return (
<div>
<h1>{n}</h1>
</div>
);
}
复制代码
import React, { useState, useRef, useEffect } from 'react';
export default function App() {
const [n, setN] = useState(10);
// const nRef = useRef(n); // {current:10}
var flag = n;
useEffect(() => {
const timer = setInterval(() => {
flag--;
setN(flag);
if (flag === 0) {
clearInterval(timer);
}
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return (
<div>
<h1>{n}</h1>
</div>
);
}
复制代码
ImperativeHandle Hook
函数:useImperativeHandleHook
import React, { useRef } from 'react';
class Test extends React.Component {
method() {
console.log('Test method called');
}
render() {
return <h1>Test Component</h1>;
}
}
export default function App() {
const testRef = useRef();
return (
<div>
<Test ref={testRef} />
<button
onClick={() => {
testRef.current.method();
}}
>
点击调用Test组件的method方法
</button>
</div>
);
}
复制代码
import React, { useRef, useImperativeHandle } from 'react';
function Test(props, ref) {
useImperativeHandle(
ref,
() => {
//如果不给依赖项,则每次运行函数组件都会调用该方法
//如果使用了依赖项,则第一次调用后,会进行缓存,只有依赖项发生变化时才会重新调用函数
// return 1;
// return 1;相当于给 ref.current = 1
return {
method() {
console.log('Test Component Called');
},
};
},
[]
);
return <h1>Test Component</h1>;
}
const TestWrapper = React.forwardRef(Test);
// class Test extends React.Component {
// method() {
// console.log("Test method called");
// }
// render() {
// return <h1>Test Component</h1>
// }
// }
export default function App() {
// const [, forceUpdate] = useState({})
const testRef = useRef();
return (
<div>
<TestWrapper ref={testRef} />
<button
onClick={() => {
testRef.current.method();
// console.log(testRef)
// forceUpdate({})
}}
>
点击调用Test组件的method方法
</button>
</div>
);
}
复制代码
官网的例子
function FancyInput(props, ref) {
// 这里ref拿到的是父组件的testRef
const inputRef = useRef();
useImperativeHandle(ref, () => ({
// useImperativeHandle第二个参数函数的返回值的父组件的testRef.current
focus: () => {
inputRef.current.focus();
},
}));
return <input ref={inputRef} />;
}
const TestWrapper = React.forwardRef(FancyInput);
export default function App() {
// const [, forceUpdate] = useState({})
const testRef = useRef();
return (
<div>
<TestWrapper ref={testRef} />
<button
onClick={() => {
// testRef.current.method();
console.log(testRef); //current: {focus: ƒ}
// forceUpdate({})
}}
>
点击调用Test组件的method方法
</button>
</div>
);
}
复制代码
LayoutEffect Hook
useEffect:浏览器渲染完成后,用户看到新的渲染结果之后
useLayoutEffectHook:完成了 DOM 改动,但还没有呈现给用户
应该尽量使用 useEffect,因为它不会导致渲染阻塞,如果出现了问题,再考虑使用 useLayoutEffectHook
原因:如果在 useLayoutEffectHook 中调用了 5 秒钟的 dom 操作,就会导致浏览器的渲染延迟,用户会看不到效果,同样体验不好
- 对 DOM 进行了改动同步(下面的 setN 函数)
- componentDidMount&componentDidUpdate(类组件中的生命钩子函数) 这里说函数组件 HOOK 就是 useLayoutEffectHook
- 浏览器下一次渲染时间点达到 对比差异,进行渲染 用户看到新的效果
- 调用 useEffect(所以这里改动 dom 会有闪烁的效果(重新渲染的效果))
import React, { useState, useLayoutEffect, useRef } from 'react';
export default function App() {
const [n, setN] = useState(0);
const h1Ref = useRef();
useLayoutEffect(() => {
//无闪烁
//调用时间相当于类组件的componentDidMount&componentDidUpdate
h1Ref.current.innerText = Math.random().toFixed(2);
});
// useEffect(() => { //有闪烁
// h1Ref.current.innerText = Math.random().toFixed(2);
// })
return (
<div>
<h1 ref={h1Ref}>{n}</h1>
<button
onClick={() => {
setN(n + 1);
}}
>
+
</button>
</div>
);
}
复制代码
DebugValue Hook
useDebugValue:用于将自定义 Hook 的关联数据显示到调试栏
如果创建的自定义 Hook 通用性比较高,可以选择使用 useDebugValue 方便调试
import React, { useState, useEffect, useDebugValue } from 'react';
function useTest() {
const [students] = useState([]);
useDebugValue('学生集合');
return students;
}
export default function App() {
useState(0);
useState('abc');
useEffect(() => {
console.log('effect');
}, []);
useTest();
return <div></div>;
}
复制代码