Skip to content

React-基础

[TOC]

API

  • ReactDOM. createRoot(domNode)参数:domNode返回:root,用于创建一个React根,之后渲染的内容会包含在这个根中
    • 参数
    • domNode:``,将渲染的内容,挂载到哪一个HTML元素上
    • 返回
    • rootRoot对象,通过该对象,React 将会管理该节点内的 DOM 元素
  • root.render(jsx)参数:jsx返回:,渲染函数
    • 参数
    • jsx:``,要渲染的根组件或DOM元素
  • React.createElement(type, props, ...children?)参数:返回:ReactElement,用于创建一个 React 元素并返回该元素的对象
    • 参数
    • typestring,元素类型,可以是 HTML 标签名或自定义组件。
    • propsobject,属性对象,包括该元素的所有属性和事件处理函数等。
    • ...children?:``,可选的子元素节点。可以为文本节点、React 元素等。
    • 返回
    • ReactElement:``,虚拟DOM对象

React在前端的地位

React是什么呢?

  • 相信每个做开发的人对它都或多或少有一些印象;

  • 这里我们来看一下官方对它的解释:用于构建用户界面的 JavaScript 库

image-20230315120238154

目前对于前端开发来说,几乎很少直接使用原生的JavaScript来开发应用程序,而是选择一个JavaScript库(框架)。

  • 在过去的很长时间内,jQuery是被使用最多的JavaScript库;

  • 在过去的一份调查中显示,全球前10,000个访问最高的网站中,有65%使用了jQuery,是当时最受欢迎的JavaScript库;

  • 但是,目前甚至已经处于淘汰的边缘了;

而无论是国内外,最流行的其实是三大框架:Vue、React、Angular

目前React在前端处于什么地位?

目前前端最流行的是三大框架:Vue、React、Angular。

image-20230315120441975

框架数据对比(Google指数)

image-20230315120511164

npm下载量

image-20230315120523052

框架数据对比(GitHub)

image-20230315120536660

HackerRank调查显示

在HackerRank中,有一份调用,你更想要学习的framework(框架):

image-20230315120548530

招聘岗位对React的要求

image-20230315120609995

React的技术特点

React由Facebook来更新和维护,它是大量优秀程序员的思想结晶:

  • React的流行不仅仅局限于普通开发工程师对它的认可;

  • 大量流行的其他框架借鉴React的思想

Vue.js框架设计之初,有很多的灵感来自Angular和React。

  • 包括Vue3很多新的特性,也是借鉴和学习了React;

  • 比如React Hooks是开创性的新功能(也是我们课程的重点);

  • Vue Composition API学习了React Hooks的思想;

Flutter的很多灵感都来自React,来自官网的一段话:(SwiftUI呢)

  • 事实上Flutter中的Widget – Element – RenderObject;

  • 对应React的就是JSX – 虚拟DOM – 真实DOM;

所以React可以说是前端的先驱者,它总是会引领整个前端的潮流。

Vue和React的选择

首先,React和Vue是前端开发人员必须掌握的两个框架。

image-20230315120643740

下面的观点是一个目前比较普遍的共识,没有贬低任何框架的意思。

  • 大中型公司选择React会较多,灵活和稳定;

  • 中小型公司选择Vue会较多,易上手和代码统一;

  • 难度:React难度大于Vue

  • 工资:React工资大于Vue

如何学习React?

image-20230315120710850

React课程体系

image-20230315120738744

React项目实战-弘源头条(候补)

image-20230315120758911

React项目实战-弘源彼迎(暂定)

image-20230315120810843

哪些人适合学习?

React和Vue都是前端工程师必须掌握的两个框架:

  • 大多数同学都是学习了Vue,并且刚开始工作都是使用的Vue,所以通常对Vue是有深入的感情的(某些同学可能是小程序);

  • 但是在前端整个职业发展的过程中,不能仅仅将自己局限在某一个框架或者技术中;

  • 并且React是作为前端进阶来说自己必须要掌握的一个框架;

本次课程要求:

  • 本课程要求掌握前端的核心开发语言:HTMLCSSJavaScript

  • React本身是JavaScript的要求相对会更高一些,所以也需要掌握一些高级的JavaScript语法,比如ES6以上的语法、this绑定规则等等;

  • 整个课程从零讲解React,所以并不要求之前学习过React相关的知识。

  • 如果你之前已经掌握了一些React,也可以从课程中学习到非常多其他的核心知识和实战细节,也包括原理、源码、架构等知识内容。

所以无论你目前处于前端哪一个阶段,都可以在这个过程中有很多的收获。

React的介绍(技术角度)

React是什么?

image-20230315121639758

React的特点-声明式编程

声明式编程:

  • 声明式编程是目前整个大前端开发的模式:Vue、React、Flutter、SwiftUI;

  • 允许我们只需要维护自己的状态,当状态改变时,React可以根据最新的状态去渲染我们的UI界面

image-20230315121651175

React特点-组件化开发

组件化开发:

  • 组件化开发页面目前前端的流行趋势,我们会将复杂的界面拆分成一个个小的组件

  • 如何合理的进行组件的划分和设计也是后面我会讲到的一个重点;

image-20230315121701736

React的特点-多平台适配

多平台适配:

  • 2013年,React发布之初主要是开发Web页面

  • 2015年,Facebook推出了ReactNative,用于开发移动端跨平台;(虽然目前Flutter非常火爆,但是还是有很多公司在使用ReactNative);

  • 2017年,Facebook推出ReactVR,用于开发虚拟现实Web应用程序;(VR也会是一个火爆的应用场景);

image-20230315121709582

Hello React

为了演练React,我们可以提出一个小的需求:

  • 在界面显示一个文本:Hello World

  • 点击下方的一个按钮,点击后文本改变为Hello React

当然,你也可以使用jQuery和Vue来实现,甚至是原生方式来实现,对它们分别进行对比学习

html
    <div id="root"></div>

    <script crossorigin src="../lib/react.js"></script>
    <script crossorigin src="../lib/react-dom.js"></script>
    <script src="../lib/babel.js"></script>

    <script type="text/babel">
      // 属性
      let msg = "Hello World";

      // 方法
      function onChange() {
        msg = "Hello React"
+        rootRender()
      }

      // 渲染
