Skip to main content

类组件

类组件的生命周期​

image-20220115165144277

详见 官方生命周期图。

挂载阶段​

该阶段表示组件首次渲染到页面中,会依次执行如下生命周期函数:

  • constructor,用于组件的初始化,比如状态初始化、响应事件绑定
  • getDerivedStateFromProps(不常用)
  • render,返回虚拟 DOM,最终会被转换为真实 DOM 渲染在浏览器中
  • componentDidMount,真实 DOM 渲染在页面中后会立刻执行该函数

更新阶段​

当我们给页面中已挂载完毕的组件执行如下操作:

  • 父组件重新渲染
  • 父组件传入新的 props 给当前组件
  • 当前组件调用 setState 方法
  • 当前组件调用 forceUpdate 方法

就会让当前组件进入更新阶段,会依次执行如下的生命周期函数:

  • getDerivedStateFromProps(不常用)
  • shouldComponentDidMount
  • render,返回新的虚拟 DOM,新的虚拟 DOM 和旧的虚拟 DOM 会进行 Diff 比较,然后根据比较结果,准备更新真实 DOM 上的部分节点
  • getSnapshotBeforeUpdate(不常用)
  • componentDidUpdate,页面中的真实 DOM 更新完毕后,会执行该函数

卸载阶段​

新的虚拟 DOM 和旧的虚拟 DOM 进行 Diff 比较后,如果某个组件被标记为移除,那么该组件在真实 DOM 中被移除之前会执行如下生命周期函数:

  • componentWillUnmount,该函数执行完毕后,组件就会从真实 DOM 中移除
important
  • display: none 不会导致组件卸载,它仅是在 CSS 层面隐藏了组件
  • 路由组件切换时,会导致旧组件的卸载和新组件的挂载

生命周期函数中要做的事情​

constructor

constructor(props) {
super(props);
// 不要在这里调用 this.setState()
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}

在该生命周期函数中,我们一般进行对组件的初始化:

  • 通过 this.state 初始化组件状态
  • 通过 bind 为事件响应函数绑定 this
important

一定要尽量避免将 props 的值赋值给 state,这是初学者非常常见的一种错误!

render

在类组件中唯一必须实现的方法,该方法必须是一个纯函数,且返回的数据类型必须为下面类型之一:

  • JSX 创建的 React 元素
  • 数组,会将数组中的每个元素作为虚拟 DOM 的节点
  • Fragment
  • Portals
  • 字符串、数值
  • 布尔类型、null、undefined,不会在页面中渲染
function Test() {
return (
<>
[1, 2, 3] {/* JSX 中要渲染数组,必须加上大括号,这里实际上就是一段文本 */}
{[1, 2, 3]}
{null}
{undefined}
{true}
{"hello"}
</>
);
}

componentDidMount

调用该方法时,组件已经挂载完毕,真实的 DOM 树已经渲染到了页面中,我们会在该生命周期函数中进行如下操作:

  • 执行一些异步操作,比如网络请求
  • 进行原生事件监听,比如我们要操作 DOM,添加 addEventListener
  • 进行消息订阅,比如 redux 中的 store.subscribe

需要注意的是,该函数只会在组件进行挂载时调用。

componentDidUpdate

componentDidUpdate(prevProps, prevState, snapshot);

React 根据 Diff 新旧虚拟 DOM 差异,更新了真实 DOM 后,会立刻执行该函数,这里的 preProps、preState 都是更新前的数据。

假如我们有一个 Counter 组件,并且实现了该生命周期函数:

componentDidUpdate(preProps, preState, snapshot) {
console.log("cur:", this.state.count);
console.log("pre:", preState.count);
}

每次 Counter 递增时,preState.count 总是 this.state 的上一次值。

另外,这里的 snapshot 参数是 getSnapshotBeforeUpdate 生命周期函数的返回值,默认情况下为 undefined。

我们能在 componentDidUpdate 生命周期函数中做些什么?

  • 直接操作某些 DOM,比如某些组件更新时,可能需要伴随滚动条的移动,此时就需要通过 js 操控滚动条
  • 判断 this.props 和 preProps 来进行新的网络请求
  • 也可以调用 setState,但是必须包裹在一个条件语句内,不然会造成生命周期死循环
