前端学习踩坑(2)

笔者这两天正在进行React基础的学习,涉及到的问题多为基础

React组件三大特性

state

state是React.Component中已经定义好的一个属性,在我们继承React.Component并编写类式组件时,如果我们要改组件保存一些状态 / 属性,我们可以使用setState方法对基类中的state进行修改。state只能在类式组件中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
stateOne : true,
stateTwo : "mjmm"
}
}

chanceState = () => {
this.setState({
stateOne : false,
stateThree : 666
});
}
}

我们可以看到,通过setState方法进行state的改变是一个覆盖 / 叠加的过程,stateOne的值被覆盖了,原先未定义的stateThree被添加了进去。

简化书写,还可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Demo extends React.Component {
state = {
stateOne : true,
stateTwo : "mjmm"
};

chanceState = () => {
this.setState({
stateOne : false,
stateThree : 666
});
}
}

相当于直接继承并重写了父类的state属性,修改操作建议使用setState,而不是直接使用赋值运算符进行修改。

props

state 和 props 主要的区别在于 props 是不可变的,而 state 可以根据与用户交互来改变。这就是为什么有些容器组件需要定义 state 来更新和修改数据。 而子组件只能通过 props 来传递数据。props可以在函数式组件和类式组件中使用。

举例如下:

1
2
3
4
5
6
7
8
9
10
function HelloMessage(props) {
return <h1>Hello {props.name}!</h1>;
}

const element = <HelloMessage name="Runoob"/>;

ReactDOM.render(
element,
document.getElementById('example')
);

我们也可以通过对组件的defaultProps和propTypes属性进行设置来达到给props设置默认值和类型约束的目的。

1
2
3
4
5
6
7
HelloMessage.defaultProps = {
name: 'Runoob'
};

HelloMessage.propTypes = {
name: PropTypes.string
};

更多验证器说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
MyComponent.propTypes = {
// 可以声明 prop 为指定的 JS 基本数据类型,默认情况,这些数据是可选的
optionalArray: React.PropTypes.array,
optionalBool: React.PropTypes.bool,
optionalFunc: React.PropTypes.func,
optionalNumber: React.PropTypes.number,
optionalObject: React.PropTypes.object,
optionalString: React.PropTypes.string,

// 可以被渲染的对象 numbers, strings, elements 或 array
optionalNode: React.PropTypes.node,

// React 元素
optionalElement: React.PropTypes.element,

// 用 JS 的 instanceof 操作符声明 prop 为类的实例。
optionalMessage: React.PropTypes.instanceOf(Message),

// 用 enum 来限制 prop 只接受指定的值。
optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),

// 可以是多个对象类型中的一个
optionalUnion: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
React.PropTypes.instanceOf(Message)
]),

// 指定类型组成的数组
optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),

// 指定类型的属性构成的对象
optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),

// 特定 shape 参数的对象
optionalObjectWithShape: React.PropTypes.shape({
color: React.PropTypes.string,
fontSize: React.PropTypes.number
}),

// 任意类型加上 `isRequired` 来使 prop 不可空。
requiredFunc: React.PropTypes.func.isRequired,

// 不可空的任意类型
requiredAny: React.PropTypes.any.isRequired,

// 自定义验证器。如果验证失败需要返回一个 Error 对象。不要直接使用 `console.warn` 或抛异常,因为这样 `oneOfType` 会失效。
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error('Validation failed!');
}
}
}
}

内容引用自React Props | 菜鸟教程 (runoob.com)

ref

React 支持一种非常特殊的属性 Ref ,你可以用来绑定到 render() 输出的任何组件上。

这个特殊的属性允许你引用 render() 返回的相应的支撑实例( backing instance )。这样就可以确保在任何时间总是拿到正确的实例。

用于类组件中,例如设置了ref="inputRef",可以通过this.refs.inputRef获取到DOM节点/React实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class StringRef extends React.Component {
render() {
return <>
<input ref="inputRef" />
<Button ref="btnRef" onClick={() => {
this.refs.inputRef.value = "Click"
}}>Click</Button>
</>
}

componentDidMount() {
console.log(this.refs)
}
}

注意:不能在函数组件内使用string类型的ref。 Function components cannot have string refs. We recommend using useRef() instead。同时,string类型的ref已经不被官方推荐使用,尽量不要使用字符串类型的ref。

函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。

