零基础学习React(入门篇)

361 阅读14分钟

前言

本篇文章是整个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,否则文件无法被解析,最后我们把该组件默认导出就可以了。
补充说明:这里补充一下默认导出和命名导出的区别

  1. 默认导出:默认导出是模块中默认提供的导出项,一个模块只能有一个默认导出。使用默认导出时,可以在导入语句中使用任意名称,它会被视为导出模块的主要功能
  2. 命名导出:命名导出是指模块中通过指定名称导出的功能。一个模块可以有多个命名导出,通过使用 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 />)

同步渲染和异步渲染对比

  1. 同步渲染

同步渲染是指 React 在更新组件状态或 props 后立即进行重新渲染,并且将新的变化立即更新到 DOM 中。在同步渲染中,React 会一直执行更新过程,直到更新完成,然后再执行其他任务。这种渲染方式是默认的渲染方式,在 React 16 之前一直采用的方式。
优势:
实时更新:更新发生时立即反应到界面上,用户可以看到实时的变化。
简单可控:由于是同步执行,更新的过程相对较为直观和可控。
劣势:
阻塞主线程:如果更新过程非常耗时,可能会阻塞主线程,导致用户界面卡顿或失去响应。
不够流畅:在大量更新发生时,可能会导致界面闪烁,影响用户体验。

  1. 异步渲染

异步渲染是指 React 使用一种异步更新机制,将更新过程划分为多个优先级较低的任务,优先处理高优先级的任务,然后空闲时再处理低优先级的任务。这样可以将渲染过程拆分成多个时间片段,在每个时间片段内完成部分更新,从而提高界面的响应性。
优势:
提高响应性:异步渲染将渲染过程分片处理,能够更快地响应用户交互和动态更新,提高用户体验。
避免卡顿:通过异步处理任务,避免了长时间的主线程阻塞,减少了界面卡顿问题。
劣势:
复杂性:异步渲染引入了更复杂的渲染机制,可能需要额外的处理和优化,增加了代码的复杂性。
难以调试:异步渲染可能会导致渲染时序的不一致,可能会使调试过程更具挑战性。
从 React 16 开始,React 引入了 Fiber 架构,这是一种支持异步渲染的机制。Fiber 可以将渲染过程拆分成多个任务单元,使得 React 可以在每个时间片段内处理一部分任务,并在空闲时根据任务优先级调整处理顺序。这种异步渲染的方式显著提高了 React 的性能和响应性。因此我们在react16之后选择异步渲染是个更好的选择。

基础组件编写和使用

组件编写及jsx介绍

JSX(JavaScript XML)是一种在React中用于声明用户界面的语法扩展。它允许在JavaScript代码中直接编写类似HTML的结构,使得编写和组织React组件变得更加直观和简洁。我们编写的tsx文件实际上就是TypeScript XML,和jsx规则几乎相同,只是代码编写要符合ts规范。

  1. 以app组件为例,我们可以在函数的最后return一个dom结构,该结构就是组件的html部分
function App() {
    return (
        <div>
            <h1>React App</h1>
        </div>
    )
}
  1. 除此之外我们还可以在函数的任意位置,给一个变量赋值为一个dom元素
function App() {
    // 给变量赋值为dom元素
    const divElement = <div>123</div>
    // 函数返回值为dom元素
    const getButtonElement = () => {
        return <button>按钮</button>
    }
    return (
        <div>
            <h1>React App</h1>
        </div>
    )
}
  1. 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获取所有元素,例如

  1. 父组件
function App() {

    const demoCount = 1
    
    return (
        <div>
            <h1>React App</h1>
            <Demo01 demoCount={demoCount} demoName="demo01">
                <span>This is props.children</span>
            </Demo01>
        </div>
    )
}
  1. 子组件

其中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指定类名

  1. 在相同目录创建样式文件demo.less
.demo01 {
    width: 500px;
    height: 500px;
    background-color: gold;
}
  1. 导入并使用样式文件,使用内联样式
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方法实现循环渲染

如下代码我们就可以得到三个

  • 标签,并且会把animalArr的三个动物信息显示出来

    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>
        )
    }
    

    保持组件的纯粹性

    我们现在的组件都是函数式组件,也就是以函数的形式存在,为了保证组件可预测性,需要使函数式组件是个纯函数,我们先了解一下什么是纯函数。

    纯函数的概念

    纯函数是完全独立自足的,不依赖和影响外部状态,多次调用相同的输入一定返回相同的输出。也就是说当我们调用一个函数时,只要我们输入的参数相同,任何时候返回的值都是相同的,这样的函数就是纯函数。纯函数有以下优势

    1. 代码更加简洁、可读
    2. 易于调试和测试
    3. 多线程友好,不需要担心竞态条件
    4. 缓存优化,对相同输入可返回缓存结果
    5. 组合性好,便于构建复杂的程序逻辑

    例如下面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
    

    为什么函数式组件要是纯函数

    函数式组件要求必须是纯函数有以下几点原因:

    1. 可预测性:纯函数只依赖输入参数,每次输入相同必然得到相同的输出,这样更易于跟踪和预测组件的渲染结果。
    2. 优化性能:纯函数只有在输入发生变化时才会重新渲染,减少了不必要的重复渲染,优化了性能。
    3. 易于测试:纯函数可以通过简单地判断输入和输出来进行测试,不需要考虑外部状态的影响。
    4. 易于理解:纯函数代码简洁,没有外部副作用,更易于阅读理解。
    5. 避免副作用:纯函数内部不会改变外部状态,避免了复杂和难以跟踪的副作用。

    非纯函数式组件,不可预测性案例

    例如以下组件,当你使用ImpureCounter时,每次获得的结果都不相同,

    // 非纯函数组件
    
    let count = 0
    
    function ImpureCounter() {
        return <div>{++count}</div>
    } 
    
    // 使用
    <ImpureCounter /> // 输出: 1
    <ImpureCounter /> // 输出: 2 
    <ImpureCounter /> // 输出: 3