componentDidUpdate(prevProps) {
// 典型用法(不要忘记比较 props):
if (this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}

componentWillUnmount

该方法会在组件销毁之前进行调用,一般会在该方法中执行一些清理操作:

  • 清除 timer、取消网络请求
  • 清除在 componentDidMount 中的消息订阅、事件监听

shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState);

该生命周期函数仅用于类组件的性能优化,目的是减少 render 函数的执行次数,从而避免生成新的虚拟 DOM 和旧的虚假 DOM 进行 Diff 比较。

当组件进入更新阶段时,nextProps 和 nextState 会接收到此次更新传来的 props 和 state,我们可以把它们和当前的 this.props 与 this.state 进行比较,来决定是否生成新的虚拟 DOM:

  • 返回 true,则执行 render 方法
  • 返回 false,则不执行
important
  • 该函数返回 false 并不会阻止状态的更新,只会阻断 render 及其以后的生命周期函数的执行
  • 使用 forceUpdate 可以强制将新的状态数据生成虚拟 DOM(跳过 SCU)

getDerivedStateFromProps

static getDerivedStateFromProps(props, state)

这是一个静态方法,此方法无权访问组件实例,即不可以在方法内部使用 this.props / this.state。

该方法的作用是,将 props 派生给 state,默认情况下会返回 state。

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate(prevProps, prevState);

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。

此用法并不常见,但它可能出现在 UI 处理中,如需要以特殊方式处理滚动位置的聊天线程等。

class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}

getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动位置以便我们稍后调整滚动位置。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}

componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}

render() {
return <div ref={this.listRef}>{/* ...contents... */}</div>;
}
}

在上述示例中,重点是从 getSnapshotBeforeUpdate 读取 scrollHeight 属性,因为 “render” 阶段生命周期(如 render)和 “commit” 阶段生命周期(如 getSnapshotBeforeUpdate 和 componentDidUpdate)之间可能存在延迟。

类组件中的 this​

setState 详解​

state 不可变值原理​

在类组件中,this.state 是只读的,更新状态时,我们不能直接去修改它,必须使用 this.setState 去更新状态,才能让组件进入生命周期的更新阶段。

如果我们通过直接修改 this.state 来更新状态:

render() {
return (
<button onClick={(e) => {this.state.count += 1;}>
{this.state.count}
</button>
);
}

实际上组件的状态也是更新了的,效果和 this.setState({ count: this.state.count + 1 }) 一样,唯一不同的是,无法让组件进入生命周期的更新阶段。

setState 状态数据合并​

比如我们在类组件中具有下面的状态:

this.state = {
name: "kll",
age: 18,
};

当我们使用 setState 更新状态的时候底层是使用了 Object.assign 来合并状态数据,因此我们修改部分数据并不会影响状态里的别的数据:

this.setState({ name: "dwd" }); // { name: 'dwd', age: 18 }

setState 异步和同步更新状态​

异步更新状态 —— 批处理​

当我们正常使用 setState 时,即 在组件的事件响应函数内部调用 setState,它是 异步更新状态 的。

render() {
return (
<button
onClick={() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
}}
>
{this.state.count}
</button>
);
}

在调用 setState 更新状态后,立刻打印 this.state,控制台中显示的结果还是更新前的状态。

React 这样做的目的是 实现批量更新机制,比如当触发一个点击事件时,可能伴随多次 setState 的调用,但是 React 并不会在每次 setState 调用后都让组件进入更新阶段,而是对所有的 setState 进行一次性批量处理,下面是伪代码:

const newState = Object.assign({}, oldState);

Object.assign(newState, state1);
Object.assign(newState, state2);
Object.assign(newState, state3);

// ... 最后带着 newState 进入组件的更新阶段生命周期函数
  • 这里的 state1、state2、state3 可以理解为触发一次点击事件导致多次调用 setState 所传入的参数
  • 调用 Object.assign 方法,将 oldState(更新前的状态)和此次批量更新的状态进行对象属性合并

具体可以参考下面的例子:

const oldState = { count: 0, test: "" }; // 组件更新前的状态
const newState = Object.assign({}, oldState);

const state1 = { count: 1, test: "b" }; // setState({ count: this.state.count + 1, test: "b" });
Object.assign(newState, state1);

const state2 = { count: 2, test: "a" }; // setState({ count: this.state.count + 1, test: "a" });
Object.assign(newState, state2);

