React 核心概念
# JSX🌿
jsx 中嵌入表达式,在 {}
中写表达式,那什么是表达式呢?如下
小插曲:js 表达式与 js 语句的区别
- 表达式:一个表达式会产生一个值,可以放到任何一个需要该值的地方 比如:a, b, a + b, demo(), function test(){} 等都是表达式
- 语句(代码):控制程序走向的代码片段 比如:if(){}, for(){}, switch(){} 等
jsx
会被 babel
编译为 React.createElement
,即我们编写的 jsx
会通过一个函数(React.createElement
)转换成虚拟 DOM
,这也是为什么我们要导入 React
的原因。但在 React
最新版(脚手架)中好像不需要我们导入 React
了,让我们看看它(jsx
)内部到底做了些什么:
const element = <div><span>hello</span></div> //编译为如下内容
const element = React.createElement("div", null, React.createElement("span", null, "hello"));
// createElement 源码
export function createElement(type, config, children) {
let propName;
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
// 将 config 处理后赋值给 props
// ...省略
}
const childrenLength = arguments.length - 2;
// 处理 children,会被赋值给props.children
// ...省略
// 处理 defaultProps
// ...省略
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// 标记这是个 React Element
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
return element;
};
提示
在 React 17.x 之后 JSX 的转换不再需要 React.createElement,故而你也无需通过 import React from 'react';
此种方式导入 React 以进行 JSX 转换。因为此种 JSX 转换的方式并不完美,理由如下
- 如果使用 JSX,则需在 React 的环境下,因为 JSX 将被编译成 React.createElement。
- 有一些 React.createElement 无法做到的性能优化和简化。
所以在 React 17.x 之后你可以直接这样写
function App() {
return <h1>Hello World</h1>;
}
现在将转换为:
// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
# 两种组件
类组件
import { Component } from 'react' export default class Clock extends Component { constructor(props){ super(props); this.state = { count: 0 } } handleClick = () => { // 当下一个状态依赖于上一个状态时推荐使用函数形式 this.setState((state) => ({count: state.count + 1})) // this.setState({count: this.state.count + 1}) // 不推荐 } render() { return ( <div> <h1>hello Clock</h1> <div>{this.state.count}</div> <button onClick={this.handleClick}>increase</button> </div> ) } }
函数组件
import React, { useState } from 'react' export default function Pick(props) { const [num, setNum] = useState(0) return ( <div> <h1>Function Component</h1> <div>{num}</div> <button onClick={() => { setNum(num + 1) }}>increase</button> </div> ) }
# 状态与属性
单项数据流(自上而下)
state:组件内部自己维护的数据,只能由组件自身更改
类组件通过 this.state 获取状态数据,只能通过 this.setState 改变状态数据
props:组件外部传入的数据,组件自身不可更改
类组件通过 this.props 获取外部传入的数据,但不可更改 props 数据
# 深入认识 setState🌿
setState<K extends keyof S>(
state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
callback?: () => void
): void;
「参数」
- state:可为返回状态对象的函数,也可以为一个状态对象
- callback:回调函数,该回调函数执行的时机在于状态更改之后并且是在
render
(一个常用的生命周期钩子) 函数调用之后
setState,它对状态的改变,可能是异步的
如果改变状态的代码处于某个 HTML 元素的事件中,则其是异步的,否则是同步(如页面挂载完毕后开启一个定时器,在该定时器内部调用 setState 改变状态就是同步的)
如果遇到某个事件中,需要同步调用多次,需要使用函数的方式得到最新状态
「最佳实践」
- 把所有的
setState
当作是异步的 - 永远不要信任
setState
调用之后的状态 - 如果要使用改变之后的状态,需要使用回调函数( setState 的第二个参数)
- 如果新的状态要依赖于之前的状态,则使用函数的方式改变状态
React
会对异步的setState
进行优化,将多次setState
进行合并(将多次状态改变完成后,再统一对state
进行改变,然后触发render
)
# 事件处理🌿
核心在于事件中的 this
指向(针对类组件而言)
题外话:其实 react 最 nb 的一点就是它并没有改变原生 js 的特性(如原型、原型链、闭包、this 指向等),所以相对于 vue 来说 react 对初学者 js 基础的要求比较高。
上面这一句话如何理解?请看下面的例子
import { Component } from 'react'
export default class Clock extends Component {
constructor(props){
super(props);
this.state = {
count: 0
}
// 在构造函数中绑定 this
// this.handleClick = this.handleClick.bind(this)
}
// 箭头函数形式绑定 this
handleClick = () => {
this.setState((state) => {
return {
count: state.count + 1
}
})
}
render() {
return (
<div>
<h1>hello Clock</h1>
<div>{this.state.count}</div>
<button onClick={this.handleClick}>increase</button>
</div>
)
}
}
在构造函数中绑定
this
this.handleClick = this.handleClick.bind(this)
- 此处的
this
表示的是组件实例。 - 构造函数的执行要先于
render
函数(涉及到组件的生命周期)
程序是从右往左读的,右边
this.handleClick.bind(this)
,表示的是先从组件实例上找handleClick
,没有的话再沿着原型链上找(原型链上刚好有handleClick
),直到找到handleClick
,然后绑定this
(组件实例)并返回一个新的函数给组件实例对象的一个属性handleClick
,当你点击按钮的时候这时组件实例上已经有一个属性handleClick
,所以事件执行的其实就是这个组件实例的属性handleClick
函数,它不会再沿着原型链上找handleClick
执行了,因为对象本身就已经有了。- 此处的
箭头函数的形式(实验阶段)
handleClick = () => { this.setState((state) => { return { count: state.count + 1 } }) }
把一个箭头函数赋值给组件实例上的一个属性
handleClick
,箭头函数内部的this
指向的就是组件实例本身。因为箭头函数本身没有this
,箭头函数里的this
指向的是箭头函数所身处的那个环境中的this
直接在
onClick
属性中写箭头函数(不推荐)<button onClick={() => this.handleClick()}>increase</button>
箭头函数本身是没有
this
的,箭头函数中的this
指的是箭头函数本身所身处的那个环境中的this
该箭头函数所身处的环境中的
this
即为 render 函数中的this
,而render
函数中的this
指的就是组件实例对象了
# 非受控组件
数据不是即时获取的,当你点击触发事件的时候才会获取相关数据,并且该数据并未保存到组件的 state 状态中,理解非受控组件还是得要先理解受控组件呀
import React, { Component } from 'react'
export default class Demo extends Component {
handleSubmit = (event) => {
event.preventDefault();
const { username, password } = this;
console.log(username.value, password.value)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input ref={c => this.username = c} type="text" />
<input ref={c => this.password = c} type="password" />
<button>登入</button>
</form>
)
}
}
# 受控组件
定义:在通过表单元素输入数据的同时会自动将你输入的数据保存到 state 状态中,当你需要获取表单数据时会从 state 状态中获取,表现在代码上就是 value 和 onChange props 的绑定,看代码就明白了
import React, { Component } from 'react'
// 受控组件
export default class Demo extends Component {
state = {
username: "",
password: ""
}
handleSubmit = event => {
event.preventDefault();// 阻止表单的提交
const {username, password} = this.state;
alert(`你输入的用户名:${username},密码:${password}`)
}
handleUsername = event => {
this.setState({username: event.target.value})
}
handlePassword = event => {
this.setState({password: event.target.value})
}
render() {
return (
<form action="" onSubmit={this.handleSubmit}>
<input type="text" value={this.state.username} onChange={this.handleUsername} />
<input type="password" value={this.state.password} onChange={this.handlePassword} />
<button>登入</button>
</form>
)
}
}
如上代码实现的功能(受控组件)和 vue
的双向数据绑定非常相似,只是说法不同而已
受控组件主要是针对表单组件而言的,常见的表单组件有:
普通输入框:
<input type="text" name="name" />
使之变成受控组件需添加一个属性:
- onChange:当你改变表单数据的时候会自动触发该函数并传入一个事件对象作为参数,在该函数内部先通过事件参数获取到表单输入的数据,然后调用 setState 以将表单中的数据更新到 state 状态中
形式如下:
<input type="text" name="name" value={this.state.value} onChange={this.handleChange}/>
普通文本域:
<textarea>你好, 这是在 text area 里的文本</textarea>
使之变成受控组件需添加一个属性:
- onChange:当你改变表单数据的时候会自动触发该函数并传入一个事件对象作为参数,在该函数内部先通过事件参数获取到表单输入的数据,然后调用 setState 以将表单中的数据更新到 state 状态中
形式如下:
<textarea value={this.state.value} onChange={this.handleChange} />
密码框:
<input type="password" />
使之变成受控组件需添加一个属性:
- onChange:当你改变表单数据的时候会自动触发该函数并传入一个事件对象作为参数,在该函数内部先通过事件参数获取到表单输入的数据,然后调用 setState 以将表单中的数据更新到 state 状态中
形式如下:
<input type="password" name="name" value={this.state.value} onChange={this.handleChange}/>
下拉列表:
<select> <option value="grapefruit">葡萄柚</option> <option value="lime">酸橙</option> <option selected value="coconut">椰子</option> <option value="mango">芒果</option> </select>
使之变成受控组件需添加一个属性:
- onChange:当你改变表单数据的时候会自动触发该函数并传入一个事件对象作为参数,在该函数内部先通过事件参数获取到表单输入的数据,然后调用 setState 以将表单中的数据更新到 state 状态中
形式如下:
<select value={this.state.value} onChange={this.handleChange}> <option value="grapefruit">葡萄柚</option> <option value="lime">酸橙</option> <option value="coconut">椰子</option> <option value="mango">芒果</option> </select>
多选框
单选框
# 生命周期🌿🌿
生命周期:组件从诞生到销毁会经历一系列的过程,该过程就叫做生命周期。React 在组件的生命周期中提供了一系列的钩子函数(类似于事件),可以让开发者在函数中注入代码,这些代码会在适当的时候运行。
生命周期仅存在于类组件中,函数组件每次调用都是重新运行函数,旧的组件即刻被销毁
- 旧版生命周期
- 新版生命周期
「总结」
新版生命周期与旧版生命周期的不同:componentWillMount、componentWillReceiveProps、componentWillUpdate 这三个生命周期钩子在未来版本会被弃用,随之添加了 getDerivedStateFromProps、getSnapshotBeforeUpdate 这两个生命周期钩子
# 旧版生命周期
「React < 16.0.0」
- 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官方认为,某个数据的来源必须是单一的
- getDerivedStateFromProps
- 通过参数可以获取新的属性和状态
- 该函数是静态的
- 该函数的返回值会覆盖掉组件状态
- 该函数几乎是没有什么用
- getSnapshotBeforeUpdate
- 真实的 DOM 构建完成,但还未实际渲染到页面中。
- 在该函数中,通常用于实现一些附加的 dom 操作
- 该函数的返回值,会作为 componentDidUpdate 的第三个参数
# Context
结构:A 组件中套了 B 组件,B 组件中套了 C 组件
import React, { Component, createContext } from 'react'
// 创建一个带有默认值{username: "henry",age:12}的上下文对象,当然你也可以不传递默认值
const MyContext = createContext({ username: "henry", age: 12 })
// 从上下文对象中解构出两个组件 Provider, Consumer
const { Provider, Consumer } = MyContext
export default class A extends Component {
render() {
return (
<>
<h1>我是 A 组件</h1>
{/* <B /> */}
<Provider value={{username: "jack", age: 90}}>
<B />
</Provider>
</>
)
}
}
class B extends Component {
render() {
return (
<>
<h1>我是 B 组件</h1>
<C />
</>
)
}
}
class C extends Component {
// 指定 contextType 读取当前的 MyContext。
// 相当于给组件实例的 context 属性赋值,而赋予的值来自于最近的父组件所提供(通过 Provider 组件的 value 属性提供)的值
// 若父组件未通过 Provider 组件所包裹,则赋予当时创建上下文对象的默认值
// 但该方式有局限性:只适用于类组件
static contextType = MyContext
render() {
console.log(this.context)
const { username } = this.context;
return (
<>
<Consumer>
{/*
若该组件的父组件中有有被 Provider 组件所包裹,则下面的 value 值会赋值为 Provider 组件中所提供的 value 值,
否则就赋值为当初创建上下文对象时的默认值
一句话解释:就近原则
*/}
{value => {
console.log(value)
return <h1>我是 C 组件,我从上下文中获取到的用户名:{value.username}</h1>
}}
</Consumer>
</>
)
}
}
// function C() {
// return (
// <>
// <Consumer>
// {(value) => {
// console.log(value)
// return <h1>我是 C 组件,我从上下文中获取到的用户名:{value.username}</h1>
// }}
// </Consumer>
// </>
// )
// }
「最佳实践」
Provider, Consumer 两个组件要么同时使用,要么同时不用
# 组件优化
Component 的 2 个问题
- 只要执行 setState,即使不改变状态数据,组件也会重新调用 render
- 只要当前组件重新 render,就会自动重新 render 子组件,纵使子组件没有用到父组件的任何数据
以上两个问题都会造成效率低的结果
效率高的做法
只有当前组件的 state 或 props 发生改变时才重新 render
效率低下的原因
Component 的 shoudComponentUpdate 生命周期钩子默认情况总是返回 true
解决方式
重写 shoudComponentUpdate 生命周期钩子
比较新旧 state 或 props 数据,如果有变化才返回 true,否则返回 false
使用 PureComponent
PureComponent 重写了 shoudComponentUpdate 生命周期钩子,只有当 state 或 props 数据有变化时才返回 true,否则返回 false
注意⚠️:PureComponent 中的 shoudComponentUpdate 对 state 和 props 比较只是浅比较,如果只是数据对象内部发生变化就会返回 false。所以不要直接修改 state,而是要产生新数据
项目中一般使用 PureComponent 来进行优化
# key 属性🌿
虚拟 DOM 中 key 的作用
简单地说:key 是虚拟 DOM 对象的标识,在更新显示时 key 起着极其重要的作用
详细地说:当状态中的数据发生变化时,react 中会根据「新数据」生成「新的虚拟DOM」,随后 React 会进行「新虚拟 DOM」 与「旧虚拟 DOM」 的 diff 比较,比较规则如下:
「旧虚拟 DOM」中找到了与「新虚拟 DOM」相同的 key
- 若虚拟 DOM 中内容没变,直接使用之前真实的 DOM
- 若虚拟 DOM 中内容变了,则生成新的真实 DOM,随后替换掉页面中之前的真实 DOM
「旧虚拟 DOM」中未找到与「新虚拟 DOM」相同的 key
- 根据数据创建新的真实 DOM,随后渲染到页面
用 index 作为 key 可能会引发的问题
若对数据进行逆序添加、逆序删除等破环顺序的操作,会产生没有必要的真实 DOM 更新,界面效果没问题,但效率低
如果结构中还包含输入类的 DOM,会产生错误 DOM 的更新,界面有问题
注意⚠️:如果不存在对数据的逆序添加、逆序删除等破环顺序的操作,仅用于渲染列表的展示,使用 index 作为 key 是没有问题的
开发中如何选择 key?
- 最好使用每条数据的唯一标识作为 key,比如 id、手机号、学号、身份证号等唯一值
- 如果确定只是简单的展示数据,用 index 也是可以的
# 高阶函数
如果一个函数符合下面两个规范中的任何一个,那该函数就是高阶函数
- 若 A 函数,接收的参数是一个函数,那么 A 就可以称之为高阶函数
- 若 A 函数,调用时其返回值依然是一个函数,那么 A 就可以称之为高阶函数
函数的柯里化:通过函数调用继续返回函数的方式,实现多次接受参数,最后统一处理的函数编码形式