前端学习踩坑(2)
前端学习踩坑(2)
笔者这两天正在进行React基础的学习,涉及到的问题多为基础
React组件三大特性
state
state是React.Component中已经定义好的一个属性,在我们继承React.Component并编写类式组件时,如果我们要改组件保存一些状态 / 属性,我们可以使用setState方法对基类中的state进行修改。state只能在类式组件中使用。
1 | class Demo extends React.Component { |
我们可以看到,通过setState方法进行state的改变是一个覆盖 / 叠加的过程,stateOne的值被覆盖了,原先未定义的stateThree被添加了进去。
简化书写,还可以这样:
1 | class Demo extends React.Component { |
相当于直接继承并重写了父类的state属性,修改操作建议使用setState,而不是直接使用赋值运算符进行修改。
props
state 和 props 主要的区别在于 props 是不可变的,而 state 可以根据与用户交互来改变。这就是为什么有些容器组件需要定义 state 来更新和修改数据。 而子组件只能通过 props 来传递数据。props可以在函数式组件和类式组件中使用。
举例如下:
1 | function HelloMessage(props) { |
我们也可以通过对组件的defaultProps和propTypes属性进行设置来达到给props设置默认值和类型约束的目的。
1 | HelloMessage.defaultProps = { |
更多验证器说明如下:
1 | MyComponent.propTypes = { |
ref
React 支持一种非常特殊的属性 Ref ,你可以用来绑定到 render() 输出的任何组件上。
这个特殊的属性允许你引用 render() 返回的相应的支撑实例( backing instance )。这样就可以确保在任何时间总是拿到正确的实例。
用于类组件中,例如设置了ref="inputRef"
,可以通过this.refs.inputRef
获取到DOM节点/React实例
1 | class StringRef extends React.Component { |
注意:不能在函数组件内使用string类型的ref。 Function components cannot have string refs. We recommend using useRef() instead。同时,string类型的ref已经不被官方推荐使用,尽量不要使用字符串类型的ref。
函数中接受 React
组件实例或 HTML DOM
元素作为参数,以使它们能在其他地方被存储和访问。
React
将在组件挂载时,会调用 ref
回调函数并传入 DOM
元素,当卸载时调用它并传入 null
。在 componentDidMount
或 componentDidUpdate
触发前,React
会保证 refs
一定是最新的。
1 | class CallbackRef extends React.Component { |
可以看到,我们编写了一个名为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 | class ObjectRef extends React.Component { |
注意:默认情况下,不能在函数组件上使用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生命周期
挂载
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
更新
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
卸载
当组件从 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 | { |
并在metro.config.js
中添加以下配置:
1 | const { getDefaultConfig } = require("metro-config"); |
完成后,你就可以在你的Expo CLI或React Native CLI项目中使用Sass。
编写
在我的项目编写中,考虑到不同模块的样式表之间的可复用性,我们不一定需要每个模块对应一个样式表,例如我新建一个名为App.scss
的文件,来进行App模块的样式编写。
代码内容如下:
1 | .container { |
在scss格式下,我们可以使用完全原生的css编写样式来编写css。在App模块中使用也很简单,只需要在头部引入即可
1 | import Appstyles from './App.scss'; |
sass会自动将上面的样式表转换为一个普通的js对象,使你的样式表可以在React Native项目中正常的使用。
1 | export default function App() { |
变量
Sass允许我们创建变量来存储信息,我们可以在各种样式块中使用,以提高样式表的可配置性。我们可以使用$
前缀来创建一个Sass变量。让我们在我们的App.scss
文件内创建以下变量。
推荐将变量统一编写在头部并使用注释加以说明
1 | $background-color: #333; |
并直接在样式表中使用:
1 | $background-color: #333; |
继承
Sass最富有成效的特点之一是,它可以让你在许多选择器中重用一组样式。
就像我们可以为一个样式规则定义变量一样,我们可以使用%
符号定义一个包含一组样式规则的占位符类。然后,我们可以在任何我们想要的地方重复使用这些样式。
我们定义一个占位符类如下:
1 | %box-shared { |
当我们想使用这个类时,可以使用@extend关键字来继承它
1 | .boxWhite { |
运算符
我们可以使用运算符来动态地计算我们的元素的高度、宽度、padding、margin和其他类似属性。
你可以使用所有的运算符,如+
,-
,*
,/
, 和%
来计算你规则中的数值。只需要使用括号将其包裹即可。
1 | .boxInside { |
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 | class Example extends React.Component { |
我们使用函数式组件的形式,代码则是:
1 | import React, { useState } from 'react'; |
要实现其中最为核心的部分,则是state中的count属性。在类式组件中,我们使用setState方法来设置state的值,而在函数式组件中,我们提供了新的方法:
首先使用useState
Hook新建一个变量
1 | // 声明一个叫 "count" 的 state 变量 |
我们声明了一个叫 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.count
和this.setState
类似,唯一区别就是你需要成对的获取它们。
读取定义的state,我们可以直接使用变量名读取,无需使用this.state.变量名
1 | <p>You clicked {count} times</p> |
更新state,我们使用我们在定义变量时定义的对变量的set方法进行更新。
1 | <button onClick={() => setCount(count + 1)}> Click me |
可以看出,相对于类式组件,使用Hook的函数式组件更加简介和易于理解。
Effect Hook
Effect Hook 可以让你在函数组件中执行副作用操作。
1 | import React, { useState, useEffect } from 'react'; |
这段代码基于上一章节中的计数器示例进行修改,我们为计数器增加了一个小功能:将 document 的 title 设置为包含了点击次数的消息。
数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是“副作用”这个名字,应该都在组件中使用过它们。
如果你熟悉 React class 的生命周期函数,你可以把
useEffect
Hook 看做componentDidMount
,componentDidUpdate
和componentWillUnmount
这三个函数的组合。
上文中的案例,我们使用类式组件的形式代码是这样的:
1 | class Example extends React.Component { |
可以看到,很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。所以,在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。
现在让我们来看看如何使用 useEffect
执行相同的操作。
1 | import React, { useState, useEffect } from 'react'; |
useEffect
做了什么? 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。为什么在组件内部调用
useEffect
? 将useEffect
放在组件内部让我们可以在 effect 中直接访问count
state 变量(或其他 props)。我们不需要特殊的 API 来读取它 —— 它已经保存在函数作用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的情况下,还引入特定的 React API。
useEffect
会在每次渲染后都执行吗? 是的,默认情况下,它在第一次渲染之后和每次更新之后都会执行。(我们稍后会谈到如何控制它。)你可能会更容易接受 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
现在我们已经对 effect 有了大致了解,下面这些代码应该不难看懂了:
1 | function Example() { |
我们声明了 count
state 变量,并告诉 React 我们需要使用 effect。紧接着传递函数给 useEffect
Hook。此函数就是我们的 effect。然后使用 document.title
浏览器 API 设置 document 的 title。我们可以在 effect 中获取到最新的 count
值,因为他在函数的作用域内。当 React 渲染组件时,会保存已使用的 effect,并在更新完 DOM 后执行它。这个过程在每次渲染时都会发生,包括首次渲染。
经验丰富的 JavaScript 开发人员可能会注意到,传递给 useEffect
的函数在每次渲染中都会有所不同,这是刻意为之的。事实上这正是我们可以在 effect 中获取最新的 count
的值,而不用担心其过期的原因。每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect “属于”一次特定的渲染。我们将在本章节后续部分更清楚地了解这样做的意义。
与
componentDidMount
或componentDidUpdate
不同,使用useEffect
调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),有单独的useLayoutEffect
Hook 供你使用,其 API 与useEffect
相同。