文章目录
React是由Facebook研发的、用于解决UI复杂度的开源JavaScript库,目前由React联合社区维护。
官网:https://react.docschina.org/
React的特点
- 轻量:React的开发版所有源码(包含注释)仅3000多行
- 原生:所有的React的代码都是用原生JS书写而成的,不依赖其他任何库
- 易扩展:React对代码的封装程度较低,也没有过多的使用魔法,所以React中的很多功能都可以扩展。
- 不依赖宿主环境:React只依赖原生JS语言,不依赖任何其他东西,包括运行环境。因此,它可以被轻松的移植到浏览器、桌面应用、移动端。
- 渐近式:React并非框架,对整个工程没有强制约束力。这对与那些已存在的工程,可以逐步的将其改造为React,而不需要全盘重写。
- 单向数据流:所有的数据自顶而下的流动
- 用JS代码声明界面
- 组件化
使用脚手架搭建工程
- 官方:create-react-app
- 第三方:next.js、umijs
React.createElement
-
创建一个React元素,称作虚拟DOM,本质上是一个对象
- 参数1:元素类型,如果是字符串,一个普通的HTML元素
- 参数2:元素的属性,一个对象
- 后续参数:元素的子节点
<div id="root"></div>
<!-- React的核心库,与宿主环境无关 -->
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<!-- 依赖核心库,将核心的功能与页面结合 -->
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script>
//创建一个span元素
var span = React.createElement("span", {}, "一个span元素");
//创建一个H1元素
var h1 = React.createElement("h1", {
title: "第一个React元素"
}, "Hello", "World", span);
ReactDOM.render(h1, document.getElementById("root"));
</script>
JSX
<div id="root"></div>
<!-- React的核心库,与宿主环境无关 -->
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<!-- 依赖核心库,将核心的功能与页面结合 -->
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<!-- 编译JSX -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
ReactDOM.render(<h1 title="第一个react元素"><span>一个span元素</span></h1>, document.getElementById('root'))
</script>
- 注意
- 凡是使用JSX的文件,必须导入React
- JS的扩展语法,需要使用babel进行转义
什么是JSX
- Facebook起草的JS扩展语法
- 本质是一个JS对象,会被babel编译,最终会被转换为React.createElement
- 每个JSX表达式,有且仅有一个根节点
- React.Fragment
- <></>
- 每个JSX元素必须结束(XML规范)
在JSX中嵌入表达式
- 在JSX中使用注释 {/* */}
- 将表达式作为内容的一部分
- null、undefined、false不会显示
- 普通对象,不可以作为子元素
- 可以放置React元素对象
- 将表达式作为元素属性
- 属性使用小驼峰命名法
- 防止注入攻击
- 自动编码
- dangerouslySetInnerHTML
元素的不可变性
- 虽然JSX元素是一个对象,但是该对象中的所有属性不可更改
- 如果确实需要更改元素的属性,需要重新创建JSX元素
组件和组件属性
组件:包含内容、样式和功能的UI单元
创建一个组件
特别注意:组件的名称首字母必须大写
函数组件
import React from "react";
export default function MyFnComp(props) {
return (<div>
这是函数组件
</div>)
}
- 注意
- 返回一个React元素
类组件
import React, { Component } from "react";
export default class MyClassComp extends Component {
render() {
return (<div>
这是类组件
</div>)
}
}
- 注意
-
必须继承React.Component
-
必须提供render函数,用于渲染组件
-
组件的属性
import React from 'react';
import ReactDOM from 'react-dom';
import MyFuncComp from "./MyFuncComp";
import MyClassComp from "./MyClassComp";
const comp = <MyFuncComp number={3}/>; //使用组件,生成的,仍然是一个React元素,变化的,只是type值
console.log(comp);
const div = <div title="Asdfa"></div>
console.log(div);
// const comp = <MyClassComp/>;
// console.log(comp);
ReactDOM.render(<div>
{/* {comp} */}
<MyFuncComp number="2" />
<MyFuncComp number={5} />
<MyFuncComp number={6} />
<MyFuncComp number={7} />
<MyClassComp number="2" ennable />
<MyClassComp number={5} obj={{
name: "xxx",
age: 100
}}/>
<MyClassComp number={6} ui={<h2>这是我传递属性</h2>}/>
<MyClassComp number={7} />
</div>, document.getElementById('root'));
- 对于函数组件,属性会作为一个对象的属性,传递给函数的参数
import React from 'react'
export default function MyFuncComp(props) {
// return <h1>函数组件的内容</h1>
return <h1>函数组件,目前的数字:{props.number}</h1>
}
- 对于类组件,属性会作为一个对象的属性,传递给构造函数的参数
import React from 'react'
export default class MyClassComp extends React.Component {
// constructor(props) {
// super(props); // this.props = props;
// console.log(props, this.props, props === this.props);
// }
/**
* 该方法必须返回React元素
*/
render() {
if (this.props.obj) {
return (
<>
<p>
姓名:{this.props.obj.name}
</p>
<p>
年龄:{this.props.obj.age}
</p>
</>
);
}
else if (this.props.ui) {
return (
<div>
<h1>下面是传入的内容</h1>
{this.props.ui}
</div>
);
}
return <h1>类组件的内容,数字:{this.props.number}</h1>
}
}
- 注意:
-
组件的属性,应该使用小驼峰命名法
-
组件无法改变自身的属性。
-
React元素,本质上,就是一个组件(内置组件)
-
数据属于谁,谁才有权力改动
-
React中的数据,自顶而下流动
-
组件状态
-
组件状态:组件可以自行维护的数据
-
组件状态仅在类组件中有效
-
状态(state),本质上是类组件的一个属性,是一个对象
状态初始化
//计时器,用作倒计时
import React, { Component } from 'react'
export default class Tick extends Component {
//初始化状态,JS Next 语法,目前处于实验阶段
state = {
left: this.props.number,
n: 123
}
constructor(props) {
super(props);
//初始化状态
// this.state = {
// left: this.props.number
// };
}
render() {
return (
<>
<h1>
{this.state.left}
</h1>
<p>
{this.state.n}
</p>
</>
)
}
}
状态的变化
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>
}
-
不能直接改变状态:因为React无法监控到状态发生了变化
-
必须使用this.setState({})改变状态
-
一旦调用了this.setState,会导致当前组件重新渲染
组件中的数据
- props:该数据是由组件的使用者传递的数据,所有权不属于组件自身,因此组件无法改变该数组
- state:该数组是由组件自身创建的,所有权属于组件自身,因此组件有权改变该数据
深入认识setState
setState,它对状态的改变,可能是异步的
如果改变状态的代码处于某个HTML元素的事件中,则其是异步的,否则是同步
import React, { Component } from 'react'
export default class Comp extends Component {
state = {
n: 0
}
handleClick = () => {
this.setState({
n: this.state.n + 1
});
console.log(this.state.n); //还没有重新渲染,说明目前状态仍然没有改变
}
render() {
console.log("render");
return (
<div>
<h1>
{this.state.n}
</h1>
<p>
<button onClick={this.handleClick}>+</button>
</p>
</div>
)
}
}
如果遇到某个事件中,需要同步调用多次,需要使用函数的方式得到最新状态
import React, { Component } from 'react'
export default class Comp extends Component {
state = {
n: 0
}
handleClick = () => {
this.setState(cur => {
//参数cur表示当前的状态
//该函数的返回结果,会混合(覆盖)掉之前的状态
//该函数是异步执行
return {
n: cur.n + 1
}
}, ()=>{
//所有状态全部更新完成,并且重新渲染后执行
console.log("state更新完成", this.state.n);
});
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>
)
}
}
最佳实践:
- 把所有的setState当作是异步的
- 永远不要信任setState调用之后的状态
- 如果要使用改变之后的状态,需要使用回调函数(setState的第二个参数)
- 如果新的状态要根据之前的状态进行运算,使用函数的方式改变状态(setState的第一个函数)
React会对异步的setState进行优化,将多次setState进行合并(将多次状态改变完成后,再统一对state进行改变,然后触发render)
事件
在React中,组件的事件,本质上就是一个属性
按照之前React对组件的约定,由于事件本质上是一个属性,因此也需要使用小驼峰命名法
如果没有特殊处理,在事件处理函数中,this指向undefined
- 使用bind函数,绑定this
import React, { Component } from 'react'
import Tick from "./Tick"
export default class TickControl extends Component {
state = {
isOver: false //倒计时是否完成
}
// constructor(props){
// super(props);
//. 绑定在原形上的方法再次绑定到对象上
// this.handleClick = this.handleClick.bind(this);
// this.handleOver = this.handleOver.bind(this);
// }
handleClick() {
console.log(this)
console.log("点击了")
}
handleOver() {
this.setState({
isOver: true
})
}
render() {
let status = "正在倒计时";
if (this.state.isOver) {
status = "倒计时完成";
}
return (
<div>
<Tick
onClick={this.handleClick.bind(this)}
onOver={this.handleClick.bind(this)}
number={10} />
<h2>
{status}
</h2>
</div>
)
}
}
- 使用箭头函数
import React, { Component } from 'react'
import Tick from "./Tick"
export default class TickControl extends Component {
state = {
isOver: false //倒计时是否完成
}
// 结果:handleClick不在原型上,而在对象上
handleClick = () => {
console.log(this)
console.log("点击了")
}
handleOver = () => {
this.setState({
isOver: true
})
}
render() {
let status = "正在倒计时";
if (this.state.isOver) {
status = "倒计时完成";
}
return (
<div>
<Tick
onClick={this.handleClick}
onOver={this.handleClick}
number={10} />
<h2>
{status}
</h2>
</div>
)
}
}
生命周期
生命周期:组件从诞生到销毁会经历一系列的过程,该过程就叫做生命周期。React在组件的生命周期中提供了一系列的钩子函数(类似于事件),可以让开发者在函数中注入代码,这些代码会在适当的时候运行。
生命周期仅存在于类组件中,函数组件每次调用都是重新运行函数,旧的组件即刻被销毁
旧版生命周期(React < 16.0.0)
import React, { Component } from 'react'
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>
)
}
}
- 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
- 通常在该函数中销毁一些组件依赖的资源,比如计时器
新版生命周期(React >= 16.0.0)
React官方认为,某个数据的来源必须是单一的
import React, { Component } from 'react'
export default class NewLifeCycle extends Component {
state = {
n: this.props.n
}
// static getDerivedStateFromProps(props, state) {
// console.log("getDerivedStateFromProps");
// // return null;//不改变当前状态
// return { //用新的对象替换掉之前的状态
// n: props.n
// }
// }
getSnapshotBeforeUpdate = (prevProps, prevState) => {
console.log("getSnapshotBeforeUpdate");
return 132;
}
componentDidUpdate(prevProps, prevState, snap) {
console.log("componentDidUpdate", snap);
}
render() {
return (
<div>
<h1>{this.state.n}</h1>
<p>
<button onClick={() => {
this.setState({
n: this.state.n + 1
})
}}>+1</button>
</p>
</div>
)
}
}
- getDerivedStateFromProps
- 通过参数可以获取新的属性和状态
- 该函数是静态的
- 该函数的返回值会覆盖掉组件状态
- 该函数几乎是没有什么用
- getSnapshotBeforeUpdate
- 真实的DOM构建完成,但还未实际渲染到页面中。
- 在该函数中,通常用于实现一些附加的dom操作
- 该函数的返回值,会作为componentDidUpdate的第三个参数
表单
受控组件和非受控组件
受控组件:组件的使用者,有能力完全控制该组件的行为和内容。通常情况下,受控组件往往没有自身的状态,其内容完全收到属性的控制。
非受控组件:组件的使用者,没有能力控制该组件的行为和内容,组件的行为和内容完全自行控制。
表单组件,默认情况下是非受控组件,一旦设置了表单组件的value属性,则其变为受控组件(单选和多选框需要设置checked)
属性默认值 和 类型检查
属性默认值
通过一个静态属性defaultProps
告知react属性默认值
属性类型检查
使用库:prop-types
对组件使用静态属性propTypes
告知react如何检查属性
PropTypes.any://任意类型
PropTypes.array://数组类型
PropTypes.bool://布尔类型
PropTypes.func://函数类型
PropTypes.number://数字类型
PropTypes.object://对象类型
PropTypes.string://字符串类型
PropTypes.symbol://符号类型
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, 高阶组件,以组件作为参数,并返回一个组件
通常,可以利用HOC实现横切关注点。
举例:20个组件,每个组件在创建组件和销毁组件时,需要作日志记录
20个组件,它们需要显示一些内容,得到的数据结构完全一致
注意
- 不要在render中使用高阶组件
- 不要在高阶组件内部更改传入的组件
ref
reference: 引用
场景:希望直接使用dom元素中的某个方法,或者希望直接使用自定义组件中的某个方法
- ref作用于内置的html组件,得到的将是真实的dom对象
- ref作用于类组件,得到的将是类的实例
- ref不能作用于函数组件
ref不再推荐使用字符串赋值,字符串赋值的方式将来可能会被移出
目前,ref推荐使用对象或者是函数
对象
通过 React.createRef 函数创建
import React, { Component } from 'react'
export default class Comp extends Component {
constructor(props){
super(props);
this.txt = React.createRef();
}
handleClick = () => {
this.txt.current.focus();
}
render() {
return (
<div>
<input ref={this.txt} type="text" />
<button onClick={this.handleClick}>聚焦</button>
</div>
)
}
}
函数
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>
)
}
}
函数的调用时间:
- componentDidMount的时候会调用该函数
- 在componentDidMount事件中可以使用ref
- 如果ref的值发生了变动(旧的函数被新的函数替代),分别调用旧的函数以及新的函数,时间点出现在componentDidUpdate之前
- 旧的函数被调用时,传递null
- 新的函数被调用时,传递对象
- 如果ref所在的组件被卸载,会调用函数
谨慎使用ref
能够使用属性和状态进行控制,就不要使用ref。
- 调用真实的DOM对象中的方法
- 某个时候需要调用类组件的方法
Ref转发
forwardRef
forwardRef方法:
- 参数,传递的是函数组件,不能是类组件,并且,函数组件需要有第二个参数来得到ref
- 返回值,返回一个新的组件
import React from 'react'
function A(props, ref) {
return <h1 ref={ref}>
组件A
<span>{props.words}</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} words="asfsafasfasfs" />
{/* this.ARef.current: h1 */}
</div>
)
}
}
如何在类组件使用 forwardRef
import React from 'react'
class A extends React.Component {
render() {
return <h1 ref={this.props.abc}>
组件A
<span>{this.props.words}</span>
</h1>
}
}
const NewA = React.forwardRef((props, ref) => {
return <A {...props} abc={ref} />
})
export default class App extends React.Component {
ARef = React.createRef()
componentDidMount() {
console.log(this.ARef);
}
render() {
return (
<div>
<NewA ref={this.ARef} words="asfsafasfasfs" />
{/* this.ARef.current: h1 */}
</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
}
function ChildA(props, context) {
return <div>
<h1>ChildA</h1>
<h2>a:{context.a},b:{context.b}</h2>
<ChildB />
</div>
}
ChildA.contextTypes = types;
class ChildB extends React.Component {
/**
* 声明需要使用哪些上下文中的数据
*/
static contextTypes = types
render() {
return <p>
ChildB,来自于上下文的数据:a: {this.context.a}, b:{this.context.b}
<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函数返回什么结果)
Portals
插槽:将一个React元素渲染到指定的DOM容器中
ReactDOM.createPortal(React元素, 真实的DOM容器),该函数返回一个React元素
注意事件冒泡
- React中的事件是包装过的
- 它的事件冒泡是根据虚拟DOM树来冒泡的,与真实的DOM树无关。
技巧
render props
有时候,某些组件的各种功能及其处理逻辑几乎完全相同,只是显示的界面不一样,建议下面的方式认选其一来解决重复代码的问题(横切关注点)
- render props
- 某个组件,需要某个属性
- 该属性是一个函数,函数的返回值用于渲染
- 函数的参数会传递为需要的数据
- 注意纯组件的属性(尽量避免每次传递的render props的地址不一致)
- 通常该属性的名字叫做render
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>
)
}
- HOC
import withMouseListener from "./withMouseListener";
import React from 'react'
function Point(props) {
return <>横坐标:{props.x},纵坐标:{props.y}</>
}
function MoveDiv(props) {
return (
<div style={{
width: 100,
height: 100,
background: "#008c8c",
position: "absolute",
left: props.x - 50,
top: props.y - 50
}}>
</div>);
}
const MousePoint = withMouseListener(Point);
const MouseDiv = withMouseListener(MoveDiv);
export default function Test() {
return (
<div>
<MousePoint/>
<MouseDiv/>
</div>
)
}
错误边界
默认情况下,若一个组件在渲染期间(render)发生错误,会导致整个组件树全部被卸载
错误边界:是一个组件,该组件会捕获到渲染期间(render)子组件发生的错误,并有能力阻止错误继续传播
让某个组件捕获错误
- 编写生命周期函数 getDerivedStateFromError
- 静态函数
- 运行时间点:渲染子组件的过程中,发生错误之后,在更新页面之前
- 注意:只有子组件发生错误,才会运行该函数
- 该函数返回一个对象,React会将该对象的属性覆盖掉当前组件的state
- 参数:错误对象
- 通常,该函数用于改变状态
- 编写生命周期函数 componentDidCatch
- 实例方法
- 运行时间点:渲染子组件的过程中,发生错误,更新页面之后,由于其运行时间点比较靠后,因此不太会在该函数中改变状态
- 通常,该函数用于记录错误消息
细节
某些错误,错误边界组件无法捕获
- 自身的错误
- 异步的错误
- 事件中的错误
总结:仅处理渲染子组件期间的同步错误
渲染原理
渲染:生成用于显示的对象,以及将这些对象形成真实的DOM对象
- React元素:React Element,通过React.createElement创建(语法糖:JSX)
- 例如:
<div><h1>标题</h1></div>
<App />
- React节点:专门用于渲染到UI界面的对象,React会通过React元素,创建React节点,ReactDOM一定是通过React节点来进行渲染的
- 节点类型:
- React DOM节点:创建该节点的React元素类型是一个字符串
- React 组件节点:创建该节点的React元素类型是一个函数或是一个类
- React 文本节点:由字符串、数字创建的
- React 空节点:由null、undefined、false、true
- React 数组节点:该节点由一个数组创建
- 真实DOM:通过document.createElement创建的dom元素
首次渲染(新节点渲染)
- 通过参数的值创建节点
- 根据不同的节点,做不同的事情
- 文本节点:通过document.createTextNode创建真实的文本节点
- 空节点:什么都不做
- 数组节点:遍历数组,将数组每一项递归创建节点(回到第1步进行反复操作,直到遍历结束)
- DOM节点:通过document.createElement创建真实的DOM对象,然后立即设置该真实DOM元素的各种属性,然后遍历对应React元素的children属性,递归操作(回到第1步进行反复操作,直到遍历结束)
- 组件节点
- 函数组件:调用函数(该函数必须返回一个可以生成节点的内容),将该函数的返回结果递归生成节点(回到第1步进行反复操作,直到遍历结束)
- 类组件:
- 创建该类的实例
- 立即调用对象的生命周期方法:static getDerivedStateFromProps
- 运行该对象的render方法,拿到节点对象(将该节点递归操作,回到第1步进行反复操作)
- 将该组件的componentDidMount加入到执行队列(先进先出,先进先执行),当整个虚拟DOM树全部构建完毕,并且将真实的DOM对象加入到容器中后,执行该队列
- 生成出虚拟DOM树之后,将该树保存起来,以便后续使用
- 将之前生成的真实的DOM对象,加入到容器中。
const app = <div className="assaf">
<h1>
标题
{["abc", null, <p>段落</p>]}
</h1>
<p>
{undefined}
</p>
</div>;
ReactDOM.render(app, document.getElementById('root'));
以上代码生成的虚拟DOM树:
function Comp1(props) {
return <h1>Comp1 {props.n}</h1>
}
function App(props) {
return (
<div>
<Comp1 n={5} />
</div>
)
}
const app = <App />;
ReactDOM.render(app, document.getElementById('root'));
以上代码生成的虚拟DOM树:
class Comp1 extends React.Component {
render() {
return (
<h1>Comp1</h1>
)
}
}
class App extends React.Component {
render() {
return (
<div>
<Comp1 />
</div>
)
}
}
const app = <App />;
ReactDOM.render(app, document.getElementById('root'));
以上代码生成的虚拟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函数
- 递归卸载子节点
没有找到对比的目标
新的DOM树中有节点被删除
新的DOM树中有节点添加
- 创建新加入的节点
- 卸载多余的旧节点
工具
严格模式
StrictMode(React.StrictMode
),本质是一个组件,该组件不进行UI渲染(React.Fragment <> </>
),它的作用是,在渲染内部组件时,发现不合适的代码。
- 识别不安全的生命周期
- 关于使用过时字符串 ref API 的警告
- 关于使用废弃的 findDOMNode 方法的警告
- 检测意外的副作用
- React要求,副作用代码仅出现在以下生命周期函数中
-
- ComponentDidMount
-
- ComponentDidUpdate
-
- ComponentWillUnMount
副作用:一个函数中,做了一些会影响函数外部数据的事情,例如:
- 异步处理
- 改变参数值
- setState
- 本地存储
- 改变函数外部的变量
相反的,如果一个函数没有副作用,则可以认为该函数是一个纯函数
在严格模式下,虽然不能监控到具体的副作用代码,但它会将不能具有副作用的函数调用两遍,以便发现问题。(这种情况,仅在开发模式下有效)
- 检测过时的 context API
Profiler
性能分析工具
分析某一次或多次提交(更新),涉及到的组件的渲染时间
火焰图:得到某一次提交,每个组件总的渲染时间以及自身的渲染时间
排序图:得到某一次提交,每个组件自身渲染时间的排序
组件图:某一个组件,在多次提交中,自身渲染花费的时间
HOOK
HOOK是React16.8.0之后出现
组件:无状态组件(函数组件)、类组件
类组件中的麻烦:
-
this指向问题
-
繁琐的生命周期
-
其他问题
HOOK专门用于增强函数组件的功能(HOOK在类组件中是不能使用的),使之理论上可以成为类组件的替代品
官方强调:没有必要更改已经完成的类组件,官方目前没有计划取消类组件,只是鼓励使用函数组件
HOOK(钩子)本质上是一个函数(命名上总是以use开头),该函数可以挂载任何功能
State Hook
State Hook是一个在函数组件中使用的函数(useState),用于在函数组件中使用状态
useState
- 函数有一个参数,这个参数的值表示状态的默认值
- 函数的返回值是一个数组,该数组一定包含两项
- 第一项:当前状态的值
- 第二项:改变状态的函数
一个函数组件中可以有多个状态,这种做法非常有利于横向切分关注点。
注意的细节
- useState最好写到函数的起始位置,便于阅读
- useState严禁出现在代码块(判断、循环)中
- useState返回的函数(数组的第二项),引用不变(节约内存空间)
- 使用函数改变数据,若数据和之前的数据完全相等(使用Object.is比较),不会导致重新渲染,以达到优化效率的目的。
- 使用函数改变数据,传入的值不会和原来的数据进行合并,而是直接替换。
- 如果要实现强制刷新组件
- 类组件:使用forceUpdate函数
- 函数组件:使用一个空对象的useState
- 如果某些状态之间没有必然的联系,应该分化为不同的状态,而不要合并成一个对象
- 和类组件的状态一样,函数组件中改变状态可能是异步的(在DOM事件中),多个状态变化会合并以提高效率,此时,不能信任之前的状态,而应该使用回调函数的方式改变状态。如果状态变化要使用到之前的状态,尽量传递函数。
Effect Hook
Effect Hook:用于在函数组件中处理副作用
副作用:
- ajax请求
- 计时器
- 其他异步操作
- 更改真实DOM对象
- 本地存储
- 其他会对外部产生影响的操作
函数:useEffect,该函数接收一个函数作为参数,接收的函数就是需要进行副作用操作的函数
细节
- 副作用函数的运行时间点,是在页面完成真实的UI渲染之后。因此它的执行是异步的,并且不会阻塞浏览器
- 与类组件中componentDidMount和componentDidUpdate的区别
- componentDidMount和componentDidUpdate,更改了真实DOM,但是用户还没有看到UI更新,同步的。
- useEffect中的副作用函数,更改了真实DOM,并且用户已经看到了UI更新,异步的。
- 每个函数组件中,可以多次使用useEffect,但不要放入判断或循环等代码块中。
- useEffect中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫做清理函数
- 该函数运行时间点,在每次运行副作用函数之前
- 首次渲染组件不会运行
- 组件被销毁时一定会运行
- useEffect函数,可以传递第二个参数
- 第二个参数是一个数组
- 数组中记录该副作用的依赖数据
- 当组件重新渲染后,只有依赖数据与上一次不一样的时,才会执行副作用
- 所以,当传递了依赖数据之后,如果数据没有发生变化
- 副作用函数仅在第一次渲染后运行
- 清理函数仅在卸载组件后运行
- 副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化。
- 副作用函数在每次注册时,会覆盖掉之前的副作用函数,因此,尽量保持副作用函数稳定,否则控制起来会比较复杂。
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,以达到改变数据的目的
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];
}
Context Hook
用于获取上下文数据
import React, { useContext } from 'react'
const ctx = React.createContext();
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会固定该函数的引用,只要依赖项没有发生变化,则始终返回之前函数的地址
- 数组,记录依赖项
该函数返回:引用相对固定的函数地址
Memo Hook
用于保持一些比较稳定的数据,通常用于性能优化
如果React元素本身的引用没有发生变化,一定不会重新渲染
Ref Hook
useRef函数:
- 一个参数:默认值
- 返回一个固定的对象,
{current: 值}
ImperativeHandle Hook
函数:useImperativeHandleHook
useImperativeHandle(ref, () => {
//如果不给依赖项,则每次运行函数组件都会调用该方法
//如果使用了依赖项,则第一次调用后,会进行缓存,只有依赖项发生变化时才会重新调用函数
//相当于给 ref.current = 1
return {
method(){
console.log("Test Component Called")
}
}
}, [])
LayoutEffect Hook
useEffect:浏览器渲染完成后,用户看到新的渲染结果之后
useLayoutEffectHook:完成了DOM改动,但还没有呈现给用户
应该尽量使用useEffect,因为它不会导致渲染阻塞,如果出现了问题,再考虑使用useLayoutEffectHook
DebugValue Hook
useDebugValue:用于将自定义Hook的关联数据显示到调试栏
如果创建的自定义Hook通用性比较高,可以选择使用useDebugValue方便调试
React动画
React动画库:react-transition-group
CSSTransition
当进入时,发生:
- 为CSSTransition内部的DOM根元素(后续统一称之为DOM元素)添加样式enter
- 在一下帧(enter样式已经完全应用到了元素),立即为该元素添加样式enter-active
- 当timeout结束后,去掉之前的样式,添加样式enter-done
当退出时,发生:
- 为CSSTransition内部的DOM根元素(后续统一称之为DOM元素)添加样式exit
- 在一下帧(exit样式已经完全应用到了元素),立即为该元素添加样式exit-active
- 当timeout结束后,去掉之前的样式,添加样式exit-done
设置classNames属性,可以指定类样式的名称
- 字符串:为类样式添加前缀
- 对象:为每个类样式指定具体的名称(非前缀)
关于首次渲染时的类样式,appear、apear-active、apear-done,它和enter的唯一区别在于完成时,会同时加入apear-done和enter-done
还可以与Animate.css联用
TransitionGroup
该组件的children,接收多个Transition或CSSTransition组件,该组件用于根据这些子组件的key值,控制他们的进入和退出状态