+      const root = ReactDOM.createRoot(document.querySelector("#root"));
+      rootRender()
+      function rootRender() {
+        root.render(
+          <div>
+            <h2>{msg}</h2>
+            <button onClick={onChange}>修改文本</button>
+          </div>
+        );
+      }
    </script>

image-20230316103525970

React的开发依赖

开发React必3须依赖三个库

  • react:包含react所必须的核心代码

  • react-dom:react渲染在不同平台所需要的核心代码

  • babel:将jsx转换成普通js代码的工具

第一次接触React会被它繁琐的依赖搞蒙,居然依赖这么多东西: (直接放弃?)

  • 对于Vue来说,我们只是依赖一个vue.js文件即可,但是react居然要依赖三个包。

  • 其实呢,这三个库是各司其职的,目的就是让每一个库只单纯做自己的事情;

  • 在React的0.14版本之前是没有react-dom这个概念的,所有功能都包含在react里;

为什么要进行拆分呢?原因就是react-native。

  • react包中包含了react web和react-native所共同拥有的核心代码。

  • react-dom针对web和native所完成的事情不同:

    • web端:react-dom会将jsx最终渲染成真实的DOM,显示在浏览器中
    • native端:react-dom会将jsx最终渲染成原生的控件(比如Android中的Button,iOS中的UIButton)。

Babel和React的关系

babel是什么呢?

  • Babel ,又名 Babel.js

  • 是目前前端使用非常广泛的编译器、转移器。

  • 比如当下很多浏览器并不支持ES6的语法,但是确实ES6的语法非常的简洁和方便,我们开发时希望使用它。

  • 那么编写源码时我们就可以使用ES6来编写,之后通过Babel工具,将ES6语法转成大多数浏览器都支持的ES5的语法

React和Babel的关系:

  • 默认情况下开发React其实可以不使用babel。

  • 但是前提是我们自己使用 React.createElement 来编写源代码,它编写的代码非常的繁琐和可读性差。

  • 那么我们就可以直接编写jsx(JavaScript XML)的语法,并且让babel帮助我们转换成React.createElement

  • 后续还会详细讲到;

React的依赖引入

所以,我们在编写React代码时,这三个依赖都是必不可少的。

那么,如何添加这三个依赖:

  • 方式一:直接CDN引入

  • 方式二:下载后,添加本地依赖

  • 方式三:通过npm管理(后续脚手架再使用)

1、直接CDN引入

暂时我们直接通过CDN引入,来演练下面的示例程序:

  • 这里有一个crossorigin的属性,这个属性的目的是为了拿到跨域脚本的错误信息
html
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  • 建议你验证使用的 CDN 是否设置了 Access-Control-Allow-Origin: * HTTP 请求头:

    image-20230315180343368

2、本地引入

下载3个依赖包到本地

image-20230315205648582

本地引入

html
    <div id="root"></div>

+    <script crossorigin src="../lib/react.js"></script>
+    <script crossorigin src="../lib/react-dom.js"></script>
+    <script src="../lib/babel.js"></script>

+    <script type="text/babel">
    ...
    </script>

Hello World

第一步:在界面上通过React显示一个Hello World

  • 注意:这里我们编写React的script代码中,必须添加 type="text/babel",作用是可以让babel解析jsx的语法

    image-20230315121921961

ReactDOM. createRoot函数:用于创建一个React根,之后渲染的内容会包含在这个根中

  • 参数:将渲染的内容,挂载到哪一个HTML元素上
    • 这里我们已经提定义一个id为app的div

**root.render函数:**渲染元素

  • 参数:要渲染的根组件

我们可以通过{}语法来引入外部的变量或者表达式

image-20230315181935083

Hello React-错误做法

image-20230316104658088

Hello React-正确做法

image-20230316104730673

Hello React-组件化开发

整个逻辑其实可以看做一个整体,那么我们就可以将其封装成一个组件:

  • 我们说过root.render 参数是一个HTML元素或者一个组件

  • 所以我们可以先将之前的业务逻辑封装到一个组件中,然后传入到 ReactDOM.render 函数中的第一个参数;

**在React中,如何封装一个组件呢?**这里我们暂时使用类的方式封装组件:

  • 1.定义一个类(类名大写,组件的名称是必须大写的,小写会被认为是HTML元素),继承自React.Component

  • 2.实现当前组件的render函数

    • render当中返回的jsx内容,就是之后React会帮助我们渲染的内容
js
    // 组件 - App
+    class App extends React.Component {

      // 属性
+      constructor() {
+        super()
+        this.state = {
+          msg: 'Hello World'
+        }
+      }

      // 方法
      onChange() {
+        this.setState({
          msg: 'Hello React'
        })
      }

      // 渲染
+      render() {
        return (
          <div>
            <div>{this.state.msg}</div>  
             // 注意2:在绑定事件函数时,需要显式bind(this),事件函数内部才能获取到当前组件实例App
+            <button onClick={this.onChange.bind(this)}>修改文本</button>
          </div>
        )
      }
    }

    // 渲染
    const root = ReactDOM.createRoot(document.querySelector('#root'))
    // 注意1:此处render参数中不能用引号包裹<App/>
+    root.render(<App/>)

注意:

  • 1、render参数中不能用引号包裹<App/>
  • 2、在绑定事件函数时,需要显式bind(this),事件函数内部才能获取到当前组件实例App

组件化-数据依赖

组件化问题一:数据在哪里定义?

组件中的数据,我们可以分成两类:

  • 参与界面更新的数据:当数据变量时,需要更新组件渲染的内容;

  • 不参与界面更新的数据:当数据变量时,不需要更新将组建渲染的内容;