const state3 = { count: 3 }; // setState({ count: this.state.count + 1 });
Object.assign(newState, state3);

下面来做一个面试题,当点击两次按钮后,组件的 this.state 变为什么?

class Button extends Component {
state = {
count: 0,
test: "",
};

handleClick1 = () => {
this.setState({ count: this.state.count + 1, test: "b" });
console.log("div click");
};

handleClick2 = () => {
this.setState({ count: this.state.count + 2 });
console.log("button click");
};

render() {
return (
<div onClick={this.handleClick1}>
<button onClick={this.handleClick2}>{this.state.count}</button>
</div>
);
}
}

先来回顾一下关于事件的冒泡,对于一个父子元素,我们点击父元素时,实际上事件的触发顺序是由内向外的(冒泡),因此这道题中,我们点击按钮时,事件响应函数的调用顺序是 handleClick2 --> handleClick1。

点击两次按钮可以分为两次 setState 的状态批处理:

// 第一次批处理
const oldState = { count: 0, test: "" }; // 等价于 this.state
const newState = Object.assign({}, oldState); // { count: 0, test: "" }

const state1 = { count: this.state.count + 2 };
Object.assign(newState, state1); // { count: 2, test: "" }

const state2 = { count: this.state.count + 1, test: "b" };
Object.assign(newState, state2); // { count: 1, test: "b" }
// 第二次批处理
const oldState = { count: 1, test: "b" }; // 等价于 this.state
const newState = Object.assign({}, oldState); // { count: 1, test: "b" }

const state1 = { count: this.state.count + 2 };
Object.assign(newState, state1); // { count: 3, test: "b" }

const state2 = { count: this.state.count + 1, test: "b" };
Object.assign(newState, state2); // { count: 2, test: "b" }

好了,关于 setState 批处理的机制差不多就这么多,你现在应该知道:

  • setState 在事件响应函数中调用是异步的
  • 设计成异步的目的就是实现在一次事件触发过程中按顺序批量处理所有的 setState,减少组件的渲染次数

下面我们来讨论在 事件响应函数 中 setState 传入一个函数的情况。这里直接说结论,当 setState 传入一个函数时,函数中的参数是同一次批处理中 newState 合并的上一个调用 setState 的对象参数或者函数参数返回值。描述得有点复杂,直接看下面的例子。

class Button extends Component {
state = { count: 0 };

handleClick = () => {
this.setState({ count: this.state.count + 1 });
this.setState((state) => {
return { count: state.count + 1 };
});
};

render() {
return <button onClick={this.handleClick}>{this.state.count}</button>;
}
}

对于第 6 行的 setState 传入一个函数,内部的 state 参数如下:

const oldState = { count: 0 };
const newState = Object.assign({}, oldState);

const state1 = { count: 1 };
Object.assign(newState, state1);

// 此时将 newState 作为参数传入到第 6 行 setState 的函数参数中

如果当前 setState 前一个调用 setState 传入的也是一个函数:

handleClick = () => {
this.setState((state) => ({ count: state.count + 1 }));
this.setState((state) => {
return { count: state.count + 1 };
});
};

那么就会将它的返回值与 newState 合并,最终作为参数传入给当前 setState 的函数参数。

这样一来,函数参数中的 state 参数就可以保存上一个 setState 与 newState 合并的结果了,而不是和对象参数那样直接覆盖同名属性。

现在你能说清楚下面两段代码的区别吗?

handleClick = () => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
};
handleClick = () => {
this.setState((state, props) => ({ count: state.count + 1 }));
this.setState((state, props) => ({ count: state.count + 1 }));
this.setState((state, props) => ({ count: state.count + 1 }));
};

获取 setState 异步更新后的状态​

使用 componentDidUpdate 生命周期函数

我们调用 setState 异步更新了状态后,会立刻调用 render 方法,render 方法调用完毕,页面重新渲染,接着就会调用 componentDidUpdate 生命周期函数:

componentDidUpdate() {
console.log(this.state.count);
}

由于已经异步更新了状态,并且还执行了 render 方法,那么此时方法内部打印的状态就和页面中的一致了。

使用 setState 第二个参数

我们之前使用 setState,只会向里面传入一个对象或者一个函数,其实它还有第二个参数是一个回调函数,当 setState 异步更新完状态后,会调用该函数:

this.setState({ count: this.state.count + 1 }, () => {
console.log(this.state.count);
});