React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。在 componentDidMountcomponentDidUpdate 触发前,React 会保证 refs 一定是最新的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class CallbackRef extends React.Component {
constructor(props) {
super(props);

this.textInput = null;

this.setTextInputRef = element => {
this.textInput = element;
};

this.focusTextInput = () => {
// 使用原生 DOM API 使 text 输入框获得焦点
if (this.textInput) this.textInput.focus();
};
}


render() {
// 使用 `ref` 的回调函数将 text 输入框 DOM 节点的引用存储到 React
// 实例上(比如 this.textInput)
return (
<div>
<input
type="text"
ref={this.setTextInputRef}
/>
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}

可以看到,我们编写了一个名为setTextInputRef的回调函数,在组件挂在时,React为保证每次传入的ref是最新的,会先调用ref的回调函数并传入一个null值,以清空原来的ref值,随后才会传入新的ref值。

在该例子中,我们使用this.setTextInputRef函数为this.textInput赋值,使用this.textInput保存挂载ref的节点。并使用 this.focusTextInput函数调用保存的节点信息来实现功能。

使用 React.createRef() 创建的,并通过 ref 属性附加到 React 元素。可以通过实例的 current 属性对 DOM 节点或者组件实例进行访问。这也是React最为推荐的一种使用ref的形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class ObjectRef extends React.Component {
constructor(props) {
super(props);
// 创建一个 ref 来存储 textInput 的 DOM 元素
this.textInput = React.createRef();
}

focusTextInput = () => {
// 直接使用原生 API 使 text 输入框获得焦点
// 注意:我们通过 "current" 来访问 DOM 节点
this.textInput.current.focus();
}

render() {
// 告诉 React 我们想把 <input> ref 关联到
// 构造器里创建的 `textInput` 上
return (
<div>
<input
type="text"
ref={this.textInput} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}

注意:默认情况下,不能在函数组件上使用ref属性,因为函数组件没有实例。 Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()

在通过使用React.createRef方法后,我们可以生成一个ref对象,并将其直接挂载在绑定的节点上。在函数中,我们可以通过调用ref对象的current属性直接访问被绑定的dom节点。

在该例子中,我们创建了一个名为textInput的ref对象,并直接将其与一个text-input绑定。在实现功能的focusTextInput函数中,我们直接使用this.textInput.current访问到了原生的dom节点。

React生命周期

image-20220726224722603

React lifecycle methods diagram (wojtekmaj.pl)

挂载

当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

更新

当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:

卸载

当组件从 DOM 中移除时会调用如下方法:

错误处理

当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

Sass——前端CSS预处理器

Sass是一门高于CSS的元语言,它能用来清晰地、结构化地描述文件样式,有着比普通CSS更加强大的功能。
Sass能够提供更简洁、更优雅的语法,同时提供多种功能来创建可维护和管理的样式表。

由于Sass中scss格式与css格式基本一致,学习成本更小,所以下文将主要以scss的格式编写为主。

笔者是在React native项目中,感受到了其中对象式的stylesheet编写的痛苦和响应式布局单位的缺失后,开始深深的怀念起了原生的CSS编写方式。

安装

1
npm install react-native-sass-transformer node-sass

1
yarn add react-native-sass-transformer node-sass

在使用Expo CLI的情况下,我们还需要在根目录下创建一个metro.config.js文件,并在App.js中添加以下配置:

1
2
3
4
5
6
7
8
9
10
{
"expo": {
...
"packagerOpts": {
"config": "metro.config.js",
"sourceExts": ["js", "jsx", "scss", "sass"]
}
...
}
}

并在metro.config.js中添加以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { getDefaultConfig } = require("metro-config");

module.exports = (async () => {
const {
resolver: { sourceExts }
} = await getDefaultConfig();
return {
transformer: {
babelTransformerPath: require.resolve("react-native-sass-transformer")
},
resolver: {
sourceExts: [...sourceExts, "scss", "sass"]
}
};
})();

完成后,你就可以在你的Expo CLI或React Native CLI项目中使用Sass。

编写

在我的项目编写中,考虑到不同模块的样式表之间的可复用性,我们不一定需要每个模块对应一个样式表,例如我新建一个名为App.scss的文件,来进行App模块的样式编写。

代码内容如下:

1
2
3
4
5
6
7
.container {
display: flex;
flex: 1;
justify-content: center;
align-items: center;
background-color: #333;
}

在scss格式下,我们可以使用完全原生的css编写样式来编写css。在App模块中使用也很简单,只需要在头部引入即可

1
import Appstyles from './App.scss';

sass会自动将上面的样式表转换为一个普通的js对象,使你的样式表可以在React Native项目中正常的使用。

1
2
3
4
5
6
7
export default function App() {
return (
<View style={Appstyles.container}>
<Text>Open up App.js to start working on your app!</Text>
</View>
);
}

变量

Sass允许我们创建变量来存储信息,我们可以在各种样式块中使用,以提高样式表的可配置性。我们可以使用$ 前缀来创建一个Sass变量。让我们在我们的App.scss 文件内创建以下变量。

推荐将变量统一编写在头部并使用注释加以说明

1
2
3
$background-color: #333;
$primary-color: #d3d3d3;
$font-large: 20px;

并直接在样式表中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$background-color: #333;
$primary-color: #d3d3d3;
$font-large: 20px;

.container {
display: flex;
flex: 1;
justify-content: center;
align-items: center;
background-color: $background-color;
padding: 20px;
}
.text {
font-weight: 700;
text-align: center;
font-size: $font-large;
color: $primary-color;
}

继承

Sass最富有成效的特点之一是,它可以让你在许多选择器中重用一组样式。

就像我们可以为一个样式规则定义变量一样,我们可以使用% 符号定义一个包含一组样式规则的占位符类。然后,我们可以在任何我们想要的地方重复使用这些样式。

我们定义一个占位符类如下:

1
2
3
4
5
6
7
8
%box-shared {
box-shadow: 0 0 1px rgba(0, 0, 0, 0.25);
border: 1px solid #ccc;
padding: 10px;
height: $box-dimension;
width: $box-dimension;
margin-bottom: 100px;
}

当我们想使用这个类时,可以使用@extend关键字来继承它

1
2
3
4
.boxWhite {
@extend %box-shared;
background-color: $light;
}

运算符

我们可以使用运算符来动态地计算我们的元素的高度、宽度、padding、margin和其他类似属性。

你可以使用所有的运算符,如+,-,*,/, 和% 来计算你规则中的数值。只需要使用括号将其包裹即可。

1
2
3
4
5
6
.boxInside {
@extend %box-shared;
background-color: lightblue;
height: ($box-dimension-lg/2);
width: ($box-dimension-lg/2);
}

React Hook

Hook可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

没有计划从 React 中移除 class。 你可以在本页底部的章节读到更多关于 Hook 的渐进策略。

Hook 不会影响你对 React 概念的理解。 恰恰相反,Hook 为已知的 React 概念提供了更直接的 API:props, state,context,refs 以及生命周期。稍后我们将看到,Hook 还提供了一种更强大的方式来组合他们。

State Hook

这是一个用于计数的组件,每次调用onClick函数时,会将保存计数次数的count变量加1。

我们用熟悉一点的类组件的形式来写的话,代码就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}

render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}

我们使用函数式组件的形式,代码则是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useState } from 'react';

function Example() {
// 声明一个叫 "count" 的 state 变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

要实现其中最为核心的部分,则是state中的count属性。在类式组件中,我们使用setState方法来设置state的值,而在函数式组件中,我们提供了新的方法:

首先使用useState Hook新建一个变量

1
2
// 声明一个叫 "count" 的 state 变量  
const [count, setCount] = useState(0);

我们声明了一个叫 count 的 state 变量,然后把它设为 0。React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用 setCount 来更新当前的 count

调用 useState 方法的时候做了什么? 它定义一个 “state 变量”。我们的变量叫 count, 但是我们可以叫他任何名字,比如 banana。这是一种在函数调用时保存变量的方式 —— useState 是一种新方法,它与 class 里面的 this.state 提供的功能完全相同。一般来说,在函数退出后变量就会”消失”,而 state 中的变量会被 React 保留。

useState 需要哪些参数? useState() 方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了 0 作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用 useState() 两次即可。)

useState 方法的返回值是什么? 返回值为:当前 state 以及更新 state 的函数。这就是我们写 const [count, setCount] = useState() 的原因。这与 class 里面 this.state.countthis.setState 类似,唯一区别就是你需要成对的获取它们。

读取定义的state,我们可以直接使用变量名读取,无需使用this.state.变量名

1
<p>You clicked {count} times</p>

更新state,我们使用我们在定义变量时定义的对变量的set方法进行更新。

1
2
<button onClick={() => setCount(count + 1)}>    Click me
</button>

可以看出,相对于类式组件,使用Hook的函数式组件更加简介和易于理解。

Effect Hook

Effect Hook 可以让你在函数组件中执行副作用操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);

