前言
本篇文章是整个react学习的开端,后续还有深入的文章详细介绍整个react框架技术知识,本文案例所使用的开发环境是自己搭建的脚手架,具体参考从零搭建React脚手架(Webpack5+Typescript+Eslint)
(注:本文所有案例使用ts及tsx实现)
渲染根组件App.tsx
安装react核心库
这里应该是最基本的,开发react必须要安装react和react-dom两个核心库,直接npm install就可以了
npm install react react-dom --save
创建第一个组件App
我们创建第一个组件,也是项目的根组件app,这里需要注意的是,每个组件函数的首字母需要大写,每个tsx文件都需要导入React,否则文件无法被解析,最后我们把该组件默认导出就可以了。
补充说明:这里补充一下默认导出和命名导出的区别
- 默认导出:默认导出是模块中默认提供的导出项,一个模块只能有一个默认导出。使用默认导出时,可以在导入语句中使用任意名称,它会被视为导出模块的主要功能
- 命名导出:命名导出是指模块中通过指定名称导出的功能。一个模块可以有多个命名导出,通过使用 export 关键字并指定名称来导出。在导入时,需要使用相应的名称来引用导出项。命名导出需要使用大括号 {} 来导入,以表示引入的是模块中特定的导出项
import React from 'react'
function App() {
return (
<div>
<h1>React App</h1>
</div>
)
}
export default App
渲染根组件
当前版本的react渲染根组件使用的方式是异步渲染,异步渲染在16版本之后引入,其实不管是异步还是同步渲染,一个项目中只需要有一次就好,就是渲染我们的根组件,其他的组件全都挂载到我们的根组件上一并渲染。下面我们会把两种渲染模式都解释一下,并且分析一下哪个更有优势,便于我们以后的选择
同步渲染
在react16版本之前一直都是使用同步渲染的模式,同步渲染使用ReactDom.render方法,传入两个参数,需要渲染的根组件和挂载的element元素
import React from 'react'
import ReactDOM from 'react-dom'
import App from '@app/app'
const rootElement = document.getElementById('root') as HTMLElement
ReactDOM.render(<App />, rootElement)
异步渲染
在react16之后,引入了异步渲染模式,异步渲染使用ReactDOM.createRoot方法创建一个react根实例,通过根实例的render方法渲染根组件,ReactDOM.createRoot传入一个必选参数,也是我们需要渲染的element元素,创建的react根实例就包换我们根组件需要渲染的根元素
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from '@app/app'
const rootElement = document.getElementById('root') as HTMLElement
const root = ReactDOM.createRoot(rootElement)
root.render(<App />)
同步渲染和异步渲染对比
- 同步渲染
同步渲染是指 React 在更新组件状态或 props 后立即进行重新渲染,并且将新的变化立即更新到 DOM 中。在同步渲染中,React 会一直执行更新过程,直到更新完成,然后再执行其他任务。这种渲染方式是默认的渲染方式,在 React 16 之前一直采用的方式。
优势:
实时更新:更新发生时立即反应到界面上,用户可以看到实时的变化。
简单可控:由于是同步执行,更新的过程相对较为直观和可控。
劣势:
阻塞主线程:如果更新过程非常耗时,可能会阻塞主线程,导致用户界面卡顿或失去响应。
不够流畅:在大量更新发生时,可能会导致界面闪烁,影响用户体验。
- 异步渲染
异步渲染是指 React 使用一种异步更新机制,将更新过程划分为多个优先级较低的任务,优先处理高优先级的任务,然后空闲时再处理低优先级的任务。这样可以将渲染过程拆分成多个时间片段,在每个时间片段内完成部分更新,从而提高界面的响应性。
优势:
提高响应性:异步渲染将渲染过程分片处理,能够更快地响应用户交互和动态更新,提高用户体验。
避免卡顿:通过异步处理任务,避免了长时间的主线程阻塞,减少了界面卡顿问题。
劣势:
复杂性:异步渲染引入了更复杂的渲染机制,可能需要额外的处理和优化,增加了代码的复杂性。
难以调试:异步渲染可能会导致渲染时序的不一致,可能会使调试过程更具挑战性。
从 React 16 开始,React 引入了 Fiber 架构,这是一种支持异步渲染的机制。Fiber 可以将渲染过程拆分成多个任务单元,使得 React 可以在每个时间片段内处理一部分任务,并在空闲时根据任务优先级调整处理顺序。这种异步渲染的方式显著提高了 React 的性能和响应性。因此我们在react16之后选择异步渲染是个更好的选择。
基础组件编写和使用
组件编写及jsx介绍
JSX(JavaScript XML)是一种在React中用于声明用户界面的语法扩展。它允许在JavaScript代码中直接编写类似HTML的结构,使得编写和组织React组件变得更加直观和简洁。我们编写的tsx文件实际上就是TypeScript XML,和jsx规则几乎相同,只是代码编写要符合ts规范。
- 以app组件为例,我们可以在函数的最后return一个dom结构,该结构就是组件的html部分
function App() {
return (
<div>
<h1>React App</h1>
</div>
)
}
- 除此之外我们还可以在函数的任意位置,给一个变量赋值为一个dom元素
function App() {
// 给变量赋值为dom元素
const divElement = <div>123</div>
// 函数返回值为dom元素
const getButtonElement = () => {
return <button>按钮</button>
}
return (
<div>
<h1>React App</h1>
</div>
)
}
- jsx的dom元素中使用变量,我们使用{}在xml中使用变量,以上面代码为例
function App() {
const divElement = <div>123</div>
const getButtonElement = () => {
return <button>按钮</button>
}
return (
<div>
<h1>React App</h1>
{divElement}
{getButtonElement()}
</div>
)
}
组件导入导出及使用
组件导出
我们上述已经有过app组件导出的介绍,下面再举一个demo01组件的导出,我们使用export default 将组件默认导出
import React from 'react'
function Demo01() {
return (
<div>
<h1>Demo01</h1>
</div>
)
}
export default Demo01
组件导入
我们在其父组件将组件导入,例如在app中将demo01导入,我们使用import导入组件,由于组件是默认导出,因此我们可以为组件任意命名,根据jsx规则,组件的首字母必须大写,例如demo01组件我们甚至可以命名为Aaaa。但是按照正常使用习惯,我们一般使用导出的名字命名,可增加代码可读性。
import React from 'react'
import Demo01 from '@pages/demo/demo01'
function App() {
return (
<div>
<h1>React App</h1>
</div>
)
}
export default App
组件使用
组件导入后,直接在xml中以标签的形式使用
function App() {
return (
<div>
<h1>React App</h1>
<Demo01 />
</div>
)
}
父子组件使用Props传递数据
父子组件之间传递数据主要使用Props进行传递,父组件中在组件的标签内部类似于属性的形式传递数据,子组件在函数中使用参数props接受数据
父组件传递数据
父组件传递数据可以传递静态数据也可以传递变量,例如
function App() {
const demoCount = 1
return (
<div>
<h1>React App</h1>
<Demo01 demoCount={demoCount} demoName="demo01" />
</div>
)
}
子组件接受数据并使用
子组件使用props参数接受数据,所传的所有数据以对象形式存在,使用props.属性方式读取,我们也可以使用对象解构的方式取出每个属性,由于我们编写的是tsx文件,因此需要给props定义类型,在此我们使用interface定义props的类型,当然也可以使用type定义,使用interface只是普遍习惯,并且interface扩展性较好
interface IDemo01Props {
demoCount: number,
demoName: string
}
function Demo01(props: IDemo01Props) {
const { demoCount, demoName } = props
return (
<div>
<h1>Demo01 demoCount {demoCount}</h1>
<h2>{demoName}</h2>
</div>
)
}
特殊的props,props.children
当我们想向子组件传递一个dom元素块时,我们有两种方式,一种就是之前介绍的正常的props,给变量赋值为一个dom元素,并将其传入子组件,在此不做举例,下面我们介绍一种更方便常用的方式,就是在组件中直接编写xml,然后子组件中使用props.children获取所有元素,例如
- 父组件
function App() {
const demoCount = 1
return (
<div>
<h1>React App</h1>
<Demo01 demoCount={demoCount} demoName="demo01">
<span>This is props.children</span>
</Demo01>
</div>
)
}
- 子组件
其中children?: React.ReactNode中的?是指该属性可选,可传可不传,当属性定义中有可选参数时,需要定义defaultProps,children的类型固定是React.ReactNode
interface IDemo01Props {
demoCount: number,
demoName: string,
children?: React.ReactNode
}
function Demo01(props: IDemo01Props) {
const { demoCount, demoName, children } = props
return (
<div>
<h1>Demo01 demoCount {demoCount}</h1>
<h2>{demoName}</h2>
{children}
</div>
)
}
Demo01.defaultProps = {
children: null
}
xml中添加属性和绑定事件
xml中添加属性和绑定事件和html几乎相同,我们可以直接给属性设置值,或者使用变量设置值,当使用变量时需要用{}包裹。绑定事件和html中一样,使用on+事件名。
import React from 'react'
import dogImage from '@assets/image/dog.png'
function Demo01() {
const h1ClassName = 'demo01-h1'
const handleClickButton = () => {
console.log('click button')
}
return (
<div className="demo01">
<h1 className={h1ClassName}>Demo01 demoCount</h1>
<img src={dogImage} alt="" />
<button onClick={handleClickButton}>按钮</button>
</div>
)
}
export default Demo01
组件中使用样式
组件中使用样式有两种方式,内联样式和引入样式文件,内联样式可以直接写在xml内部或者使用变量,引入样式文件,需要给元素指定class或者id,也可以直接使用标签名设置样式,为了避免样式冲突,这种方式不是很常用。需要注意的是,指定class时,在xml中需要使用className指定,区别于html中的class,这是因为在js中class是声明类的关键字,因此避免关键字冲突,需要使用className指定类名
- 在相同目录创建样式文件demo.less
.demo01 {
width: 500px;
height: 500px;
background-color: gold;
}
- 导入并使用样式文件,使用内联样式
import React from 'react'
import dogImage from '@assets/image/dog.png'
// 导入样式文件
import './demo01.less'
function Demo01() {
const buttonStyle = {
width: '100px',
height: '50px'
}
return (
// 指定类名
<div className="demo01">
{/* 使用style属性指定样式 */}
<h1 style={{ color: 'red' }}>Demo01 demoCount</h1>
<img src={dogImage} alt="" />
{/* 使用style属性指定样式,变量形式 */}
<button style={buttonStyle}>按钮</button>
</div>
)
}
export default Demo01
条件渲染和循环渲染
由于我们很多时候需要根据不同条件渲染不同的内容,或者需要渲染列表数据,因此我们需要进行进行条件渲染和循环渲染
条件渲染
通常你的组件会需要根据不同的情况显示不同的内容。在 React 中,你可以通过使用 JavaScript 的 if 语句、&& 和 ? : 运算符来选择性地渲染 JSX。
使用if语句实现条件渲染
之前我们说过如何使用函数返回一个dom元素,这里我们就可以在函数中使用if语句,根据不同的条件返回不同的内容来实现条件渲染,下面案例会根据传入的参数返回不同的dom元素
function Demo01() {
const getAnimalName = (type: string) => {
if (type === 'dog') {
return <div>dog</div>
} else {
return <div>cat</div>
}
}
return (
<div>
<h2>demo01</h2>
{getAnimalName('dog')}
</div>
)
}
使用&&符号实现条件渲染
当我们遇到根据一个条件渲染一个元素,并且该条件不成立的情况下,不需要渲染任何东西,我们就可以使用&&符合实现,因为&&符号在js中就是当&&符号之前的表达式有false时,后面的就不会再执行,例如以下代码,当isDog是false时,后面的
标签就不会渲染
function Demo01() {
const isDog = true
return (
<div>
<h2>demo01</h2>
{isDog && <p>这里是一只狗</p>}
</div>
)
}
使用三元运算符实现条件渲染
三元运算符实际是if else的一种简化写法,我们使用三元运算符可以实现和if else相同的效果,例如以下代码
function Demo01() {
const isDog = true
return (
<div>
<h2>demo01</h2>
{isDog ? <p>这里是一只狗</p> : <p>这里不是一只狗</p>}
</div>
)
}
循环渲染
你可能经常需要通过 JavaScript 的数组方法 来操作数组中的数据,从而将一个数据集渲染成多个相似的组件。在数组操作中,map方法可以返回一个新的数组,我们就利用map方法的特性来返回一个组件数组
map方法实现循环渲染
如下代码我们就可以得到三个
function Demo01() {
// 生成一个动物数组,包含id,name,age
const animalArr = [
{ name: 'dog', age: 3 },
{ name: 'cat', age: 2 },
{ name: 'pig', age: 1 },
]
return (
<div>
<h2>demo01</h2>
<ul>
{animalArr.map((item) => {
return <li>我是{item.age}岁的{item.name}</li>
})}
</ul>
</div>
)
}
循环渲染的必要属性key
如果我们只是简单的按照上述例子进行循环渲染,控制台会 Each child in a list should have a unique "key" prop的报错,这是因为在循环渲染时,每个子项都要有个唯一标识key,key有着元素重用和性能优化,帮助 React 识别更新和保持组件状态的作用,上述代码我们可以做如下改造
function Demo01() {
// 生成一个动物数组,包含id,name,age
const animalArr = [
{ id: 1, name: 'dog', age: 3 },
{ id: 2, name: 'cat', age: 2 },
{ id: 3, name: 'pig', age: 1 },
]
return (
<div>
<h2>demo01</h2>
<ul>
{
animalArr.map((item) => {
return <li key={item.id}>我是{item.age}岁的{item.name}</li>
})
}
</ul>
</div>
)
}
保持组件的纯粹性
我们现在的组件都是函数式组件,也就是以函数的形式存在,为了保证组件可预测性,需要使函数式组件是个纯函数,我们先了解一下什么是纯函数。
纯函数的概念
纯函数是完全独立自足的,不依赖和影响外部状态,多次调用相同的输入一定返回相同的输出。也就是说当我们调用一个函数时,只要我们输入的参数相同,任何时候返回的值都是相同的,这样的函数就是纯函数。纯函数有以下优势
- 代码更加简洁、可读
- 易于调试和测试
- 多线程友好,不需要担心竞态条件
- 缓存优化,对相同输入可返回缓存结果
- 组合性好,便于构建复杂的程序逻辑
例如下面add函数就是个非纯函数,它依赖了函数之外的变量,当函数之外的变量被修改后,相同的输入会有着不同的输出
let count = 1
function add(num: number): number {
return num + count
}
function changeCount(): void {
count = 3
}
console.log(add(2)) // 3
changeCount()
console.log(add(2)) // 5
将add函数改造成纯函数,不管任何时候只要输入的参数相同,输出结果就相同
function add(num: number): number {
const count = 1
return num + count
}
console.log(add(1)) // 2
console.log(add(1)) // 2
为什么函数式组件要是纯函数
函数式组件要求必须是纯函数有以下几点原因:
- 可预测性:纯函数只依赖输入参数,每次输入相同必然得到相同的输出,这样更易于跟踪和预测组件的渲染结果。
- 优化性能:纯函数只有在输入发生变化时才会重新渲染,减少了不必要的重复渲染,优化了性能。
- 易于测试:纯函数可以通过简单地判断输入和输出来进行测试,不需要考虑外部状态的影响。
- 易于理解:纯函数代码简洁,没有外部副作用,更易于阅读理解。
- 避免副作用:纯函数内部不会改变外部状态,避免了复杂和难以跟踪的副作用。
非纯函数式组件,不可预测性案例
例如以下组件,当你使用ImpureCounter时,每次获得的结果都不相同,
// 非纯函数组件
let count = 0
function ImpureCounter() {
return <div>{++count}</div>
}
// 使用
<ImpureCounter /> // 输出: 1
<ImpureCounter /> // 输出: 2
<ImpureCounter /> // 输出: 3