此时打印的数据就和页面中的数据一致了。

important

一定要注意分清楚,setState 第一个参数为函数和第二个参数为函数的区别:

handleClick = () => {
this.setState((state, props) => ({ count: state.count + 1 }));
};

第一个参数为函数时,它的返回值会用来更新组件状态,会被传入 state 和 props 作为参数,这里的 state 参数在前文已经仔细分析过了。

handleClick = () => {
this.setState({ count: state.count + 1 }, () => {
console.log(this.state);
});
};

第二个参数为函数时,它仅是一个回调函数,且不会被传入任何参数,当 componentDidUpdate 生命周期函数执行完毕后,即 Diff 的结果都已经提交了,此回调函数才会被执行,官方建议用 componentDidUpdate 代替该函数。

同步更新状态​

把 setState 放入定时器中执行

setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
}, 0);

把 setState 放入原生事件监听的回调函数中

componentDidMount() {
document.getElementById('btn')?.addEventListener('click', () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
})
}

Props 组件通信​

父传子​

在父组件中,直接把想要传递的数据通过 props 给子组件:

function Father(props) {
return (
<div>
<Child data='momo' />
</div>
)
}

function Child(props) {
return <div>{props.data}</div>
}

子传父​

让父组件通过 props 传递一个函数给子组件,当子组件调用这个函数的时候,父组件那边定义的函数也会被调用:

function Father(props) {
const handleSubmit = (e, data) => {
console.log(e, data);
}
return (
<div>
<Child onSubmit={handleSubmit} />
</div>
)
}

function Child(props) {
const handleClick = (e, data) => {
props.onSubmit(e, data);
}
return <div onClick={(e) => handleClick(e, 100)}>Child</div>
}

Context 组件通信​

在开发过程中常见的数据传递方式就是通过 props 自上而下的传递,但是对于一些场景:

  • UI 主题
  • 地区偏好
  • 用户登陆状态

如果我们在顶层 App 组件中定义这些数据,然后通过 props 一层层传下去,那么对于一些中间层不需要这些数据的组件来说是一种冗余操作。

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。Context 常用于跨组件的通信,其设计目的是为了共享那些对于一个组件树而言是 "全局" 的数据。

API 概览​

API描述
React.createContext创建一个需要共享的 Context 对象
Context.Provider由 Context 对象返回的组件,它可以让消费组件订阅 Provider 的 value 变化,如果 value 变化,那么消费组件会重新渲染
Class.contextType用于 class 组件订阅 Provider 的 value,订阅后可以通过 this.context 获取
Context.Consumer一般用于函数组件订阅 Provider 的 value,订阅后可以通过回调函数的 value 参数获取
Context.displayName不常用,作用是给 React DevTools 确定在组件树中 Context 对象的展示名称

下面我们依次使用上面的 API 来实现一个效果:

pic.nm

在 App 组件内越过 Father 组件,直接给 Child 组件传递数据。

React.createContext​

使用 React.createContext 创建一个 context 对象。

context.js
import { createContext } from "react";

export const NumberContext = createContext(0); // 默认共享 value 为 0

Context.Provider​

获取 context 对象上的 Provider 组件,让其包裹需要共享 "全局" 数据的根组件,并利用 value 属性来传递数据。

"App.jsx
import React, { useState } from "react";
import Child from "./child";
import Father from "./father";
import { NumberContext } from "./context";

function App() {
const [number, setNumber] = useState(0);

return (
<div className="app">
app: {number}
{/* 从 context 对象上获取 Provider 组件,value 为需要共享的数据 */}
<NumberContext.Provider value={number}>
<Father>
<Child />
</Father>
</NumberContext.Provider>
<button onClick={() => setNumber(number + 1)}>+1</button>
</div>
);
}

export default App;

Class.contextType​

在由 NumberContext.Provider 包裹的组件树中,如果某个组件需要使用共享的数据,可以为其绑定 contextType 属性为 NumberContext,然后再使用 this.context 获取到共享数据。

"Child.jsx
import React from 'React'
import { NumberContext } from './context';

class Child extends React.Component {
render() {
return <div className="child">child: {this.context}</div>;
}
}

Child.contextType = NumberContext; // Child 组件订阅了 NumberContext

export default Child;