// Similar to componentDidMount and componentDidUpdate: useEffect(() => { // Update the document title using the browser API document.title = `You clicked ${count} times`; });
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

这段代码基于上一章节中的计数器示例进行修改,我们为计数器增加了一个小功能:将 document 的 title 设置为包含了点击次数的消息。

数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。

如果你熟悉 React class 的生命周期函数,你可以把 useEffect Hook 看做 componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

上文中的案例,我们使用类式组件的形式代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}

componentDidMount() { document.title = `You clicked ${this.state.count} times`; }
componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; }
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}

可以看到,很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。所以,在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。

现在让我们来看看如何使用 useEffect 执行相同的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);

useEffect(() => { document.title = `You clicked ${count} times`; });
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}

useEffect 做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。

为什么在组件内部调用 useEffectuseEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。

useEffect 会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后每次更新之后都会执行。(我们稍后会谈到如何控制它。)你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。

现在我们已经对 effect 有了大致了解,下面这些代码应该不难看懂了:

1
2
3
4
5
6
7
function Example() {
const [count, setCount] = useState(0);

useEffect(() => {
document.title = `You clicked ${count} times`;
});
}

我们声明了 count state 变量,并告诉 React 我们需要使用 effect。紧接着传递函数给 useEffect Hook。此函数就是我们的 effect。然后使用 document.title 浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 count 值,因为他在函数的作用域内。当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。这个过程在每次渲染时都会发生,包括首次渲染。

经验丰富的 JavaScript 开发人员可能会注意到,传递给 useEffect 的函数在每次渲染中都会有所不同,这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的 count 的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。我们将在本章节后续部分更清楚地了解这样做的意义。

componentDidMountcomponentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的 useLayoutEffect Hook 供你使用,其 API 与 useEffect 相同。