React 基础
这篇文章并非完全零基础,零基础最好先看一下官网文档入门,并搭配我另外一篇关于 React 用法的博客更佳:《React 使用》。
0. 构建项目
1. 代码优化
1.1 构造器里使用 bind 绑定 this
constructor(props) {
super(props);
this.state = {
inputValue: "",
list: [],
};
this.handleInputChange = this.handleInputChange.bind(this);
this.handleBtnClick = this.handleBtnClick.bind(this);
this.handleItemDelete = this.handleItemDelete.bind(this);
}
1.2 setState 里使用异步
handleInputChange(e) {
// 使用异步,提升性能
const value = e.target.value;
this.setState(() => ({
inputValue: value,
}));
}
因为 setState 是异步的,所以有个回调函数用来对 setState 后的处理
handleInputChange(e) {
// 使用异步,提升性能
const value = e.target.value;
this.setState(() => ({
inputValue: value,
}), () => {
// setState 后获取 ul 里 div 长度,放外边会比 setState 先执行
console.log(this.ul.querySelectorAll('div').length)
});
}
1.3 使用 prevState
prevState 相当于 this.state
handleBtnClick() {
// prevState 修改之前的数据,避免不小心修改 state 的状态
this.setState((prevState) => ({
list: [...prevState.list, prevState.inputValue],
inputValue: "",
}));
}
优化前:
handleItemDelete(index) {
// immutable
// state 不允许我们做任何的改变
// 拷贝数组
const list = [...this.state.list];
list.splice(index, 1);
this.setState(({
list,
}));
}
优化后:
handleItemDelete(index) {
this.setState((prevState) => {
const list = [...prevState.list];
list.splice(index, 1);
return {
list,
};
});
}
2. React 衍生的思考
声明式开发,不同于命令式(频繁操作 DOM),专注于数据的构建,减少大量 DOM 操作代码。
可以与其他框架共存。不操作 id 为 root 的 div 就行。
组件化,首字母大写形式。
单向数据流,父组件可以给子组件传值,但是子组件不可以改变父组件的值。如果没有单向数据流,导致其他使用的子组件会受到影响。子组件只能调用父组件传过来的方法,最终还是由父组件进行数据的改变。
视图层框架,只负责数据渲染到页面的过程,传值等功能交给其他框架实现。
函数式编程。维护容易,各司其职,不改变原数据,函数式编程方便测试。
3. PropTypes 与 DefaultProps
PropTypes 类型的强校验。不阻止运行,但是会发出警告。
DefaultProps 默认属性,表示没传递的时候的默认值。
import React, { Component } from "react";
import PropTypes from "prop-types";
export default class TodoItem extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
const { onItemDelete } = this.props;
onItemDelete(this.props.index);
}
render() {
const { item, text } = this.props;
return (
<div onClick={this.handleClick}>
{text} - {item}
</div>
);
}
}
TodoItem.propTypes = {
// isRequired 表示必须要传递,没传会报错
text: PropTypes.string.isRequired,
// 传过来的值必须分别是 string, 函数, number
// oneOfType 表示支持多种类型
item: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
onItemDelete: PropTypes.func,
index: PropTypes.number,
};
// defaultProps 设置默认值
TodoItem.defaultProps = {
text: "hello world",
};
4. Props,State 与 Rander 函数的关系
当组件的 state 或者 props 发生改变的时候,render 函数会重新执行。
当父组件的 render 函数被运行时,它的子组件的 render 都将被重新执行一次。
5. 虚拟 DOM
普通想法:
- state 数据
- JSX 模板
- 数据 + 模板结合,生成真实的 DOM 来显示
- state 发生改变
- 数据 + 模板结合,生成真实的 DOM,用来替换原始的 DOM
缺陷:
第一次生成了一个完整的 DOM 片段
第二次生成了一个完整的 DOM 片段
第二次生成的 DOM 替换第一个 DOM,非常耗性能
优化:
- state 数据
- JSX 模板
- 数据 + 模板结合,生成真实的 DOM 来显示
- state 发生改变
- 数据 + 模板结合,生成真实的 DOM,并不直接替换原始的 DOM
- 新的 DOM (DocumentFragment) 和原始的 DOM 作比对,找差异
- 找到了 input 发生了变化
- 只用新的 DOM 中的 input 元素,替换掉老的 DOM 中的 input 元素
缺陷:
性能的提升并不明显,因为比对需要消耗性能
React 做法:
- state 数据
- JSX 模板
- 数据 + 模板结合,生成虚拟 DOM (虚拟 DOM 就是一个 JS 对象,用它来描述真实的 DOM)
- 用虚拟 DOM 的解耦生成真实的 DOM,来显示
- state 发生变化
- 数据 + 模板 生成新的虚拟 DOM (极大的提升了性能)
- 比较原始虚拟 DOM 和新的虚拟 DOM 的区别,找到区别是 span 中内容 (极大的提升性能)
- 直接操作 DOM,改变 span 中的内容
虚拟 DOM 虽然有性能损耗,但是损耗极小,创建 JS 对象损耗小。相比之下,真实 DOM 的生成需要调用 Web Application API
6. 深入了解虚拟 DOM
JSX -> React.createElement -> JS 对象 -> 真实的 DOM
优点:
- 性能提升了。
- 它使得跨端应用得以实现。React Native
7. 虚拟 DOM 中的 Diff 算法
DIff 算法比较原始虚拟 DOM 和新的虚拟 DOM 的区别
为什么 setState 设置为异步可以提升性能?
如果 setState 连续调用三次,时间很短的话会合并起来只进行一次 diff 比对。
比较方法:
同级比较,如果不同,同级以及以下的节点都被替换。
假设有一个节点数组,两个虚拟 DOM 的节点数组进行比对,使用 key 进行比对,如果一致的话,就可以进行复用。因此需要有稳定的 key 值来提升性能。
8. React 中 ref 的使用
未使用 ref
<input
id="insertArea"
type="text"
className="input"
value={this.state.inputValue}
onChange={this.handleInputChange}
/>
e.target 就是一个 DOM 节点
handleInputChange(e) {
const value = e.target.value;
this.setState(() => ({
inputValue: value,
}));
}
使用 ref
<input
id="insertArea"
type="text"
className="input"
value={this.state.inputValue}
onChange={this.handleInputChange}
ref={(input) => {this.input = input}}
/>
构建一个 ref 引用,this.input 指向当前的 input DOM 元素
handleInputChange() {
// 使用异步,提升性能
const value = this.input.value;
this.setState(() => ({
inputValue: value,
}));
}
但是 React 并不推荐这样做,鼓励数据驱动,尽量不要操作 DOM
9. 生命周期函数
生命周期函数指在某一时刻会自动调用执行的函数
React 通常将组件⽣命周期分为三个阶段:
-
挂载阶段(Mount),组件第⼀次在DOM树中被渲染的过程;
-
更新过程(Update),组件状态发⽣变化,重新更新渲染的过程;
-
卸载过程(Unmount),组件从DOM树中被移除的过程;
9.1 旧版生命周期函数
Mounting
componentWillMount:组件被挂载到页面前执行。
componentDidMount:组件被挂载到页面之后,自动被执行。
Updation
componentWillReceiveProps:没有 props 函数,不会被触发。当组件从父组件接收 props 的时候,且父组件的 render 函数重新被执行了,该函数触发。该组件之前已经存在于父组件中,才会被执行。
shouldComponentUpdate: 组件被更新之前,会自动被执行,如果返回值为 false,组件不会更新。
componentWillUpdate:组件被更新之前执行,但是在 shouldComponentUpdate 之后执行,取决于返回结果是否为 true。
componentDidUpdate:组件更新完成之后,将会被执行。
Unmounting
componentWillUnmount:当这个组件即将被从页面中剔除的时候,会被执行。
9.2 新版生命周期函数
10. 生命周期函数的使用场景
10.1 防止重复渲染
shouldComponentUpdate:父组件 render 的时候,子组件也会跟着 render,但是往往子组件内容并没有发生改变。这样会造成性能的损耗。
比如上面的 todolist,里边的 list 就没必要跟着父组件渲染。
性能优化
防止无谓的 render
// 可接收两个参数
// nextProps 指将要接收的新 props
// nextState 指将要接收的新 state
shouldComponentUpdate(nextProps, nextState) {
// 如果新的 props 内容不变,则不 render
if (nextProps.item !== this.props.item) {
return true;
}
return false;
}
引申
在构造器里进行 this 作用域的绑定,只执行一次,有效提升性能。
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
10.2 Ajax 请求
Ajax 不能放在 render 中进行获取数据,因为会造成死循环(每次 render 都会发送一次请求)。componentDidMount 在挂载的时候才会触发,且仅触发一次,因此放 componentDidMount 里边比较合适。
11. 使用 Charles 实现本地数据 mock
1. 创建 todolist.json 虚拟数据
["sjh", "React", "male"]
2. 添加映射
Tools -> Map Local
上面的配置有点问题,还是 404
解决办法:
Host 设置为 localhost.charlesproxy.com
此时,输入 http://localhost.charlesproxy.com:3000/ ,正常发送 Ajax 请求
但是这样写比较麻烦,所以可以写一个新映射 Tools -> Map Remote
将域名映射到 http://localhost:3001/
但是这个写法在谷歌 90 后的版本会报错 strict-origin-when-cross-origin
使用 axios 发送请求:
componentDidMount() {
axios
.get("/api/todolist")
.then((res) => {
this.setState(() => ({
// 拷贝,避免修改 res 造成不可预知的问题
list: [...res.data],
}));
})
.catch((err) => {
throw new Error(err);
});
}
请求结果:
12. React 中 CSS 实现动画效果
有两种写法,一种使用 transition,另一种使用 animation。只能实现简单动画
在 jsx 中:
import React, { Component } from "react";
import "./style.css";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
show: true,
};
this.handleToggle = this.handleToggle.bind(this);
}
handleToggle() {
this.setState((prevState) => ({
show: prevState.show ? false : true,
}));
}
render() {
return (
<>
<div className={this.state.show ? "show" : "hide"}>hello</div>
<button onClick={this.handleToggle}>toggle</button>
</>
);
}
}
第一种方法,在 css 中:
.show {
opacity: 1;
transition: all 1s ease-in;
}
.hide {
opacity: 0;
transition: all 1s ease-in;
}
第二种方法,在 css 中:
.input {
border: 1px solid red;
}
.show {
animation: show-item 2s ease-in forwards;
}
.hide {
animation: hide-item 2s ease-in forwards;
}
@keyframes show-item {
0% {
opacity: 0;
color: red;
}
50% {
opacity: 0.5;
color: green;
}
100% {
opacity: 1;
color: blue;
}
}
@keyframes hide-item {
0% {
opacity: 1;
color: red;
}
50% {
opacity: 0.5;
color: green;
}
100% {
opacity: 0;
color: blue;
}
}
13. 使用 react-transition-group 实现动画
13.1 安装 react-transition-group
# npm
npm install react-transition-group --save
# yarn
yarn add react-transition-group
13.2 写代码 - CSSTransition
通过按钮实现文本的显示和隐藏
import React, { Component } from "react";
import "./style.css";
import { CSSTransition } from "react-transition-group";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
show: true,
};
this.handleToggle = this.handleToggle.bind(this);
}
handleToggle() {
this.setState((prevState) => ({
show: prevState.show ? false : true,
}));
}
render() {
return (
<>
<CSSTransition in={this.state.show} timeout={1000} classNames="fade">
{/* 包裹后,自动执行内部类名的变更操作 */}
{/* <div className={this.state.show ? "show" : "hide"}>hello</div> */}
<div>hello</div>
</CSSTransition>
<button onClick={this.handleToggle}>toggle</button>
</>
);
}
}
/* 入场动画的瞬间状态 (false -> true) */
.fade-enter {
opacity: 0;
}
/* 入场动画进入后到完成前 */
.fade-enter-active {
opacity: 1;
transition: opacity 1s ease-in;
}
/* 动画结束后的状态 */
.fade-enter-done {
opacity: 1;
}
/* 离场动画的瞬间状态 (true -> false) */
.fade-exit {
opacity: 1;
}
/* 离场动画进入后到完成前 */
.fade-exit-active {
opacity: 0;
transition: opacity 1s ease-in;
}
.fade-exit-done {
opacity: 0;
}
如果控制台报错:
Warning: findDOMNode is deprecated in StrictMode. findDOMNode was passed an instance of Transition which is inside StrictMode. Instead, add a ref directly to the element you want to reference.
则需要关闭React的“严格模式”。把 index.js 里的 React.StrictMode
标签删了就好啦。
虽然写法变麻烦了,但是带来了许多新特性。React Transition Group
13.3 写第二组代码 - TransitionGroup
要求:按钮点击的时候增加 item,并附加动画效果。
TransitionGroup 的效果在于,能给包裹的一组数据都赋予动画效果。每个具体的标签之外,还需要添加一层 CSSTransition。此时的 in 属性就没有必要使用了。
import React, { Component } from "react";
import "./style.css";
import { CSSTransition, TransitionGroup } from "react-transition-group";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
list: [],
};
this.handleAddItem = this.handleAddItem.bind(this);
}
handleAddItem() {
this.setState((prevState) => ({
list: [...prevState.list, "item"],
}));
}
render() {
return (
<>
<TransitionGroup>
{this.state.list.map((item, index) => {
return (
<CSSTransition timeout={1000} classNames="show" key={index}>
<div>{item}</div>
</CSSTransition>
);
})}
</TransitionGroup>
<button onClick={this.handleAddItem}>Add</button>
</>
);
}
}
/* 入场动画的瞬间状态 (false -> true) */
.show-enter {
opacity: 0;
}
/* 入场动画进入后到完成前 */
.show-enter-active {
opacity: 1;
transition: opacity 1s ease-in;
}
/* 动画结束后的状态 */
.show-enter-done {
opacity: 1;
}
/* 如果要做消失动画,添加相关样式即可 */