此时 Child 组件就对 NumberContext.Provider 上的 value 属性进行了监听,如果 Child 组件从当前节点开始往上匹配没有找到 Number.Provider 那么 this.context 就会取 NumberContext 设置的默认值。

important

使用 contextType 订阅 context 对象来监听 Provider 的 value 变化,这种方式只适合类组件。

Context.Consumer​

对于函数组件,需要使用 context 对象上的 Consumer 组件来订阅 Provider 的 value 变化。

Child.jsx
import React from 'React'
import { NumberContext } from './context';

const Child = (props) => {
return (
<NumberContext.Consumer>
{(value) => (
<div className="child">child: {value}</div>
)}
</NumberContext.Consumer>
)
};

export default Child;

需要注意的是,Consumer 组件内部是一个回调函数,一旦 Provider 上的 value 属性发生变化,就会回调 Consumer 内部的函数并传入 value 参数。

消费组件向顶层组件传值​

原理和利用 props 子传父一样,就是给 Provider 的 value 传入一个函数,然后消费组件在恰当时机传入相应的参数调用这个函数,这样在顶层组件中该函数会被回调,接受到消费组件传来的参数。

嵌套 Provider​

当我们的组件树需要共享多个 context 的时候,那么就会出现 Provider 嵌套的情况:

<ThemeContext.Provider value={themes}>
<NumberContext.Provider value={number}>
<Father>
<Child />
</Father>
</NumberContext.Provider>
</ThemeContext.Provider>

此时想在消费组件中取到所有 value,只能通过嵌套 Context.Consumer 的方式:

<ThemeContext.Consumer>
{(theme) => (
<NumberContext.Consumer>
{(number) => (
<div>{theme}{number}</div>
)}
</NumberContext.Consumer>
)}
</ThemeContext.Consumer>

这种方式非常麻烦,后面可以使用 useContext 代替,不过只能简化消费组件在订阅 context 时的代码,顶层组件的 Provider 依旧要嵌套,当然最好还是用社区的状态管理工具 ( redux、mobx、recoil )。

实现插槽效果​

利用 props​

const Father = (props) => {
return (
<div className='father'>
<div className='left'>{props.left}</div>
<div className='mid'>{props.mid}</div>
<div className='right'>{props.right}</div>
</div>
)
}
<Father 
left={<div>left</div>}
mid={<div>mid</div>}
right={<div>right</div>}
/>

利用 props.children​

const Father = (props) => {
const children = React.Children.toArray(props.children);
return (
<div className='father'>
<div className='left'>{children[0]}</div>
<div className='mid'>{children[1]}</div>
<div className='right'>{children[2]}</div>
</div>
)
}
<Father>
<div>left</div>
<div>mid</div>
<div>right</div>
</Father>

Key 的作用​

Refs​

获取 DOM 节点​

在 React 开发过程中,有些需求可能需要去直接操作 DOM,但是 React 官方并不建议使用 document.getElementById 这种方式去获取 DOM。React 提供了 Refs 这种方式允许我们访问 DOM 节点。

这里介绍两种创建 Refs 的方式,首先是使用 React.createRef。

将创建好的 Refs 对象绑定到需要操作的 DOM 的 ref 属性上,然后我们就可以通过 Refs 对象上的 current 属性来获取到这个 DOM 实例了。

import React, { Component } from "react";

export default class Input extends Component {
inputRef = React.createRef();

render() {
return (
<div>
<input ref={this.inputRef} />
<button onClick={(e) => this.inputRef.current?.focus()}>focus</button>
<button onClick={(e) => this.inputRef.current?.blur()}>blur</button>
</div>
);
}
}

还有一种方式是传入回调函数给 DOM 的 ref 属性。

import React, { Component } from "react";

export default class Input extends Component {
inputEle = null;

render() {
return (
<div>
<input ref={(ele) => { this.inputEle = ele }} />
<button onClick={(e) => this.inputEle.focus()}>focus</button>
<button onClick={(e) => this.inputEle.blur()}>blur</button>
</div>
);
}
}

组件在挂载时,会将节点对应的 DOM 对象作为参数传入到 ref 回调函数中。

获取类组件实例​

另外,我们还能通过 Refs 去获取一个类组件实例。

  • 如果 ref 绑定的是 React 中原生的虚拟 DOM 节点,比如 <div> 那么我们获取到的对象就可以把它当作真实 DOM 节点那样进行操作。

  • 如果 ref 绑定的是我们自定义的 class 组件,比如 <Input> 那么我们获取到的对象就是这个组件实例对象。

  • 函数组件是没有实例的!所以上面方式创建出的 Refs 是不能绑定到函数组件上的。