参与界面更新的数据我们也可以称之为是参与数据流,这个数据是定义在当前对象的state中

  • 我们可以通过在构造函数中 this.state = {定义的数据}

  • 当我们的数据发生变化时,我们可以调用 `this.setState 来更新数据,并且通知React进行update操作;

    • 在进行update操作时,就会重新调用render函数,并且使用最新的数据,来渲染界面
js
    // 组件 - App
    class App extends React.Component {
      // 属性
      constructor() {
        super()
+        this.state = {
+          msg: 'Hello World'
+        }
      }

      // 渲染
      render() {
        return (
          <div>
+            <div>{this.state.msg}</div>  
          </div>
        )
      }
    }

注意:

  • 1、设置属性:在构造函数中设置:this.state = { key: value }
  • 2、访问属性:在渲染函数中访问:<div>{ this.state.msg }</div>
  • 3、修改属性:在事件函数中修改:this.setState({ key: newValue })

组件化-事件绑定

组件化问题二:事件绑定中的this

  • 在类中直接定义一个函数,并且将这个函数绑定到元素的onClick事件上,当前这个函数的this指向的是谁呢?

默认情况下是undefined

  • 很奇怪,居然是undefined

  • 因为在正常的DOM操作中,监听点击,监听函数中的this其实是节点对象(比如说是button对象);

  • 这是因为React并不是直接渲染成真实的DOM,我们所编写的button只是一个语法糖,它的本质是React的Element对象;

  • 那么在这里发生监听的时候,react在执行函数时并没有绑定this,默认情况下就是一个undefined;

我们在绑定的函数中,可能想要使用当前对象,比如执行 this.setState 函数,就必须拿到当前对象的this

  • 我们就需要在传入函数时,给这个函数直接绑定this

  • 类似于下面的写法:<button onClick={this.changeText.bind(this)}>改变文本</button>

js
      // 方法
      onChange() {
+        this.setState({
          msg: 'Hello React'
        })
      }

      // 渲染
+      render() {
        return (
          <div>
            <div>{this.state.msg}</div>  
             // 注意2:在绑定事件函数时,需要显式bind(this),事件函数内部才能获取到当前组件实例App
+            <button onClick={this.onChange.bind(this)}>修改文本</button>
          </div>
        )
      }

setState()方法做的事情:

  • 1、修改state中的值
  • 2、自动重新执行render函数

绑定事件函数的方法:

  • onClick={this.btnClick}
  • onClick={this.btnClick.bind(this)}
  • onClick={() => this.btnClick()

案例:电影列表展示

方法一:将movies数据转成liEls数组

image-20230315220030362

方法二:通过map映射movies数据

jsx
    // 组件-App
    class App extends React.Component {
      // 属性
      constructor() {
        super()
        this.state = {
+          movies: ['阿凡达', '流浪地球', '外星人ET', '变形金刚', '速度与激情', '致青春']
        }
      }

      // 渲染
      render() {
        return (
          <div>
            <h3>电影列表</h3>
            <ul>
+              { this.state.movies.map(item => <li key={item}>{item}</li>) }
            </ul>
          </div>
        )
      }
    }

    // 渲染
    const root = ReactDOM.createRoot(document.querySelector('#root'))
    root.render(<App/>)

image-20230316111851449

案例:计数器

js
    // 组件
    class App extends React.Component {
      // 属性
      constructor() {
        super()
        this.state = {
          count: 100
        }
      }

      // 渲染
      render() {
+        const { count } = this.state
        return (
          <div>
            <div className="count">当前计数:{ count }</div>
+            <button onClick={this.increment.bind(this)}> +1 </button>
+            <button onClick={this.decrement.bind(this)}> -1 </button>
          </div>
        )
      }

      // 方法
+      increment() {
+        this.setState({ count: this.state.count + 1 })
+      }
+      decrement() {
+        this.setState({ count: this.state.count - 1 })
+      }
    }

    const root = ReactDOM.createRoot(document.querySelector('#root'))
    root.render(<App/>)

image-20230317145012526

VSCode代码片段

我们在前面练习React的过程中,有些代码片段是需要经常写的,我们在VSCode中我们可以生成一个代码片段,方便我们快速生成。

VSCode中的代码片段有固定的格式,所以我们一般会借助于一个在线工具来完成。

具体的步骤如下:

  • 第一步,复制自己需要生成代码片段的代码

  • 第二步,https://snippet-generator.app/在该网站中生成代码片段

  • 第三步,在VSCode中配置代码片段

模板:

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title></title>
</head>
<body>
  <div id="root"></div>

  <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

  <script type="text/babel">
    // 组件
    class App extends React.Component {
      // 属性
      constructor() {
        super()
        this.state = {
          
        }
      }

      // 渲染
      render() {
        return (
          <div>
            
          </div>
        )
      }
    }

    const root = ReactDOM.createRoot(document.querySelector('#root'))
    root.render(<App/>)
  </script>
</body>
</html>

代码片段过程

image-20230315122324502

jsx语法

认识jsx

js
  <script type="text/babel">
    // 1. 定义根组件
    const el = <div>Hello World</div>

    // 2. 渲染根组件
    const root = ReactDOM.createRoot(document.querySelector('#root'))
    root.render(el)
  </script>

这段element变量的声明右侧赋值的标签语法<div>Hello World</div>是什么呢?

  • 不是一段字符串(因为没有使用引号包裹);

  • 它看起来是一段HTML元素,但是我们能在js中直接给一个变量赋值html吗?

  • 其实是不可以的,如果我们将 type="text/babel" 去除掉,那么就会出现语法错误;

  • 它到底是什么呢?其实它是一段jsx的语法

JSX是什么?

  • JSX 是 JS 语法扩展,可以让你在 JS 文件中书写类似 HTML 的标签。

  • jsx是一种JavaScript的语法扩展(JavaScript eXtension),也在很多地方称之为JavaScript XML,因为看起就是一段XML语法;

  • 它用于描述我们的UI界面,并且其完全可以和JavaScript融合在一起使用;

  • 它不同于Vue中的模块语法,你不需要专门学习模块语法中的一些指令(比如v-for、v-if、v-else、v-bind);

image-20230316120050582

为什么React选择了jsx

React认为渲染逻辑本质上与其他UI逻辑存在内在耦合

  • 比如UI需要绑定事件(button、a原生等等);

  • 比如UI中需要展示数据状态;

  • 比如在某些状态发生改变时,又需要改变UI;

他们之间是密不可分,所以React没有将标记分离到不同的文件中,而是将它们组合到了一起,这个地方就是组件(Component)

  • 当然,后面我们还是会继续学习更多组件相关的东西;

在这里,我们只需要知道,jsx其实是嵌入到JavaScript中的一种结构语法;

jsx的书写规范

  • 1、jsx的顶层只能有一个根元素,所以我们很多时候会在外层包裹一个div元素(或者使用后面我们学习的Fragment);

  • 2、为了方便阅读,我们通常在jsx的外层包裹一个小括号(),这样可以方便阅读,并且jsx可以进行换行书写;

  • 3、jsx中的标签可以是单标签,也可以是双标签;如果是单标签,必须以*/>*结尾;

jsx基本使用

jsx中的注释

js
      <div>
+        {/* 这是注释 */}  
        <div>{ msg }</div>
      </div>

注意:.jsx文件中可以直接通过快捷键ctrl + / 添加jsx的注释

jsx嵌入变量作为子元素

  • 情况一:当变量是NumberStringArray类型时,可以直接显示

    js
          // 属性
          constructor() {
            super()
            this.state = {
    +          msg: 'Hello React',
    +          num: 100,
    +          arr: ['历史', '语文', '数学'],
            }
          }
          // 渲染
          render() {
            const { msg, num, arr } = this.state
            return (
              <div>
                {/* 1. 直接显示String, Number, Array类型的数据 */}  
    +            <div>{ msg }</div>
    +            <div>{ num }</div>
    +            <div>{ arr }</div>
          }

    image-20230317153838237

  • 情况二:当变量是nullundefinedBoolean类型时,内容为空

    js
          // 属性
          constructor() {
            super()
            this.state = {
    +          n: null,
    +          u: undefined,
    +          b: true,
            }
          }
          // 渲染
          render() {
            const { n, u, b } = this.state
            return (
              <div>
                {/* 2. 空白显示null, undefined, Boolean类型的数据 */}
    +            <div>{ n }</div>
    +            <div>{ u }</div>
    +            <div>{ b }</div>
          }
    • 如果希望可以显示null、undefined、Boolean,那么需要转成字符串

    • 转换的方式有很多,比如toString方法、和空字符串拼接,String(变量)等方式;

      image-20230316123400446

  • 情况三:Object对象类型不能作为子元素(not valid as a React child)

    js
          // 属性
          constructor() {
            super()
            this.state = {
    +          obj: { name: 'Jack' }
            }
          }
          // 渲染
          render() {
            const { obj } = this.state
            return (
              <div>
                {/* 3. 报错:Object类型的数据 */}
    +            <div>{ obj }</div>
          }

    image-20230317154025158

jsx嵌入表达式

  • 运算表达式

  • 三元运算符

  • 执行一个函数

运算表达式

js
      // 属性
      constructor() {
        super()
        this.state = {
          firstName: '张',
          lastName: '飞',
          num1: 10,
          num2: 30
        }
      }

      // 渲染
      render() {
        const { firstName, lastName, num1, num2 } = this.state
        return (
          <div>
            {/* 1. 算数表达式 */}
+            <div>{ firstName + lastName }</div>
+            <div>{ num1 + num2 }</div>
+            <div>{ num1 - num2 }</div>
+            <div>{ num1 * num2 }</div>
+            <div>{ num1 / num2 }</div>
+            <div>{ num1 % num2 }</div>
+            <div>{ Math.abs(num1 - num2) }</div>
        )
      }

三元运算符

js
      // 属性
      constructor() {
        super()
        this.state = {
+          flag: true
        }
      }

      // 渲染
      render() {
        const { flag } = this.state
        return (
          <div>
            {/* 2. 三元运算符 */}
+            <div>{ flag ? '真' : '假' }</div>
        )
      }

执行一个函数

js
      // 渲染
      render() {
        return (
            {/* 3. 执行函数 */}
+            <div>{ this.getRadom() }</div>
        )
      }

+      getRadom() {
        return Math.floor(Math.random() * 10)
      }

属性绑定

  • 比如元素都会有title属性

  • 比如img元素会有src属性

  • 比如a元素会有href属性

  • 比如元素可能需要绑定class

  • 比如原生使用内联样式style

基本绑定

基本属性:title、src、href等

js
        this.state = {
          msg: 'Hello React',
          title: 'React',
          href: 'https://www.baidu.com',
          src: 'https://n.sinaimg.cn/sinacn10211/360/w180h180/20191010/8404-ifrwayx4734762.jpg',
        }
      render() {
        return (
          <div>
            {/* 注意:绑定基本属性 */}
+            <div title={title}>{msg}</div>
+            <a href={href}>百度一下</a>
+            <img src={src} alt={title}/>
        )
      }

绑定class

写法一:字符串拼接

js
        // 属性
        constructor() {
          super()
          this.state = {
+            isActive: true
          }
        }

        // 渲染
        render() {
+          const { isActive } = this.state

          return (
            <div>
              {/* 1. 字符串拼接 */}
+              <h2 className={`red size${isActive ? ' active' : ''}`}>字符串拼接</h2>
            </div>
          )
        }

写法二:数组拼接

js
        this.state = {
+          isActive: true
          aClass: ['bd', 'active'],
        }
      render() {
        const { isActive, aClass } = this.state
+        if(isActive) aClass.push('active')
        return (
          <div>
 +           <div className={aClass.join(' ')}>绑定多个class-数组</div>
		  </div>
        )
      }

写法三:第三方库 classnames

classnames写法和vue很相似

绑定style:绑定对象类型

js
        this.state = {
+          oStyle: { color: 'red', backgroundColor: '#eee' }
        }
        return (
          <div>
            {/* 3. 绑定样式 */}
+            <div style={{ fontWeight: 600 }}>绑定样式</div>
+            <div style={oStyle}>绑定样式</div>
          </div>
        )

事件绑定

如果原生DOM原生有一个监听事件,我们可以如何操作呢?

  • 方式一:获取DOM原生,添加监听事件;

  • 方式二:在HTML原生中,直接绑定onclick;

**在React中是如何操作呢?**我们来实现一下React中的事件监听,这里主要有两点不同

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写;

  • 我们需要通过*{}传入一个事件处理函数*,这个函数会在事件发生时被执行;

this的绑定问题

在事件执行后,我们可能需要获取当前类的对象中相关的属性,这个时候需要用到this

  • 如果我们这里直接打印this,也会发现它是一个undefined

为什么是undefined呢?

  • 原因是btnClick函数并不是我们主动调用的,而且当button发生改变时,React内部调用了btnClick函数

  • 而它内部调用时,并不知道要如何绑定正确的this;

如何绑定this呢?

  • 方案一:bind()给btnClick显式绑定this

    js
                <div>{ msg }</div>
                {/* 1. 通过bind(this)在绑定事件的时候传递this */}
     +           <button onClick={this.onChangeMsg.bind(this)}>按钮1</button>
    
          // 方法
          onChangeMsg() {
            console.log('this', this);
            this.setState({ msg: '你好世界' })
          }
  • 方案二:使用 ES6 class fields 语法

    js
            return (
              <div>
                <div>{ msg }</div>
                {/* 2. 通过es6的 class fields语法写函数 */}
    +            <button onClick={this.onChangeMsg2}>按钮2</button>
              </div>
            )
            
    +      onChangeMsg2 = () => {
            console.log('this', this)
            this.setState({ msg: '你好,中国' })
          }
  • 方案三:事件监听时传入箭头函数(个人推荐

    js
            return (
              <div>
                <div>{ msg }</div>
                {/* 3. 通过箭头函数绑定事件:(e) => this.onChangeMsg3(e) */}
    +            <button onClick={(e) => this.onChangeMsg3(e)}>按钮3</button>
              </div>
            )
            
    +      onChangeMsg3(e) {
            console.log('this', this, e)
            this.setState({ msg: '你好,安徽' })
          }

事件参数传递

在执行事件函数时,有可能我们需要获取一些参数信息:比如event对象其他参数

情况一:获取event对象

  • 很多时候我们需要拿到event对象来做一些事情(比如阻止默认行为)

  • 那么默认情况下,event对象有被直接传入,函数就可以获取到event对象;

    js
            return (
              <div>
    +            <button onClick={(e) => this.onChangeMsg3(e)}>按钮3</button>
              </div>
            )
            
    +      onChangeMsg3(e) {
            console.log('this', this, e)
          }

情况二:获取更多参数

  • 使用bind的方式的话,event参数会被放在btnClick的参数列表的最后【不推荐

    js
        {/* 1. 通过bind的方法传递参数 */}
    +    <button onClick={this.showUserInfo.bind(this, '刘备', 44, '女')}>修改个人信息</button>
    
    +    showUserInfo(name, age, gender, e) {
            console.log('name:', name, 'age: ', age, 'gender: ', gender, 'this: ', this, 'e: ', e);
        }
  • 有更多参数时,我们最好的方式就是传入一个箭头函数,主动执行的事件函数,并且传入相关的其他参数;

    js
          render() {
            return (
              <div>
                {/* 2. 通过箭头函数传递参数 */}
    +            <button onClick={e => this.getPraram(e, '徐志摩', 22, '男')}>传递参数-箭头函数</button>
              </div>
            )
          }
    +      getPraram(e, name, age, gender) {
    +        console.log('name:', name, 'age: ', age, 'gender: ', gender, 'this: ', this, 'e: ', e);
    +      }

案例:电影列表的选中

js
    // 组件-App
    class App extends React.Component {
      // 属性
      constructor() {
        super()
        this.state = {
          movies: ['阿凡达', '流浪地球', '外星人ET', '变形金刚', '速度与激情', '致青春'],
+          currIndex: 0
        }
      }

      // 方法
+      onChange(index) {
+        this.setState({ currIndex: index })
+      }

      // 渲染
      render() {
        const { movies, currIndex } = this.state
        return (
          <div>
            <h3>电影列表</h3>
            <ul>
              { movies.map((item, index) => {
                return (
                  <li key={item} 
+                      className={currIndex === index ? 'active' : ''} 
+                      onClick={() => this.onChange(index)}>
                    {item}
                  </li>
                )
              }) }
            </ul>
          </div>
        )
      }
    }

    // 渲染
    const root = ReactDOM.createRoot(document.querySelector('#root'))
    root.render(<App/>)

案例:电影列表的选中-抽取1

image-20230316161931064

案例:电影列表的选中-抽取2

image-20230316162554152

条件渲染

某些情况下,界面的内容会根据不同的情况显示不同的内容,或者决定是否渲染某部分内容:

  • 在vue中,我们会通过指令来控制:比如v-if、v-show;

  • React中,所有的条件判断都和普通的JavaScript代码一致

常见的条件渲染的方式有哪些呢?

方式一:条件判断语句if

  • 适合逻辑较多的情况
js
    class App extends React.Component {
      // 属性
      constructor() {
        super()
        this.state = {
+          isReady: true
        }
      }

      // 渲染
      render() {
        const { isReady } = this.state
        
        // 条件渲染
+        let showEl
+        if(isReady) {
+          showEl = <h2>开始!</h2>
+        }else {
+          showEl = <h2>准备!</h2>
+        }

        return (
          <div>
+            <button onClick={() => this.setState({ isReady: !isReady })}>切换状态</button>
            <hr/>
+            {showEl}
          </div>
        )
      }
    }

方式二:三元运算符?:**

  • 适合逻辑比较简单
js
        return (
+            <div>{isReady ? <h2>开始!</h2> : <h2>准备!</h2>}</div>
        )

方式三:与运算符&&**

  • 适合如果条件成立,渲染某一个组件;如果条件不成立,什么内容也不渲染;

使用场景: 当某个值有可能为undefined / null时,使用&& 进行条件判断

js
    class App extends React.Component {
      // 属性
      constructor() {
        super()
        this.state = {
+          isTrue: true,
+          isNotNull: null,
+          isNotUndefined: undefined,
            
          isNotEmpty: {},
          isNotEmptyArray: []
          
+          friend: { name: 'Jack' }
        }
      }

      // 渲染
      render() {
        const { isTrue, isNotNull, isNotUndefined, isNotEmpty, isNotEmptyArray } = this.state

        return (
          <div>
+            <div>{isTrue && <h2>开始!</h2>}</div>
+            <div>{!isNotNull && <h2>不为null</h2>}</div>
+            <div>{!isNotUndefined && <h2>不为undefined</h2>}</div>
            
            <div>{!isNotEmpty && <h2>不为Empty</h2>}</div>
            <div>{!isNotEmptyArray && <h2>不为EmptyArray</h2>}</div>
                   
+            <div>{friend && <h2>{friend.name}</h2>}</div>
          </div>
        )
      }
    }

image-20230317171858063

方式四:可选链??**

案例:v-if效果

image-20230316164835224

案例:v-show效果

主要是控制display属性是否为none

js
      constructor() {
        super()
        this.state = {
+          isShow: true
        }
      }

      // 渲染
      render() {
        const { isShow } = this.state
        return (
          <div>
+            <button onClick={() => this.setState({ isShow: !isShow })}>切换</button>
+            <div style={ { display: isShow ? 'block' : 'none' } }>显示</div>
          </div>
        )
      }

列表渲染

真实开发中我们会从服务器请求到大量的数据,数据会以列表的形式存储:

  • 比如歌曲、歌手、排行榜列表的数据;

  • 比如商品、购物车、评论列表的数据;

  • 比如好友消息、动态、联系人列表的数据;

在React中并没有像Vue模块语法中的v-for指令,而且需要我们通过JavaScript代码的方式组织数据,转成jsx

  • 很多从Vue转型到React的同学非常不习惯,认为Vue的方式更加的简洁明了;

  • 但是React中的jsx正是因为和JavaScript无缝的衔接,让它可以更加的灵活;

  • 另外我经常会提到React是真正可以提高我们编写代码能力的一种方式;

如何展示列表呢?

  • 在React中,展示列表最多的方式就是使用数组的map高阶函数;

很多时候我们在展示一个数组中的数据之前,需要先对它进行一些处理:

  • 比如过滤掉一些内容:filter函数

  • 比如截取数组中的一部分内容:slice函数

1、展示所有学生

js
    class App extends React.Component {
      // 属性
      constructor() {
        super()
        this.state = {
          aStudent: [
            { name: '张飞', age: 22, score: 78 },
            { name: '刘备', age: 25, score: 88 },
            { name: '关羽', age: 23, score: 98 },
            { name: '曹操', age: 27, score: 100 }
          ]
        }
      }

      // 渲染
      render() {
        const { aStudent } = this.state
        return (
          <div>
+            {
        		// 1. 全部学生
+              aStudent.map(item => {
+                return (
+                  <div className="item" key={item.name}>
+                    <span>姓名:{item.name}</span>
+                    <span>年龄:{item.age}</span>
+                    <span>分数:{item.score}</span>
+                  </div>
+                )
+              })
+            }
          </div>
        )
      }
    }

2、只展示分数大于90的人,链式调用

filter(item => item.score > 90)

js
          return (
            <div>
              {
                  // 2. 分数大于90的学生 
+                aStudent.filter(item => item.score > 90).map(item => {
                  return (
                    <div className="item" key={item.name}>
                      <span>姓名:{item.name}</span>
                      <span>年龄:{item.age}</span>
                      <span>分数:{item.score}</span>
                    </div>
                  )
                })
              }
            </div>
          )

3、分数大于100,只展示前2个人的信息,链式调用

slice(0, 2)

js
          return (
            <div>
              {
                // 3. 分数大于90,展示前2位
+                aStudent.filter(item => item.score > 90).slice(0, 2).map(item => {
                  return (
                    <div className="item" key={item.name}>
                      <span>姓名:{item.name}</span>
                      <span>年龄:{item.age}</span>
                      <span>分数:{item.score}</span>
                    </div>
                  )
                })
              }
            </div>
          )

列表中的key

我们会发现在前面的代码中只要展示列表都会报一个警告:

image-20230316114846200

这个警告是告诉我们需要在列表展示的jsx中添加一个key。

  • key主要的作用是为了提高diff算法时的效率

  • 这个我们在后续内容中再进行讲解;

js
    aStudent.map(item => {
      return (
+        <div className="item" key={item.name}>
          <span>姓名:{item.name}</span>
          <span>年龄:{item.age}</span>
          <span>分数:{item.score}</span>
        </div>
      )
    })

jsx的本质

实际上,jsx 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖。

  • 所有的jsx最终都会被转换成React.createElement的函数调用。

createElement需要传递三个参数:

参数一:type

  • 当前ReactElement的类型;

  • 如果是标签元素,那么就使用字符串表示 “div”;

  • 如果是组件元素,那么就直接使用组件的名称;

参数二:props

  • 所有jsx中的属性都在props中以对象的属性和值的形式存储;

  • 比如传入className作为元素的class;

参数三:children

  • 存放在标签中的内容,以children数组的方式进行存储;

  • 当然,如果是多个元素呢?React内部有对它们进行处理,处理的源码在下方

image-20230316181140881

createElement源码

image-20230316114912539

Babel官网查看

我们知道默认jsx是通过babel帮我们进行语法转换的,所以我们之前写的jsx代码都需要依赖babel

可以在babel的官网中快速查看转换的过程:https://babeljs.io/repl/#?presets=react

image-20230316114927515

image-20230316180544839

直接编写jsx代码

我们自己来编写React.createElement代码:

  • 我们就没有通过jsx来书写了,界面依然是可以正常的渲染。

  • 另外,在这样的情况下,你还需要babel相关的内容吗?不需要了

    • 所以,type="text/babel"可以被我们删除掉了;
    • 所以,<script src="../react/babel.min.js"></script>可以被我们删除掉了;
html
<div>
  <div>Header</div>
  <div>
    Content
    <div className="left">Left</div>
    <div className="right">Right</div>
  </div>
  <div>Footer</div>  
</div>

转化成ReactElement的形式

js
    const el = React.createElement(
                "div",
                null,
                /*#__PURE__*/ React.createElement("div", null, "Header"),
                /*#__PURE__*/ React.createElement(
                  "div",
                  null,
                  "Content",
                  /*#__PURE__*/ React.createElement(
                    "div",
                    {
                      className: "left"
                    },
                    "Left"
                  ),
                  /*#__PURE__*/ React.createElement(
                    "div",
                    {
                      className: "right"
                    },
                    "Right"
                  )
                ),
                /*#__PURE__*/ React.createElement("div", null, "Footer")
              );

    const root = ReactDOM.createRoot(document.querySelector('#root'))
    root.render(el)

虚拟DOM的创建过程

我们通过 React.createElement 最终创建出来一个 ReactElement对象

image-20230316115045061

这个ReactElement对象是什么作用呢?React为什么要创建它呢?

  • 原因是React利用ReactElement对象组成了一个JS对象树

  • JS对象树就是虚拟DOM(Virtual DOM);

如何查看ReactElement的树结构呢?

  • 我们可以将之前的jsx返回结果进行打印;

  • 注意下面代码中我打jsx的打印;

而ReactElement最终形成的树结构就是Virtual DOM

image-20231027145024038

ReactElement

属性:

  • type:``,表示组件的类型(函数组件、类组件或原生 DOM 元素)
  • key:``,用于帮助 React 在进行列表渲染时识别每个元素
  • props:``,包含组件的属性(如className)和子元素(children)
  • ref:``,表示为对组件真正实例的引用

特性:

  • 虚拟表示:ReactElement 是组件在内存中的虚拟表示,它描述了组件的结构和属性,但并不直接对应于实际的 DOM 元素。
  • 不可变性:ReactElement 是不可变的,一旦创建就不能被修改。如果你想更新组件的状态或属性,需要创建一个新的 ReactElement。

JSX – 虚拟DOM – 真实DOM

image-20230316115114624

虚拟DOM的作用:

  • 1、通过diff算法,实现页面内容的局部更新
  • 2、跨平台渲染(web,iOS, Andriod)
  • 3、帮助我们从命令式编程转到了声明式编程的模式

声明式编程

虚拟DOM帮助我们从命令式编程转到了声明式编程的模式

**React官方的说法:**Virtual DOM 是一种编程理念。

  • 在这个理念中,UI以一种理想化或者说虚拟化的方式保存在内存中,并且它是一个相对简单的JavaScript对象

  • 我们可以通过root.render让 虚拟DOM 和 真实DOM同步起来,这个过程中叫做协调(Reconciliation);

这种编程的方式赋予了React声明式的API:

  • 你只需要告诉React希望让UI是什么状态;

  • React来确保DOM和这些状态是匹配的;

  • 不需要直接进行DOM操作,就可以从手动更改DOM、属性操作、事件处理中解放出来;

关于虚拟DOM的一些其他内容,在后续的学习中还会再次讲到;

案例:书籍购物车

1.在界面上以表格的形式,显示一些书籍的数据;

2.在底部显示书籍的总价格;

3.点击+或者-可以增加或减少书籍数量(如果为1,那么不能继续-);

4.点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:购物车为空~);

image-20230316115134666

1、界面搭建

js
    class App extends React.Component {
      // 属性
      constructor() {
        super()
        this.state = {
          aBook: [
            { id: 0, name: '⟪三国演义⟫', pubDate: '2006-2', price: '86.00', count: 0 },
            { id: 1, name: '⟪水浒传⟫', pubDate: '2007-3', price: '76.00', count: 0 },
            { id: 2, name: '⟪西游记⟫', pubDate: '2002-11', price: '45.00', count: 0 },
            { id: 3, name: '⟪红楼梦⟫', pubDate: '2003-8', price: '78.00', count: 0 }
          ]
        }
      }

      // 渲染
      render() {
        const { aBook } = this.state
        return (
          <div>
            <table>
              <thead>
                <tr>
                  <th>序号</th>
                  <th>名称</th>
                  <th>出版日期</th>
                  <th>价格</th>
                  <th>购买数量</th>
                  <th>操作</th>
                </tr>
              </thead>  
              <tbody>
                {
                  aBook.map((item, index) => {
                    return (
                      <tr>
                        <td>{item.id + 1}</td>
                        <td>{ item.name }</td>
                        <td>{ item.pubDate }</td>
                        <td>{ item.price }</td>
                        <td>
                          <button> - </button>  
                          <span className="count">{item.count}</span>
                          <button> + </button>  
                        </td>
                        <td><button>移除</button></td>
                      </tr>
                    )
                  })
                }
                
              </tbody>
            </table>
            <h3>总价格:333</h3>
          </div>
        )
      }
    }

    const root = ReactDOM.createRoot(document.querySelector('#root'))
    root.render(<App/>)

2、计算总价格

js
const totalPrice = aBook.reduce((prevValue, item) => prevValue + item.price*item.count, 0)
<h3>总价格:{totalPrice}</h3>

3、加减数量

注意: react中不要直接修改 state中的数组,对象内部的子元素,而是要对它进行浅拷贝后修改

js
    // 加减购买数量
    changeCount(index, pm) {
        const aNewBook = [...this.state.aBook]
        aNewBook[index].count += pm 
        this.setState({ aBook: aNewBook })
    }

<td>
    <button disabled={item.count <= 1 ? true : false} onClick={() => this.changeCount(index, -1)}> - </button>  
	<span className="count">{item.count}</span>
	<button onClick={() => this.changeCount(index, 1)}> + </button>  
</td>

4、移除商品

js
    // 移除书籍
    removeBook(index) {
        const aNewBook = [...this.state.aBook]
        aNewBook.splice(index, 1)
        this.setState({ aBook: aNewBook })
    }

<td><button onClick={() => this.removeBook(index)}>移除</button></td>

5、购物车为空提示

js
        // 有书籍显示表格,没有书籍显示空提示
        let elCart
        if(aBook.length === 0){
          elCart = <div>空白购物车~</div>
        } else {
          elCart = (
          <div>
            <table>省略...</table>
            <h3>总价格:{totalPrice}</h3>
          </div>
        )
        } 
        return elCart

React脚手架

认识脚手架工具

前端工程的复杂化

如果我们只是开发几个小的demo程序,那么永远不需要考虑一些复杂的问题:

  • 比如目录结构如何组织划分;

  • 比如如何管理文件之间的相互依赖;

  • 比如如何管理第三方模块的依赖;

  • 比如项目发布前如何压缩、打包项目;

  • 等等...

现代的前端项目已经越来越复杂了:

  • 不会再是在HTML中引入几个css文件,引入几个编写的js文件或者第三方的js文件这么简单;

  • 比如css可能是使用less、sass等预处理器进行编写,我们需要将它们转成普通的css才能被浏览器解析;

  • 比如JavaScript代码不再只是编写在几个文件中,而是通过模块化的方式,被组成在成百上千个文件中,我们需要通过模块化的技术来管理它们之间的相互依赖;

  • 比如项目需要依赖很多的第三方库,如何更好的管理它们(比如管理它们的依赖、版本升级等);

为了解决上面这些问题,我们需要再去学习一些工具:

  • 比如babel、webpack、gulp,配置它们转换规则、打包依赖、热更新等等一些的内容;

  • 脚手架的出现,就是帮助我们解决这一系列问题的;

脚手架是什么呢?

传统的脚手架指的是建筑学的一种结构:在搭建楼房、建筑物时,临时搭建出来的一个框架;

image-20230317104313791

编程中提到的脚手架(Scaffold),其实是一种工具,帮我们可以快速生成项目的工程化结构

  • 每个项目作出完成的效果不同,但是它们的基本工程化结构是相似的;

  • 既然相似,就没有必要每次都从零开始搭建,完全可以使用一些工具,帮助我们生产基本的工程化模板;

  • 不同的项目,在这个模板的基础之上进行项目开发或者进行一些配置的简单修改即可;

  • 这样也可以间接保证项目的基本结构一致性,方便后期的维护;

总结:脚手架让项目从搭建到开发,再到部署,整个流程变得快速和便捷;

create-react-app

缩写:CRA

前端脚手架

对于现在比较流行的三大框架都有属于自己的脚手架:

  • Vue的脚手架:

    • 基于Webpack:@vue/cli

    • 基于Vite: create-vue

  • Angular的脚手架:@angular/cli

  • React的脚手架:create-react-app

它们的作用都是帮助我们生成一个通用的目录结构,并且已经将我们所需的工程环境配置好

使用这些脚手架需要依赖什么呢?

  • 目前这些脚手架都是使用node编写的,并且都是基于webpack的;

  • 所以我们必须在自己的电脑上安装node环境

这里我们主要是学习React,所以我们以React的脚手架工具:create-react-app作为讲解;

安装node

React脚手架本身需要依赖node,所以我们需要安装node环境:

  • 无论是windows还是Mac OS,都可以通过node官网直接下载;

  • 官网地址:https://nodejs.org/en/download/

  • 注意:这里推荐大家下载LTS(Long-term support )版本,是长期支持版本,会比较稳定;

image-20230317104350749

下载后,双击安装即可:

1.安装过程中,会自动配置环境变量;

2.安装时,会同时帮助我们安装npm管理工具;

安装create-react-app

sh
npm i create-react-app -g

创建React项目

现在,我们就可以通过脚手架来创建React项目了。

创建React项目的命令如下:

  • 注意:项目名称不能包含大写字母

  • 另外还有更多创建项目的方式,可以参考GitHub的readme

sh
create-react-app <项目名>

创建完成后,进入对应的目录,就可以将项目跑起来:

sh
cd 01-test-react
npm start

image-20230317104407746

目录结构分析

我们可以通过VSCode打开项目:

image-20230317104424651

了解PWA

整个目录结构都非常好理解,只是有一个PWA相关的概念:

  • PWA全称Progressive Web App,即渐进式WEB应用

  • 一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用;

  • 随后添加上 App ManifestService Worker 来实现 PWA 的安装和离线缓存等功能;

  • 这种Web存在的形式,我们也称之为是 Web App;

PWA解决了哪些问题呢?

  • 可以添加至主屏幕,点击主屏幕图标可以实现启动动画以及隐藏地址栏;

  • 实现离线缓存功能,即使用户手机没有网络,依然可以使用一些离线功能;

  • 实现了消息推送

  • 等等一系列类似于Native App相关的功能;

更多PWA相关的知识,可以自行去学习更多;

https://developer.mozilla.org/zh-CN/docs/Web/Progressive_web_apps

webpack配置

React脚手架默认是基于Webpack来开发的;

但是,很奇怪:我们并没有在目录结构中看到任何webpack相关的内容?

  • 原因是React脚手架将webpack相关的配置隐藏起来了(其实从Vue CLI3开始,也是进行了隐藏);

如果我们希望看到webpack的配置信息,应该怎么来做呢?

  • 我们可以执行一个package.json文件中的一个脚本:"eject": "react-scripts eject"

  • 这个操作是不可逆的,所以在执行过程中会给与我们提示;

sh
npm run eject

image-20230317104442252

webpack的配置文件在 config/scripts/ 2个目录中

脚手架中的webpack

image-20230317104456797

文件结构删除

通过脚手架创建完项目,很多同学还是会感觉目录结构过于复杂,所以我打算从零带着大家来编写代码。

我们先将不需要的文件统统删掉:

  • 1.将src下的所有文件都删除

  • 2.将public文件下出列favicon.ico和index.html之外的文件都删除掉

image-20230317104514107

开始编写代码

在src目录下,创建一个index.js文件,因为这是webpack打包的入口

在index.js中开始编写React代码:

  • 我们会发现和写的代码是逻辑是一致的;

  • 只是在模块化开发中,我们需要手动的来导入React、ReactDOM,因为它们都是在我们安装的模块中;

js
import ReactDOM from 'react-dom/cli'
import App from './App'

const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)

如果我们不希望直接在 root.render 中编写过多的代码,就可以单独抽取一个组件App.js

js
import React from 'react'

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      msg: 'Hello React'
    }
  }
  render() {
    const { msg } = this.state 
    return (
      <div>
        <div>App根组件{ msg }</div>
      </div>
    )
  }
}

export default App

将App组件封装到App.jsx文件中

image-20231027162829673