import React, { Component } from "react";

export default class Input extends Component {
inputEle = null;

setInputFocus() {
this.inputEle.focus();
}

setInputBlur() {
this.inputEle.blur();
}

render() {
return (
<div>
<input ref={(ele) => { this.inputEle = ele }} />
</div>
);
}
}
function App() {
const inputRef = React.createRef();
return (
<div>
<Input ref={inputRef} />
<button onClick={(e) => inputRef.current?.setInputFocus()}>focus</button>
<button onClick={(e) => inputRef.current?.setInputBlur()}>blur</button>
</div>
)
}

通过 inputRef.current 我们就能获取到该组件实例,并且能够调用它的实力方法。

important

不能在函数组件上直接使用 ref 属性,然后来操控函数组件,因为函数组件是没有实例的,上述的两种方式只能给类组件或者原生 DOM 组件绑定 ref。

上面的例子中如果 Input 组件是一个函数组件,那么 ref 属性并不会生效,因为函数组件是没有实例的,我们需要通过下面 Refs 转发的方式在 App 组件内获取到 Input 组件内封装的 input DOM 实例 ,去操作原生的 input 元素而不是 Input 组件。

Refs 转发​

有时候我们会有这样一个需求,在当前组件中我们想要操控 自定义的子组件内部的某个 HtmlElement ( 原生虚拟 DOM )

但是 ref 这个属性和 key 一样比较特殊,是不能把它当作 props 传递的,它是独立于 props 的属性。这时我们可以使用 React.forwardRef 这个高阶组件。它的作用是将当前组件创建的 ref 对象传递到其子组件内部。

继续使用上面的例子,我们将 Input 组件修改为函数组件,然后让它使用 React.forwardRef。

const Input = React.forwardRef((props, ref) => {
return (
<div>
<input ref={ref} />
</div>
);
});

export default Input;

这样我们在 App 组件内就可以直接获取到 Input 组件内部的 input 元素了:

function App() {
const inputRef = React.createRef();
return (
<div>
<Input ref={inputRef} />
<button onClick={(e) => inputRef.current?.focus()}>focus</button>
<button onClick={(e) => inputRef.current?.blur()}>blur</button>
</div>
)
}

受控组件​

受控组件的原理如下:

  • 创建 ref 对象绑定到表单元素上
  • 通过表单元素的 onChange 等回调函数,将 e.target.value/checked 的值更新到组件的 state 中
  • 再将 state 赋值到表单元素的 value/checked 属性上

上面整个过程就形成了组件的 state 和表单元素 value 的双向绑定。

import React, { useState } from "react";

export default function Form() {
const [username, setUsername] = useState("");
return (
<form
onSubmit={(e) => {
e.preventDefault();
console.log(username);
}}
>
<label htmlFor="username">
用户名:
<input
type="text"
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</label>
<input type="submit" />
</form>
);
}

另外说一下非受控组件,它和受控组件相反,不会讲表单数据映射给 state,而是在提交时直接通过 ref 获取 DOM 节点对象上的属性。

我们可以使用 e.target.name 来获取表单元素的 name 属性,从而绑定多个受控组件。

import React, { useState } from "react";

export default function Form() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");

const handleChange = (e) => {
const { name, value } = e.target;
switch (name) {
case "username":
return setUsername(value);
case "password":
return setPassword(value);
}
};

return (
<form
onSubmit={(e) => {
e.preventDefault();
console.log(username);
}}
>
<label htmlFor="username">
用户名:
<input
type="text"
id="username"
name="username"
value={username}
onChange={handleChange}
/>
</label>
<label htmlFor="password">
密码:
<input
type="text"
id="password"
name="password"
value={password}
onChange={handleChange}
/>
</label>
<input type="submit" />
</form>
);
}

高阶组件​

一个函数为高阶函数至少满足下面条件之一:

  • 接收一个或者多个函数作为输入
  • 返回一个函数

在原生 JS 中比较常见的高阶函数有 forEach、map、filter、reduce ...

Render Props​

Portals​

ReactDOM.createPortal(child, container)

Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。

StrictMode​

Reference​

  • Key 的作用
  • Refs