react(part1/2)

1.React介绍

1.1.React起源与发展

React 起源于Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。

1.2.React与传统MVC的关系

轻量级的视图层库!A JavaScript library for building user interfaces

React不是一个完整的MVC框架,最多可以认为是MVC中的V(View),甚至React并不非常认可MVC开发模式;React 构建页面 UI 的库。可以简单地理解为:React 将界面分成了各个独立的小块,每一个块就是组件,这些组件之间可以组合、嵌套,就成了我们的页面。

1.3.React的特性

  1. 声明式设计-React采用声明范式,可以轻松描述应用。

    如果从后端拿回数据,按照react规则写好模板(不用自己判断、创建for循环、生成元素插入指定节点),react自己实现渲染,后端传来的数据改变react自己更新。

  2. 高效-React通过对DOM的模拟(虚拟dom),最大限度地减少与DOM的交互。

    虚拟dom最大限度减少对dom的操作

  3. 灵活-React可以与已知的库或框架很好地配合。

    可以与好看的ui框架,快速完成大型项目开发

  4. JSX- JSX是 JavaScript语法的扩展。

    JSX- JSX可以解析html、js、css

  5. 组件-通过React构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。

    组件复用,例如轮播

  6. 单向响应的数据流–React实现了单向响应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。

1.4.虚拟DOM

  1. 传统dom更新

    传统的dom更新一个页面节点时,需要直接在dom上进行节点的增删改,引发文档的重排(reflow)和重绘(repaint),这个过程非常耗费浏览器性能。

  2. 虚拟dom

    在 React 中,虚拟 DOM (Virtual DOM) 是一个抽象的概念,它是使用 JavaScript 对象来表示真实 DOM 树的一种方式。当组件的状态(state)发生变化时,React 会创建一个新的虚拟 DOM 树,并通过比较新旧两棵虚拟 DOM 树的差异(Diff 算法)来计算出需要更新的部分,然后仅仅更新这些部分对应的真实 DOM 节点,而不是重新渲染整个页面。

    这种方式相比传统的直接操作真实 DOM 树的方式,具有更高的性能和更好的用户体验。因为虚拟 DOM 能够对比出真实 DOM 树中发生变化的部分,并进行局部更新,减少了页面的重排(reflow)和重绘(repaint)次数,从而提升了页面的渲染性能和用户的交互体验。

    在 React 中,当组件的状态发生变化时,React 会调用组件的 render 方法来创建一个新的虚拟 DOM 树,然后将新的虚拟 DOM 树与旧的虚拟 DOM 树进行比较,计算出需要更新的部分,然后将这些部分对应的真实 DOM 节点更新到页面上。

    虚拟 DOM 的工作原理可以归纳为以下几个步骤:

    1. 组件的状态(state)发生变化时,React 调用组件的 render 方法,创建一个新的虚拟 DOM 树。
    2. React 将新的虚拟 DOM 树与旧的虚拟 DOM 树进行比较,计算出需要更新的部分。
    3. React 根据计算出的差异,更新对应的真实 DOM 节点。
    4. 组件的状态变化引起了新的渲染时,重复上述步骤。

    总之,虚拟DOM 技术是 React 中实现高效渲染的核心,通过将真实 DOM 树的操作转换为对虚拟 DOM 树的操作,大大提高了页面渲染的性能和用户的交互体验。

2.create-react-app

  1. 检查node.js是否已安装

    node -v
    

    安装node.js是为了下载react需要依赖的各种包和各种环境。

  2. 通过npm全局安装create-react-app(react脚手架环境)

    npm install -g create-react-app
    
    1. 这个命令会在全局安装 React 脚手架环境,使得我们可以在命令行中使用 create-react-app 命令来创建新的React项目。注意:该命令需要管理员权限运行,如果在命令行中出现权限不足的错误提示,请使用管理员权限运行命令行工具。

    2. react脚手架环境的作用:帮助开发者快速创建一个hello world的react应用,不用开发者自己配置环境。

    3. 全局安装方式安装的react脚手架不会更新,需要自己手动更新。也可以使用npx进行临时安装create-react-app,它是最新的,下次安装继续这样,每次都是最新的react脚手架。

      npx creat-react-app your-app-name 注意命名方式
      
  3. 检查create-react-app是否安装成功

    create-react-app -V
    
  4. 创建一个react项目

    create-react-app your-app-name 注意命名方式
    
    16110602300

    这需要等待一段时间,这个过程实际上会安装三个东西

    • react: react的顶级库
    • react-dom: 因为react有很多的运行环境,比如app端的react-native, 我们要在web上运行就使用react-dom
    • react-scripts: 包含运行和打包react应用程序的所有脚本及配置

    出现下面的界面,表示创建项目成功:

    Success! Created myapp at D:\Projects\CodeReact\myapp
    Inside that directory, you can run several commands:
    
      npm start
        Starts the development server.
    
      npm run build
        Bundles the app into static files for production.
    
      npm test
        Starts the test runner.
    
      npm run eject
        Removes this tool and copies build dependencies, configuration files
        and scripts into the app directory. If you do this, you can鈥檛 go back!
    
    We suggest that you begin by typing:
    
      cd myapp
      npm start
    
    Happy hacking!
    
  5. 启动react项目

    1. 先进入新建react项目中

      [D:\Projects\CodeReact]$ cd myapp
      
    2. 启动项目

      [D:\Projects\CodeReact\myapp]$ npm start
      

      生成项目的目录结构如下:

      ├── README.md 												使用方法的文档
      ├── node_modules 											所有的依赖安装的目录
      ├── package-lock.json 										锁定安装时的包的版本号,保证团队的依赖能保证一致。
      ├── package.json
      ├── public 													静态公共目录
      └── src 													开发用的源代码目录
      

      开发中常见问题:

      1. 如何复制他人项目,并保证环境一致能运行?

        如果要复制其他人的项目,为保证能正常运行,依赖的版本必须一致。只需要拷贝别人项目,删除node_modules,在项目中运行:

        npm i
        

        npm会根据package.json(锁定大版本)和package-lock.json(锁定大版本和依赖源)完成依赖环境的下载

      2. 如果npm安装失败

        • 切换为npm镜像为淘宝镜像
        • 使用yarn,如果本来使用yarn还要失败,还得把yarn的源切换到国内
        • 如果还没有办法解决,请删除node_modules及package-lock.json然后重新执行npm install命令
        • 再不能解决就删除node_modules及package-lock.json的同时清除npm缓存npm cache clean --force 之后再执行npm install 命令
  6. react项目运行成功

    http://localhost:3000/

3.编写第一个react应用程序

react项目的入口index.html

react开发需要引入多个依赖文件:react.js、react-dom.js,分别又有开发版本和生产版本,createreact-app里已经帮我们把这些东西都安装好了。把通过CRA创建的工程目录下的src目录清空,然后在里面重新创建一个index.js.

名称为什么文件名称必须叫index.js?

  • 在 React 项目的 src 文件夹下必须存在一个 index.js 文件,这是因为在使用 create-react-app 脚手架工具创建 React 项目时,它会默认将 src/index.js 文件作为项目的入口文件。

  • 具体来说,src/index.js 文件会被作为 React 应用程序的根组件,并渲染到页面上,所以这个文件非常重要。在这个文件中,我们通常会使用 ReactDOM.render() 方法来将根组件渲染到页面上的某个 DOM 元素中。

  • 此外,src/index.js 文件也是 Webpack 打包工具的入口文件,Webpack 会将该文件作为打包的起点,自动处理依赖关系,并将打包后的代码输出到指定的目录中。

因此,在 React 项目中,src/index.js 文件的作用非常重要,它承担着项目入口、根组件和打包工具入口等多重角色

写入以下代码:

import React from 'react'
import ReactDOM from 'react-dom'
ReactDOM.render(<h1>欢迎进入React的世界</h1>,
// 渲染到哪里
    document.getElementById('root')
)
  1. import React from ‘react’

    从react 的包当中引入了React。只要你要写React.js组件就必须引入React, 因为react里有一种语法叫JSX(js+xml),要写JSX,就必须引入React。

  2. import ReactDOM from ‘react-dom’

    ReactDOM可以帮助我们把React组件渲染到页面上去,没有其它的作用了。它是从react-dom中引入的,而不是从react引入。

  3. ReactDOM.render()方法

    ReactDOM里有一个render方法,功能就是把组件渲染并且构造DOM树,然后插入到页面上某个特定的元素上

    1. 把123456渲染到root节点上

      ReactDOM.render("123456", document.getElementById('root'))
      

      呈现效果:

      <div id="root">123456</div>
      
    2. 把html片段渲染到root节点上

      ReactDOM.render(<div>123456</div>, document.getElementById('root'))
      

      效果:

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

    这里就比较奇怪了,它并不是一个字符串,看起来像是纯 HTML代码写在 JavaScript 代码里面。语法错误吗?这并不是合法的 JavaScript 代码, “在 JavaScript写的标签的”语法叫JSX-JavaScript-XML。

注意:
<React.StrictMode> 目前有助于:

  1. 识别不安全的生命周期
  2. 关于使用过时字符串 ref API 的警告
  3. 检测意外的副作用
  4. 检测过时的 context API

4.JSX语法与组件

4.1.JSX语法

JSX将HTML语法直接加入到JavaScript代码中,再通过翻译器转换到纯 JavaScript 后由浏览器执行。在实际开发中,JSX 在产品打包阶段都已经编译成纯 JavaScript,不会带来任何副作用,反而会让代码更加直观并易于维护。 编译过程由Babel的JSX编译器实现。

https://reactjs.org/docs/hello-world.html

原理是什么呢?

要明白JSX的原理,需要先明白如何用 JavaScript 对象来表现一个 DOM 元素的结构?

看下面的DOM结构

<div class='app' id='appRoot'>
<h1 class='title'>欢迎进入React的世界</h1>
    <p>
    	React.js 是一个帮助你构建页面 UI 的库
    </p>
</div>

上面这个HTML所有的信息我们都可以用 JavaScript 对象来进行描述:

{
    tag: 'div', attrs: { className: 'app', id: 'appRoot'},
    children: [
        {
            tag: 'h1',
            attrs: { className: 'title' },
            children: ['欢迎进入React的世界']
        },
        {
            tag: 'p',
            attrs: null,
            children: ['React.js 是一个构建页面 UI 的库']
        }
    ]
}

但是用 JavaScript 写起来太长了,结构看起来又不清晰,用 HTML 的方式写起来就方便很多了。

于是 React.js 就把 JavaScript 的语法扩展了一下,让 JavaScript 语言能够支持这种直接在 JavaScript 代码里面编写类似 HTML 标签结构的语法,这样写起来就方便很多了。编译的过程会把类似 HTML 的 JSX结构转换成 JavaScript 的对象结构。

  • 简单示例1

    ReactDOM.render(<div>123456</div>, document.getElementById('root'))
    

    Babel编译器将其等价转换为:

    ReactDOM.render(React.createElement("div",{id:"aaa",class:"bbb"},123456), document.getElementById('root'))
    
  • 复杂示例2

    import React from 'react'
    import ReactDOM from 'react-dom'
    
    class App extends React.Component {
        render() {
            return (
                <div className='app' id='appRoot'>
                    <h1 className='title'>欢迎进入React的世界</h1>
                    <p>
                        React.js 是一个构建页面 UI 的库
                    </p>
                </div>
            )
        }
    }
    
    ReactDOM.render(
        <App/>,
        document.getElementById('root')
    )
    

    Babel编译器将其等价转换为:

    import React from 'react'
    import ReactDOM from 'react-dom'
    
    class App extends React.Component {
        render() {
            return (
                React.createElement(
                    "div",
                    {
                        className: 'app',
                        id: 'appRoot'
                    },
                    React.createElement(
                        "h1",
                        {className: 'title'},
                        "欢迎进入React的世界"
                    ),
                    React.createElement(
                        "p",
                        null,
                        "React.js 是一个构建页面 UI 的库"
                    )
                )
            )
        }
    }
    
    ReactDOM.render(
        React.createElement(App),
        document.getElementById('root')
    )
    

React.createElement方法会构建一个 JavaScript 对象来描述你 HTML 结构的信息,包括标签名、属性、还有子元素等, 语法为

React.createElement(
    type,
    [props],
    [...children]
)

所谓的 JSX 其实就是 JavaScript 对象,所以使用 React 和 JSX 的时候一定要经过编译的过程:

整个 JSX 的编译流程可以分为以下几步:

  1. Babel 编译:在 React 项目中使用 JSX 语法需要通过 Babel 转换成 JavaScript 代码,这是因为 JSX 语法并不是标准的 JavaScript 语法,无法直接在浏览器中执行。
  2. React.createElement() 方法:在编译后的 JavaScript 代码中,JSX 语法会被转换为调用 React.createElement() 方法的形式。例如 <h1>Hello, world!</h1> 会被转换为 React.createElement("h1", null, "Hello, world!")
  3. 虚拟 DOM 对象:React.createElement() 方法会返回一个虚拟 DOM 对象,它是一个 JavaScript 对象,描述了要创建的 DOM 元素的类型、属性和子元素等信息。
  4. 渲染到页面:最后,React 会根据虚拟 DOM 对象,生成实际的 DOM 元素,并将它们渲染到页面中。

总的来说,JSX 语法能够让我们更加方便、直观地编写 React 组件的结构和行为,并且经过编译后会被转换为常规的 JavaScript 代码,在浏览器中执行。

JSX —>使用react构造组件,bable进行编译—> JavaScript对象 — ReactDOM.render() —>DOM元素 —>插入页面

4.2.react组件

把一个功能模块(比如轮播、导航栏、选项卡)的html、js、css代码封装成一个组件,可以按照不同的需求在不同的页面进行复用和随意组合。

React 中组件的好处包括以下几个方面:

  1. 可重用性:React 中的组件是可重用的,我们可以在不同的地方多次使用同一个组件,避免了重复编写相同代码的问题,也方便了代码的维护和更新。
  2. 可组合性:React 中的组件是可组合的,我们可以将多个组件组合在一起构成更复杂的 UI 界面,这样可以方便地组织代码,使得代码更加清晰易懂。
  3. 易于维护性:React 中的组件具有良好的可维护性,组件内部封装了自己的状态(State)和逻辑,使得我们能够更加专注于单个组件的开发和维护,避免了代码的冗余和复杂度。
  4. 灵活性:React 中的组件是非常灵活的,我们可以通过修改组件的 props 属性和状态(State)来实现组件的动态渲染和交互。这使得我们可以根据不同的场景需求,快速地进行开发和迭代,而不必担心代码的兼容性和维护问题。

总的来说,React 中的组件是一种非常强大和灵活的工具,能够帮助我们构建高效、可重用、可组合、易于维护的 UI 界面,提高了代码的开发效率和可读性。

4.2.1.类组件

ES6的加入让JavaScript直接支持使用class来定义一个类,react创建组件的方式就是使用的类的继承,ES6 class 是目前官方推荐的使用方式,它使用了ES6标准语法来构建,看以下代码:

  1. 创建一个Class组件

    import React from "react";
    
    class App extends React.Component {
        render() {
            return (<div>
                    <div>11111</div>
                    hello react Component</div>
                    )
        }
    }
    
    export default App
    

    注意点:

    1. 必须先引入"react"类,类继承React.Component组件类。如果不继承该类创建的才是真正的组件类,否则就是普通类,使用时会报错。

    2. 组件类的最终效果时靠render()函数渲染的,所以render()函数在Class组件中是必须,而且render()必须有返回值。

    3. 通过export default App把组件类导出去时不能改名字,如果在index.js(或其他使用该组件的页面)中导入该组件可以随便改名字

      import App111 from "./01-base/01-class组件";
      或者
      import App from "./01-base/01-class组件";
      

      以上两种写法均可,使用<App111/>或者<App>均可

    4. 在index.js(或其他使用该组件的页面)中导入组件重命名时,必须首字母大写,例如App,不能写app,因为这样容易被浏览器认为是标签自带的,不是组件。

    5. render()函数返回值必须包含在一个闭合的标签里,不允许出现两个或者多个标签。

    6. render()函数返回值可以全部包含在小括号里。

  2. 在index.js中引用类组件并使用

    • react 18之前

      import React from 'react'
      import ReactDOM from 'react-dom'
      import {createRoot} from "react-dom/client";
      import App111 from "./01-base/01-class组件";
      
      ReactDOM.render(<App111/>, document.getElementById('root'));
      
    • react 18

      由于React 18 不再支持 ReactDOM.render。控制台会抛出:

      解决办法:index.js入口文件中改用 createRoot

      import React from 'react'
      import ReactDOM from 'react-dom'
      import {createRoot} from "react-dom/client";
      import App111 from "./01-base/01-class组件";
      
      const container = document.getElementById('root');
      const root = createRoot(container);
      root.render(<App111/>)
      
  3. 效果

4.2.2.函数式组件

  1. 创建函数式组件(functional component)

    function App1() {
        return (
            <div> hello functional component
                <div>1111</div>
                <div>2222</div>
            </div>
        )
    }
    export default App1
    

    这样一个完整的函数式组件就定义好了。但要注意组件名必须大写,否则报错。

  2. 在index.js中引用函数式组件并使用

    import React from 'react'
    import ReactDOM from 'react-dom'
    import {createRoot} from "react-dom/client";
    import App from "./01-base/02-函数式组件";
    
    const container = document.getElementById('root');
    const root = createRoot(container);
    root.render(<App/>)
    

    函数式组件之前(16.8)叫无状态组件(比如无法写一个计数器),16.8之后引入react hooks后可以不再是无状态组件。现在比较推荐react hooks + 函数式组件

  3. 效果

4.3.组件的嵌套

  1. 定义一个根组件App以及被根组件嵌套的多个子组件

    import React, {Component} from "react";
    
    /**
     * 根组件App
     */
    export default class App extends Component {
        render() {
            return (
                <div>
                    <Navbar></Navbar>
                    <Tabbar></Tabbar>
                    <Swiper></Swiper>
                </div>
            )
        }
    }
    
    /**
     * 类组件
     */
    class Child extends Component {
        render() {
            return <div>Child</div>
        }
    }
    
    /**
     * 类组件
     */
    class Navbar extends Component {
        render() {
            return <div>Navbar<Child></Child></div>
        }
    }
    
    /**
     * 普通函数式组件
     */
    function Swiper() {
        return <div>swiper</div>
    }
    
    /**
     * 箭头函数组件
     */
    const Tabbar = () => <div>tabular</div>
    
    1. App组件是Navbar、Swiper、Tabbar组件的,App还是整个项目的根组件
    2. 如果想要在App组件的子组件Navbar中引入新的Child组件,让Child组件成为Navbar组件的子组件,即关系为
      App组件->Navbar组件->Child组件。则应该在Navbar组件定义模板中引入,不应该在App组件引入Navbar组件时引入Child组件。
  2. 引入根组件

    import React from 'react'
    import ReactDOM from 'react-dom'
    import {createRoot} from "react-dom/client";
    import App from "./01-base/03-组件的嵌套";
    
    const container = document.getElementById('root');
    const root = createRoot(container);
    root.render(<App/>)
    
  3. 最终效果

4.4.组件的样式

4.4.1.react简单行内表达式运算

在react中用{ }将要运算的表达式括起来,react会将{ }里的内容进行计算。表达运算一般为数字字符串加减运算,三元表达式等简单运算。被大括号括起来的部分可以直接进行运算。

render() {
        var myName = "stonebridge"
        return (<div>
            {10 + 30}-{myName}<br/>   //40-stonebridge
            {10 > 30 ? '111' : '222'} //222
        </div>)
    }

4.4.2.添加行内样式

添加行内样式时可以通过行内表达式运算的方式进行设置添加,通过style={obj}进行样式设置,或者直接在双大括号内定义行类样式添加样式。行内样式需要写入一个样式对象,而这个样式对象的位置可以放在很多地方,例如render 函数里、组件原型上、外链js文件中

注意:添加行内样式的用横杠链接属性需要改为小驼峰写法,如:backgroud-color写为backgroundColor;font-size改为fontSize

render() {
    var obj = {
        backgroundColor: 'yellow',
        fontSize: '50px',
    }
    return (<div>
        <div style={obj}>123456</div>
        // 注意这里的两个括号,第一个表示我们在要JSX里插入JS了,第二个是对象的括号
        <div style={{background: 'black'}}>123456</div>
    </div>)
}

4.4.3.引入外部css文件

  1. 定义外部css文件,在其中正常书写样式

    .active{
        background: aqua;
    }
    #myApp{
        background: burlywood;
    }
    
  2. 在js文件中通过import引入

    import './css/01-index.css' //导入css模块, webpack支持把css里面的内容做成一个内部样式,最终加在页面的style标签中
    
    import React, {Component} from "react";
    import './css/01-index.css' //导入css模块, webpack支持把css里面的内容做成一个内部样式,加在style标签中
    export default class App extends Component {
        render() {
            return (<div>
                <div className="active">3333333333333</div>
                <div id="myApp">5555555556666666666666666666</div>
            </div>)
        }
    }
    

注意点:

  1. React推荐我们使用行内样式,因为React觉得每一个组件都是一个独立的整体。

  2. 给标签元素添加类名class时,要使用className属性,因为在js文件中会和定义类的关键字class冲突,故使用className来定义样式class属性;

    在给label添加for属性时,要使用htmlFor设置,使用for设置会和js里关键字冲突。

    import React, {Component} from "react";
    import './css/01-index.css' //导入css模块, webpack支持把css里面的内容做成一个内部样式,加在style标签中
    export default class App extends Component {
        render() {
            return (<div>
                        ……
                //使用className设置class属性
                <div className="active">3333333333333</div>
                //使用htmlFor设置for属性        
                <label htmlFor="userName">用户名</label>
                <input type="text" id="userName"/>
            </div>)
        }
    }
    
    function Login() {
      return (
        <form>
          <label htmlFor="username">Username:</label>
          <input type="text" id="username" name="username" />
          <label htmlFor="password">Password:</label>
          <input type="password" id="password" name="password" />
          <button type="submit">Login</button>
        </form>
      );
    }
    

4.5.事件处理

4.5.1.事件handler的写法

采用on+事件名的方式来绑定一个事件,注意,这里和原生的事件是有区别的,原生的事件全是小写onclick , React里的事件是驼峰onClick,React的事件并不是原生事件,而是合成事件。

import React, {Component} from "react";

export default class App extends Component {
    render() {
        return (
            <div>
                <input type="text"/>
                {/*事件绑定方式1:直接在render里写行内的箭头函数*/}
                <button onClick={(() => {
                    console.log("click1")
                })}>button-add1
                </button>
                {/*绑定onMouseMove事件*/}
                <button onMouseMove={() => console.log("onMouseMove1")}>button-onMouseMove</button>
                <br/>
                {/*事件绑定方式2:直接在组件内定义一个非箭头函数的方法,然后在render里直接使用引用*/}
                <button onClick={this.handleClick2}>button-add2</button>
                {/*事件绑定方式3:在组件内使用箭头函数定义一个方法,然后在render里直接使用引用*/}
                <button onClick={this.handleClick3}>button-add3</button>
                {/*事件绑定方式4:在render里箭头函数中调用其他函数*/}
                <button onClick={() => {
                    this.handleClick2()
                }}>button-add4
                </button>
            </div>
        )
    }

    handleClick2() {
        console.log("handleClick2")
    }

    handleClick3 = () => {
        console.log("handleClick3")
    }
}

直接给标签事件绑定函数名时(方式2和方式3),函数不加括号为被动执行,需要触发才能执行;如果函数加括号则页面加载完毕后直接执行函数,点击时却不会再执行。

4.5.2.函数调用的this指向问题

  1. 直接在render里写行内的箭头函数方式(不推荐)的this指向

    export default class App extends Component {
        a = 1000
    
        render() {
            return (
                <div>
                    <input type="text"/>
                    {/*1.事件绑定方式1;这个匿名函数中的this和render()函数中的this一致,即类App的实例,可以访问到a的值*/}
                    <button onClick={(() => {
                        console.log("click1", this.a) //this指向类App的实例
                    })}>button-add1
                </div>
            )
        }
    }
    

    在这种事件函数绑定方式中,可以通过this.a获取a的值。因为这个内部函数中的this和render()函数中的this一致,即类App的实例,可以访问到a的值。

  2. 在组件内定义一个非箭头函数的方法,然后在render里直接使用引用方式的this指向

    import React, {Component} from "react";
    
    export default class App extends Component {
        a = 1000
    
        render() {
            return (
                <div>
                    {/*3.事件绑定方式2*/}
                    <button onClick={this.handleClick2}>button-add2</button>
                </div>
            )
        }
    
        handleClick2() {
            console.log("handleClick2",this)  //this不指向类App的实例,指向调用对象button标签。
        }
    }
    

    在这种事件函数绑定方式中,无法通过this.a的方式获取a的值,因为此时的this指向的是调用的它的标签,即button标签。

    但是可以通过onClick={this.handleClick.bind(this)}的方式将this的指向类App的实例,即此时handleClick2函数类的this指向的是类App的实例,而不是button标签,且不会自动执行函数。

    改变this指向的三种方式:

    1. call:可改变this,自动执行函数
    2. apply:可改变this,自动执行函数
    3. bind:可改变this,不会自动执行函数,可手动加括号执行函数。
    var obj1 = {
        name: "obj1",
        getName() {
            console.log(this.name)
        }
    }
    
    var obj2 = {
        name: "obj2",
        getName() {
            console.log(this.name)
        }
    }
    obj1.getName.call(obj2)   //obj2
    obj1.getName.apply(obj2)  //obj2
    obj1.getName.bind(obj2)() //obj2
    
  3. 在组件内使用箭头函数定义一个方法,然后在render里直接使用引用方式的this指向(推荐)

    import React, {Component} from "react";
    
    export default class App extends Component {
        a = 1000
    
        render() {
            return (
                <div>
                    <button onClick={this.handleClick3}>button-add3</button>
                </div>
            )
        }
        
        handleClick3 = () => {
            console.log("handleClick3", this) //指向类App的实例
        }
    }
    

    在组件内使用箭头函数定义一个方法(推荐),此时this指向类App的实例,可以通过this.a获取到a的值。

  4. 在render里箭头函数中调用其他函数方式的指向(推荐)

    export default class App extends Component {
        a = 1000
    
        render() {
            return (
                <div>
                    <button onClick={() => this.handleClick4()}>button-add4</button>
                </div>
            )
        }
    
        handleClick4() {
            console.log("handleClick4", this) //指向类App的实例
        }
    }
    

该方式无论调用的是否是箭头函数,函数内的this都指向类App的实例

4.5.3.Event对象

React和传统的事件绑定方式的对比:React并不会真正的绑定事件到每一个具体的</>元素上,而是采用事件代理的模式。

和普通浏览器一样,事件handler会被自动传入一个 event 对象,这个对象和普通的浏览器 event 对象所包含的方法和属性都基本一致。不同的是 React中的 event 对象并不是浏览器提供的,而是它自己内部所构建的。它同样具有event.stopPropagation 、event.preventDefault 这种常用的方法。

import React, {Component} from "react";

export default class App extends Component {
    a = 1000

    render() {
        return (
            <div>
                <button onClick={this.handleClick3}>button-add3</button>
            </div>
        )
    }

    handleClick3 = (evt) => {
        console.log("handleClick3",evt)
    }
}

4.6.Ref的应用

获取在输入标签(input标签、select标签)中用户输入的值。

4.6.1.给标签设置ref=“username”

render() {
    return (
        <div>
            <input ref="username"/>
            <button onClick={(() => {
                console.log("click1", this.refs.username);
            })}>button1
            </button>
        </div>
    )
}

this.refs.username通过这个获取this.refs.username , ref可以获取到应用的真实dom,通过this.refs.username就可以获取输入框中的值。

4.6.2.给组件设置ref=“username”

通过这个获取this.refs.username ,ref可以获取到组件对象。

4.6.3.定义createRef对象(Ref的应用推荐写法)

export default class App extends Component {
    // 1.定义createRef对象
    username = React.createRef()

    render() {
        return (
            <div>
                {/*2.在标签中引入createRef对象*/}
                <input ref={this.username}/>
                <button onClick={(() => {
                    // 3.获取标签中输入的值
                    console.log("click1", this.username.current, this.username.current.value);
                })}>button1
                </button>
            </div>
        )
    }
}

注意点:

  • react严格模式(React.StrictMode)

    这种写法即使在react严格模式(React.StrictMode)下也是不会提示错误的,建议开发时开启react严格模式(React.StrictMode),检查潜在错误。

    import React from 'react';
    import ReactDOM from 'react-dom';
    
    ReactDOM.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>,
      document.getElementById('root')
    );
    
  • 如果调用非箭头函数会因为this指向出现问题

    此时handleClick2的this指向不是类App的实例,所以会报错。

    export default class App extends Component {
        username = React.createRef()
    
        render() {
            return (
                <div>
                    <input ref={this.username}/>
                    <button onClick={this.handleClick2}>button2
                    </button>
                </div>
            )
        }
        handleClick2() {
            console.log("handleClick2", this.username.current.value)  //handleClick2 undefined
        }
    }
    

    解决方案1:使用bind更改this指向

    <button onClick={this.handleClick2.bind(this)}>button2
    

    解决方案2:将handleClick2函数改为箭头函数

    handleClick2 = () => {
        console.log("handleClick2", this.username.current.value) 
    }
    

4.7.常用的JS函数

4.7.1.splice()

JavaScript中的splice()方法是用于添加、删除或替换数组中的元素。它可以通过指定起始位置和要删除或添加的元素数量来修改数组。

splice()方法可以接受两个或三个参数。第一个参数是要操作的起始位置(索引的起始值是0),第二个参数是要删除的元素数量,可选的第三个参数是要添加到数组中的元素。

下面是一些常见的用法:

  1. 删除元素

    const arr = ['apple', 'banana', 'cherry', 'durian'];
    arr.splice(2, 1); // 从第2个位置开始删除1个元素
    console.log(arr); // ["apple", "banana", "durian"]
    
  2. 添加元素

    const arr = ['apple', 'banana', 'cherry', 'durian'];
    arr.splice(2, 0, 'orange', 'grape'); // 从第2个位置开始添加元素
    console.log(arr); // ["apple", "banana", "orange", "grape", "cherry", "durian"]
    
  3. 替换元素

    const arr = ['apple', 'banana', 'cherry', 'durian'];
    arr.splice(2, 2, 'orange', 'grape'); // 从第2个位置开始替换元素
    console.log(arr); // ["apple", "banana", "orange", "grape"]
    

需要注意的是,splice()方法会直接修改原数组,因此需要谨慎使用。

4.7.2.filter() & includes()

var arr =["aaaa","abc","bccc"]
var newarr = arr.filter(item=>item.includes("b"))
console.log(newarr)

4.7.3.map()

const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map((number) => number * 2);
console.log(doubledNumbers); // [2, 4, 6, 8, 10]

在这个示例中,map()函数将数组numbers中的每个元素乘以2,并将结果存储在一个新的数组doubledNumbers中。

需要注意的是,map()函数返回的是一个新的数组,原始数组不会被修改。此外,map()函数的回调函数可以接受三个参数:当前元素的值,当前元素的索引和原始数组本身。这些参数可以用于对元素进行操作。例如:

const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map((number, index, array) => {
  console.log(`当前元素:${number}`);
  console.log(`当前索引:${index}`);
  console.log(`原始数组:${array}`);
  return number * 2;
});

在这个示例中,回调函数输出了当前元素的值,当前元素的索引和原始数组。然后,将每个元素乘以2,并将结果存储在一个新的数组中。

4.7.4.Object.assign() & (…)

ES6 中合并对象可以使用展开运算符(…)或 Object.assign() 方法。

  1. 使用展开运算符:

    const obj1 = { a: 1, b: 2 };
    const obj2 = { c: 3, d: 4 };
    const mergedObj = { ...obj1, ...obj2 };
    console.log(mergedObj); // { a: 1, b: 2, c: 3, d: 4 }
    
  2. 使用 Object.assign() 方法:

    const obj1 = { a: 1, b: 2 };
    const obj2 = { c: 3, d: 4 };
    const mergedObj = Object.assign({}, obj1, obj2);
    console.log(mergedObj); // { a: 1, b: 2, c: 3, d: 4 }
    

需要注意的是,如果对象中有重复的属性,后面的属性会覆盖前面的属性,因此在合并对象时需要特别注意属性名的唯一性。另外,使用展开运算符合并对象时,如果对象中包含引用类型的属性,则需要注意对象的深拷贝问题。

4.7.5.substring

5.组件的数据挂载方式-状态(state)

5.1.状态(state)

状态就是组件描述某种显示情况的数据,由组件自己设置和更改,也就是说由组件自己维护,使用状态的目的就是为了在不同的状态下使组件的显示不同(自己管理)

在React中,state是一个表示组件内部状态的JavaScript对象。它可以存储并跟踪组件中需要随时间变化的值,例如用户交互或异步数据请求返回的结果。当组件的state值发生改变时,用户不操作dom对象,React会重新渲染组件,并更新视图以反映最新的状态(页面数据完成修改)。

state在React中非常重要,因为它使组件能够动态响应用户的交互和其他事件,从而提供更好的用户体验。使用状态设置数据,数据最终由react组件根据用户的需求渲染出来,当数据改变时只需要更改状态,不用操作dom对象就可以实现页面数据的修改。

5.1.1.定义state

  • 方式一:

    state变量名是固定的

    import React, {Component} from 'react'
    import ReactDOM from 'react-dom'
    
    class App extends Component {
        state = {
            name: 'React',
            isLiked: false
        }
    
        render() {
            return (
                <div>
                    <h1>欢迎来到{this.state.name}的世界</h1>
                    <button>
                        {
                            this.state.isLiked ? '❤ 取消' : '🖤收藏'
                        }
                    </button>
                </div>
            )
        }
    }
    
    ReactDOM.render(
        <App/>,
        document.getElementById('root')
    )
    
  • 方式2:

    import React, {Component} from 'react'
    import ReactDOM from 'react-dom'
    
    class App extends Component {
        constructor() {
            super()
            this.state = {
                name: 'React',
                isLiked: false
            }
        }
    
        render() {
            return (
                <div>
                    <h1>欢迎来到{this.state.name}的世界</h1>
                    <button>
                        {
                            this.state.isLiked ? '❤ 取消' : '🖤收藏'
                        }
                    </button>
                </div>
            )
        }
    }
    
    ReactDOM.render(
        <App/>,
        document.getElementById('root')
    )
    

5.1.2.setState修改状态

5.1.2.1.基础概念

使用state需要在组件的构造函数中初始化state对象,然后可以通过this.state来访问和修改state。通常情况下,应该避免直接修改state的值,而是通过调用setState()方法来更新state。这将告诉React更新组件的状态,并重新渲染组件。因为在react中this.state是纯js对象,在vue中,data属性是利用Object.defineProperty 处理过的,更改 data的数据的时候会触发数据的getter 和setter ,但是React中没有做这样的处理,如果直接更改的话,react是无法得知的,所以,需要使用特殊的更改状态的方法setState 。

isCollect存放在实例的 state 对象当中,组件的 render 函数内,会根据组件的 state 的中的isCollect 不同显示“取消收藏”或“收藏”内容。下面给button加上了点击的事件监听。

方式1:

import React, {Component} from "react";

export default class App extends Component {
    //state变量名是固定的
    state = {
        myText: "收藏",
        isCollect: false
    }

    render() {
        return (
            <div>
                <h1>欢迎来到react开发</h1>
                <button onClick={this.collectFile}>{this.state.isCollect ? "收藏" : '取消收藏'}</button>
            </div>
        )
    }

    collectFile = () => {
        this.setState({
            isCollect: !this.state.isCollect
        })
        if (this.state.isCollect) {
            console.log("完成收藏的逻辑,给后端发送信息")
        } else {
            console.log("完成取消收藏的逻辑,给后端发送信息")
        }
    }
}

方式2:

import React, {Component} from "react";

export default class App extends Component {
    //state变量名是固定的
    // state = {
    //     myText: "收藏",
    //     isCollect: false
    // }
    constructor() {
        super()
        this.state = {
            isCollect: false,
            userName: "stonebridge"
        }
    }

    render() {
        return (
            <div>
                <h1>欢迎来到react开发-{this.state.userName}</h1>
                <button onClick={this.collectFile}>{this.state.isCollect ? "收藏" : '取消收藏'}</button>
            </div>
        )
    }

    collectFile = () => {
        this.setState({
            isCollect: !this.state.isCollect,
            userName: 'zhangsan'
        })
        if (this.state.isCollect) {
            console.log("完成收藏的逻辑,给后端发送信息")
        } else {
            console.log("完成取消收藏的逻辑,给后端发送信息")
        }
    }
}

setState 有两个参数:

第一个参数可以是对象,也可以是方法return一个对象,我们把这个参数叫做updater

  • 参数是对象

    this.setState({
    	isLiked: !this.state.isLiked
    })
    
  • 参数是方法

    this.setState((prevState, props) => {
        return {
            isLiked: !prevState.isLiked
        }
    })
    

    注意的是这个方法接收两个参数,第一个是上一次的state, 第二个是props

5.1.2.2.setState方法的异步

在React中,setState方法默认是异步的,这意味着当调用setState时,React并不会立即更新组件状态,而是将其添加到队列中,稍后再异步更新状态。如果您想强制将setState变成同步,您可以使用回调函数来在setState完成后立即执行代码。

this.setState((prevState, props) => {
    return {
        isLiked: !prevState.isLiked
    }
}, () => {
	console.log('回调里的',this.state.isLiked)
})
console.log('setState外部的',this.state.isLiked)

如果在setTimeout()中执行setState方法时,setState处在异步的逻辑中,同步更新状态,同步更新真实dom。(不推荐)

setTimeout({
    this.setState({
		name:'11'
    })
},0)
5.1.2.3.不能在render函数中调用setState函数

虽然理论上可以在render()函数中调用setState(),但是这样做可能会导致无限循环渲染,从而降低性能甚至导致浏览器崩溃。这是因为每次调用setState()函数都会重新触发render()函数,而在render()函数中又调用了setState()函数,形成了无限循环。

因此,在编写React组件时,通常不会在render()函数中调用setState()函数。如果需要根据当前的propsstate计算一些值,可以在componentDidMount()componentDidUpdate()生命周期函数中进行计算,然后将计算结果设置为组件的状态,从而实现更新渲染。

5.2.循环遍历

react遵循的原则是如不必要,勿增实体。所以react的循环遍历为原生JS的遍历方式。

5.2.1.原生JS遍历

listForList = () => {
    var list = ["aaaa", "bbbbb", "cccccc"]
    var newlist = list.map(item => `<li>${item}</li>`)
    console.log(newlist.join(''))  //<li>aaaa</li><li>bbbbb</li><li>cccccc</li>
}

5.2.2.react中使用JS原生遍历方式

export default class App extends Component {
    state = {
        list: ["111", "23333", "33333333333"]
    }

    render() {
        return (
            <div>
                <ul>
                    {this.state.list.map(item => <li key={item}>{item}</li>)}
                </ul>
            </div>
        )
    }
}

5.2.3.给循环元素加上唯一标识key

React的高效依赖于所谓的 Virtual-DOM,尽量不碰 DOM。对于列表元素来说会有一个问题:元素可能会在一个列表中改变位置。要实现这个操作,只需要交换一下 DOM 位置就行了,但是React并不知道其实我们只是改变了元素的位置,所以它会重新渲染后面两个元素(再执行 Virtual-DOM ),这样会大大增加 DOM 操作。但如果给每个元素加上唯一的标识,React 就可以知道这两个元素只是交换了位置,这个标识就是key ,这个 key 必须是每个元素唯一的标识。

key最理想的是来源后端,直接通过item.id即可。

// 数据
const people = [{
    id: 1,
    name: 'Leo',
    age: 35
}, {
    id: 2,
    name: 'XiaoMing',
    age: 16
}]
// 渲染列表
{
    people.map(person => {
        return (
            <dl key={person.id}>
                <dt>{person.name}</dt>
                <dd>age: {person.age}</dd>
            </dl>
        )
    })
}

如果不涉及列表的增删,重排等需求就可以设置索引(不推荐)

export default class App extends Component {
    state = {
        list: ["111", "23333", "33333333333"]
    }

    render() {
        return (
            <div>
                <ul>
                    {this.state.list.map((item,index) => <li key={item}>{item}--->{index}</li>)}
                </ul>
            </div>
        )
    }
}

5.3.深拷贝

  • 方式1:

    var a = [1,2,3]
    var b = [...a]
    b.push(4)
    a //(3) [1, 2, 3]
    b //(4) [1, 2, 3, 4]
    
  • 方法2:

    slice()函数不传参数就是深拷贝

    var a = [1,2,3]
    var c = a.slice()  
    a.push(1000)
    a //(4) [1, 2, 3, 1000]
    c //(3) [1, 2, 3]
    

5.4.条件渲染

<div>
    <input ref={this.toDodeTail}/>
    <button onClick={this.handleClick}>add</button>
    <ul>
        {
  			……
        }
    </ul>
    {/*条件渲染方式1:条件满足就会创建div片段*/}
    {/*{this.state.list.length === 0 ? <div>暂无待办事件</div> : null}*/}
    {/*条件渲染方式2:条件满足就会创建div片段*/}
    {this.state.list.length === 0 && <div>暂无待办事件</div>}
    {/*条件渲染方式3:条件满足就会显示div片段,否则就会隐藏*/}
    <div className={this.state.list.length === 0 ? '' : 'hidden'}>暂无待办事件</div>
</div>
  1. 条件渲染方式1:使用if条件满足就会创建div片段

    {this.state.list.length === 0 ? <div>暂无待办事件</div> : null}
    
  2. 条件渲染方式2:使用运算符计算,条件满足就会创建div片段

    {this.state.list.length === 0 && <div>暂无待办事件</div>}
    
  3. 条件渲染方式3:条件满足就会显示div片段,否则就会隐藏(div片段一只都是创建的)。

    注意:通过className设置div的class

    <div className={this.state.list.length === 0 ? '' : 'hidden'}>暂无待办事件</div>
    

5.5.富文本编辑

在 React 中,如果要在组件中渲染富文本(如包含 HTML 标签、样式等),通常会使用 dangerouslySetInnerHTML 属性。这个属性的名称中带有 “dangerously” 这个词,是因为它可以允许将未经过滤的字符串渲染为 HTML。

使用 dangerouslySetInnerHTML 时,需要将要渲染的 HTML 字符串设置为一个对象的属性值,并将这个对象作为dangerouslySetInnerHTML 的值传递给组件。例如:

function RichText({html}) {
  return <div dangerouslySetInnerHTML={{__html: html}} />;
}

dangerouslySetInnerHTML 主要的应用场景是在需要将富文本数据以 HTML 格式展示的场景中,例如在网页上显示带有样式和图片的文章内容、显示富文本的聊天记录等。需要注意的是,由于使用了 dangerouslySetInnerHTML,渲染的内容可能包含未经过滤的脚本,可能存在安全风险。因此,在使用 dangerouslySetInnerHTML 时,需要确保所渲染的内容是可信的,或者需要先经过严格的安全过滤。

5.6.todolist案例

import React, {Component} from "react";
import './css/01-index.css'

export default class App extends Component {
    //1.定义定义createRef对象
    toDodeTail = React.createRef();
    //2.定义状态存储tolist的临时数据
    state = {
        list: [{
            id: 1,
            myText: "aaaa"
        }, {
            id: 2,
            myText: "bbb"
        }, {
            id: 3,
            myText: "ccc"
        },]
    }

    render() {
        return (
            <div>
                {/*3.在标签中引入createRef对象*/}
                <input ref={this.toDodeTail}/>
                {/*4.对用户输入的值进行保存操作,更新到state中。*/}
                <button onClick={this.handleClick}>add</button>
                <ul>
                    {
                        //5.使用原生JS遍历state.list对象
                        this.state.list.map((item, index) =>
                            <li key={item.id}>{/*{item.myText}*/}
                                {/*6.渲染富文本(如包含 HTML 标签、样式等)*/}
                                <span dangerouslySetInnerHTML={
                                    {
                                        __html: item.myText
                                    }
                                }></span>
                                {/*7.通过bind函数修正this,且把该项的索引(index)传参,进行后续修改*/}
                                {/*<button onClick={this.delClick.bind(this, index)}>delete</button>*/}
                                {/*8.通过箭头函数中调用其他函数方式,可以传值,便于后续修改*/}
                                <button onClick={() => this.delClick(index)}>delete</button>
                            </li>
                        )
                    }
                </ul>
                {/*9.条件渲染*/}
                {/*9.1.条件渲染方式1:条件满足就会创建div片段*/}
                {/*{this.state.list.length === 0 ? <div>暂无待办事件</div> : null}*/}
                {/*9.2.条件渲染方式2:条件满足就会创建div片段*/}
                {this.state.list.length === 0 && <div>暂无待办事件</div>}
                {/*9.3.条件渲染方式2:条件满足就会显示div片段,否则就会隐藏*/}
                <div className={this.state.list.length === 0 ? '' : 'hidden'}>暂无待办事件</div>
            </div>
        )
    }

    /**
     * 4.对用户输入的值进行保存操作,更新到state中。
     */
    handleClick = () => {
        // 4.1.深拷贝state里的值
        let newList = [...this.state.list]
        //4.2.不要直接修改状态,可能会造成不可预期的问题,修改深拷贝的数据再对state进行更新
        newList.push({
            id: Math.random() * 100000,
            myText: this.toDodeTail.current.value
        })
        //4.3.使用setState对state进行更新
        this.setState({
            list: newList
        })
        //4.4.清空输入框数据
        this.toDodeTail.current.value = ""
    }

    /**
     * 9.用户点击删除后,将数据从state中移除,完成修改
     * @param index
     */
    delClick = (index) => {
        // 9.1.深拷贝state里的值
        let newList = this.state.list.slice()
        // 9.2.使用splice(起始索引,删除数量)删除数据中的值
        newList.splice(index, 1)
        // 9.3.使用setState对state进行更新
        this.setState({
            list: newList
        })
    }
}

5.7.axios

  1. 在react项目中下载axios第三方库(专门用于请求数据)

    npm i axios
    
  2. 使用axios

    axios.get("请求地址").then(res => {
        console.log(res)
        // todo
    }).catch(err => {
        console.error(err)
    })
    
  3. 示例:请求卖座电影

    axios.get("https://m.maizuo.com/gateway?cityId=440300&ticketFlag=0&k=6939037").then(res => {
        console.log(res)
    }).catch(err => {
        console.error(err)
    })
    

    出现报错,被后台拒绝,因为缺乏一些数据,我们可以在请求头添加数据

    因为要在请求头headers添加更多的数据,所以采用axios的复杂写法

    axios({
        url: "https://m.maizuo.com/gateway?cityId=440300&ticketFlag=0&k=6939037",
        method: "get",
        headers: {
            'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.2.1","e":"16821403713477519055454209"}',
            'X-Host': 'mall.film-ticket.cinema.list'
        }
    }).then(res => {
        console.log(res)
    }).catch(err=>{
        console.error(err)
    })
    

    返回数据,所需要的数据在data中,所以需要获取data里的数据。

5.8.电影App案例

5.8.1.选项卡案例

  1. 定义三个子组件Film、Center、Cinema

    下面以Film组件为例

    import React, {Component} from "react";
    
    export default class Film extends Component {
        render() {
            return (
                <div>
                    Film组件
                </div>
            )
        }
    }
    
  2. 在主页面引入并完成开发

    import React, {Component} from "react";
    import Film from "./maizuo/Film";
    import Cinema from "./maizuo/Cinema";
    import Center from "./maizuo/Center";
    
    import "./css/maizuo.css"
    
    export default class App extends Component {
        state = {
            //1.定义点击时进行来回切换的选项卡的列表list,标注名词和ID(设置key值时需要)
            list: [
                {
                    id: 1,
                    text: "电影",
                    component: <Film></Film>
                },
                {
                    id: 2,
                    text: "影院",
                    component: <Cinema></Cinema>
                },
                {
                    id: 3,
                    text: "我的",
                    component: <Center></Center>
                },
            ],
            current: 0
        }
    
        //4.用户点击执行hanleClick函数,执行setState函数,改变state。render()函数马上就会重新渲染,就会执行{}中的whichCinema,设置指定的组件
        whichCinema() {
            // switch (this.state.current) {
            //     case 0:
            //         return <Film></Film>
            //     case 1:
            //         return <Cinema></Cinema>
            //     case 2:
            //         return <Center></Center>
            //     default:
            //         return null
            // }
            return this.state.list[this.state.current].component
        }
    
        render() {
            return (
                <div>
                    {
                        this.whichCinema()
                    }
                    <ul>
                        {
                            // 2.遍历list,将其内容渲染成标签,即选项卡的neir。1.设置handleClick函数,点击时触发;2.设置className设置标签的class,当点击时触发,更改选项卡的标签。3.设置key值
                            this.state.list.map((item, index) =>
                                <li onClick={() => this.handleClick(index)}
                                    className={this.state.current === index ? 'active' : ''}
                                    key={item.id}>{item.text}
                                </li>
                            )
                        }
                    </ul>
                </div>
            )
        }
    
        //3.点击选项卡,切换到不同的选项卡,通过设置current,current指向被触发选项卡的id。
        handleClick = (index) => {
            this.setState({
                current: index
            })
        }
    }
    
  3. 设置基本样式

    * {
        margin: 0;
        padding: 0;
    }
    
    ul {
        list-style: none;
        display: flex;
        position: fixed;
        bottom: 0;
        left: 0;
        height: 50px;
        line-height: 50px;
        width: 100%;
    }
    
    ul li {
        flex: 1;
        text-align: center;
    }
    
    .active {
        color: red;
    }
    
  4. 最终效果

5.8.2.获取电影院数据并渲染

import React, {Component} from "react";
//1.先引入axios库
import axios from "axios";

export default class Cinema extends Component {
    //2.在constructor中定义所需数据
    constructor() {
        super();
        //2.1.定义state用于存储所需数据
        this.state = {
            //2.1.1.保存每次过滤后的数据
            cinemaList: [],
            //2.1.2.保存从后台获取的原始数据,每次筛选都会里面筛选
            backCinemaList: [],
        }


        //2.2.通过axios获取后端数据
        axios({
            // 2.2.1.设置url
            url: "https://m.maizuo.com/gateway?cityId=440300&ticketFlag=0&k=6939037",
            // 2.2.2.设置请求方式
            method: "get",
            // 2.2.3.设置请求头
            headers: {
                'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.2.1","e":"16821403713477519055454209"}',
                'X-Host': 'mall.film-ticket.cinema.list'
            }
        }).then(res => {
            //2.2.4.成功获取数据,对数据进行保存
            console.log(res.data)
            this.setState({
                cinemaList: res.data.data.cinemas,
                backCinemaList: res.data.data.cinemas
            }, () => {
                //2.2.5.保存好后对State数据进行查看,因为setState是异步的
                console.log(this.state.cinemaList)
            })
        }).catch(err => {
            //2.2.6.如果有异常打印错误信息
            console.error(err)
        })
    }


    render() {
        return (
            <div>
                {/*3.设置输入框,当用户输入数据时触发handleInput函数*/}
                <input type="text" onInput={this.handleInput}/>
                {
                    // 4.渲染cinemaList数据,设置key,显示name和address
                    this.state.cinemaList.map(item =>
                        <dl key={item.cinemaId}>
                            <dt>
                                {item.name}
                            </dt>
                            <dt>
                                {item.address}
                            </dt>
                        </dl>
                    )
                }
            </div>
        )
    }

    /**
     * 5.用户输入时触发该函数,根据用户输入信息,使用filter和includes函数过滤符合要求的数据保存到cinemaList,然后react自行渲染
     * @param event
     */
    handleInput = (event) => {
        var newArr = this.state.backCinemaList.filter(item => item.name.toUpperCase().includes(event.target.value.toUpperCase()) || item.address.toUpperCase().includes(event.target.value.toUpperCase()))
        this.setState({
            cinemaList: newArr
        })
    }
}

最终效果:

5.9.better-scroll和setState的异步结合案例

5.9.1.better-scroll基础案例

import React, {Component} from "react";
//1.引入better-scroll组件
import BetterScroll from 'better-scroll'

export default class App extends Component {
    state = {
        list: []
    }

    render() {
        return (
            <div>
                <button onClick={() => this.getData()}>click</button>
                {/*2.按照better-scroll要求定义div将ul标签包裹起来,便于后续操作*/}
                <div className='stonebridgewrapper' style={{height: '200px', background: 'yellow', overflow: 'hidden'}}>
                    <ul className='stonebridgecontent'>
                        {
                            this.state.list.map(item =>
                                <li key={item}>{item}</li>
                            )
                        }
                    </ul>
                </div>
            </div>
        )
    }

    getData() {
        var list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
        this.setState({
                list: list
            }, () => {
            //3.当数据渲染成功后创建BetterScroll对象,指向上面的div
                // console.log(this.state.list)
                // new BetterScroll(".stonebridgewrapper")
            }
        )
        setTimeout(() => {
            new BetterScroll(".stonebridgewrapper")
        }, 0)
    }
}

注意:创建BetterScroll对象时,必须要等页面渲染完成,所以像在setState异步方法中,一定要在setState方法执行完成后执行new BetterScroll(“.stonebridgewrapper”)或者setTimeout()方法执行

5.9.2.better-scroll优化影院数据渲染

import React, {Component} from "react";
import axios from "axios";
//1.引入better-scroll组件
import BetterScroll from "better-scroll";

export default class Cinema extends Component {
    constructor() {
        super();
        this.state = {
            cinemaList: [],
            backCinemaList: [],
        }

        axios({
            url: "https://m.maizuo.com/gateway?cityId=440300&ticketFlag=0&k=6939037",
            method: "get",
            headers: {
                'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.2.1","e":"16821403713477519055454209"}',
                'X-Host': 'mall.film-ticket.cinema.list'
            }
        }).then(res => {
            console.log(res.data)
            this.setState({
                cinemaList: res.data.data.cinemas,
                backCinemaList: res.data.data.cinemas
            }, () => {
                //3.当数据渲染成功后创建BetterScroll对象,指向上面的div
                new BetterScroll(".stonebridgewrapper")
            })
        }).catch(err => {
            console.error(err)
        })
    }


    render() {
        return (
            <div>
                <input type="text" onInput={this.handleInput}/>
                {/*2.按照better-scroll要求定义div将ul标签包裹起来,便于后续操作*/}
                <div className='stonebridgewrapper' style={{height: '400px', background: 'yellow', overflow: 'hidden'}}>
                    <div className='stonebridgecontent'>
                        {
                            this.state.cinemaList.map(item =>
                                <dl key={item.cinemaId}>
                                    <dt>{item.name}</dt>
                                    <dt>{item.address}</dt>
                                </dl>
                            )
                        }
                    </div>
                </div>
            </div>
        )
    }

    handleInput = (event) => {
        var newArr = this.state.backCinemaList.filter(item => item.name.toUpperCase().includes(event.target.value.toUpperCase()) || item.address.toUpperCase().includes(event.target.value.toUpperCase()))
        this.setState({
            cinemaList: newArr
        }, () => {
            //4.输入框输入筛选字符串完成筛选,当筛选后的数据设置进state,后创建BetterScroll对象,所以一定要在html渲染完成后实现
            new BetterScroll(".stonebridgewrapper")
            console.log(this.state.cinemaList)
        })
    }
}

最终效果:

6.组件的数据挂载方式-属性(props)

状态(state)是每个组件内部产生,内部维护,内部组件自己访问。其他组件不能进行操作使用。如果考虑到组件的复用性,只用状态(state)挂载数据已经不够用。而且组件之间相互通信无法使用状态(state)完成,所以需要引用属性(props)。

6.1.属性(props)

props 是正常是外部传入的,组件内部也可以通过一些方式来初始化的设置,属性不能被组件自己更改,但是你可以通过父组件主动重新渲染的方式来传入新的 props

属性是描述性质、特点的,组件自己不能随意更改。

之前的组件代码里面有props 的简单使用,总的来说,在使用一个组件的时候,可以把参数放在标签的属性当中,所有的属性都会作为组件 props 对象的键值。通过箭头函数创建的组件,需要通过函数的参数来接收props。

  1. 在组件上通过key=value 写属性,通过this.props获取属性,这样组件的可复用性提高了。

    1. 父页面,通过title设置属性

      import React, {Component} from "react";
      import Navbar from "./Navbar/Navbar";
      
      export default class Props extends Component {
          render() {
              return (
                  <div>
                      <div>
                          <h2>首页</h2>
                          <Navbar title='首页'></Navbar>
                      </div>
      
                      <div>
                          <h2>列表</h2>
                          <Navbar title='列表'></Navbar>
                      </div>
                  </div>
              )
          }
      }
      
    2. 组件内部通过this.props接收属性

      export default class Navbar extends Component {
          //属性是父组件传递过来,通过this.props接收
          render() {
              console.log(this.props)
              let {title} = this.props
              return (
                  <div>
                      navbar--{title}
                  </div>
              )
          }
      }
      
    3. 结果

  2. 注意在传参数时候,如果写成isShow=“true” 那么这是一个字符串如果写成isShow={true} 这个是布尔值

    如果要想变量,表达式,数字,布尔等内容传递到组件中,就可以使用大括号传递

    export default class Props extends Component {
        render() {
            return (
                <div>
                    <div>
                        <h2>首页</h2>
                        <Navbar title='首页' leftshow={false} rightShow={true}></Navbar>
                    </div>
                </div>
            )
        }
    }
    
    import React, {Component} from "react";
    
    export default class Navbar extends Component {
        //属性是父组件传递过来,通过this.props接收
        render() {
            console.log(this.props)
            //接收leftShow, rightShow属性,且均为布尔类型
            let {title, leftShow, rightShow} = this.props
            return (
                <div>
                    {leftShow && <button>返回</button>}
                    navbar--{title}
                    {rightShow && <button>home</button>}
                </div>
            )
        }
    }
    

6.2.prop-types 属性验证

在React 18 中,已经将 PropTypes 从 React 主模块中移除,变成了一个单独的 prop-types 库,需要单独安装和导入使用。

6.2.1.实现prop-types 属性验证

  1. 安装prop-types

    npm install prop-types
    
  2. 在组件内设置类属性对父组件传递来的参数进行校验

    import React, {Component} from "react";
    //1.引入prop-types
    import PropTypes from 'prop-types'
    
    export default class Navbar extends Component {
        //属性是父组件传递过来,通过this.props接收
        render() {
            console.log(this.props)
            let {title, leftShow, rightShow} = this.props
            return (
                <div>
                    {leftShow && <button>返回</button>}
                    navbar--{title}
                    {rightShow && <button>home</button>}
                </div>
            )
        }
    }
    
    //2.设置类属性进行校验(类属性和对象属性相对)
    Navbar.propTypes = {
        title: PropTypes.bool,
        leftShow: PropTypes.string,
        rightShow: PropTypes.string
    }
    
  3. 当传递的属性不符合要求时显示结果

6.2.2.static propTypes、static defaultProps、属性简写

  1. 将组件的校验条件写在组件内部

    因为校验条件是类属性,而不是对象属性,所以使用类似java的static关键字在组件内部定义。

  2. 为组件的属性设置默认值,即当父组件不传值时使用默认组件,用static defaultProps定义

    import React, {Component} from "react";
    import PropTypes from 'prop-types'
    
    export default class Navbar extends Component {
        //1.将组件的校验条件写在组件内部
        static propTypes = {
            title: PropTypes.bool,
            leftShow: PropTypes.bool,
            rightShow: PropTypes.string
        }
        //2.为组件的属性设置默认值,即当父组件不传值时使用默认组件
        static defaultProps = {
            title: "默认的myname",
            leftShow: true
        }
    
       
        render() {
            console.log(this.props)
            let {title, leftShow, rightShow} = this.props
            return (
                <div>
                    {leftShow && <button>返回</button>}
                    navbar--{title}
                    {rightShow && <button>home</button>}
                </div>
            )
        }
    }
    
  3. 如果给子组件传递多个属性可以采用对象全部传递

    1. 父组件传递属性对象

      import React, {Component} from "react";
      import Navbar from "./Navbar/Navbar";
      
      export default class Props extends Component {
          render() {
              //1.封装要传递给子组件的全部属性到一个对象。
              var propsSet = {
                  'title': '首页',
                  'leftshow': false,
                  'rightShow': true
              }
      
              return (
                  <div>
                      <div>
                          <h2>首页</h2>
                          {/* 2.将定义好的属性对象传递给组件*/}
                          <Navbar title='首页' {...propsSet}></Navbar>
                      </div>
                  </div>
              )
          }
      }
      
    2. 子组件接收属性对象

      import React, {Component} from "react";
      import PropTypes from 'prop-types'
      
      export default class Navbar extends Component {
          // 3.定义属性的校验规则
          static propTypes = {
              title: PropTypes.string,
              leftShow: PropTypes.bool,
              rightShow: PropTypes.bool
          }
          // 4.定义属性的默认值
          static defaultProps = {
              title: "默认的myname",
              isShow: true
          }
      
          //5.解析属性,属性是父组件传递过来,通过this.props接收
          render() {
              console.log(this.props)
              let {title, leftShow, rightShow} = this.props
              return (
                  <div>
                      {leftShow && <button>返回</button>}
                      navbar--{title}
                      {rightShow && <button>home</button>}
                  </div>
              )
          }
      }
      
    3. 最终效果

6.3.函数式组件参数传递

  1. 父组件中调用子组件

    import React, {Component} from 'react';
    import Sidebar from "./Sidebar/Sidebar";
    
    export default class App extends Component {
        render() {
            return (
                <div>
                    {/*1.引用函数式组件,并给子组件设置属性*/}
                    <Sidebar bg="yellow" position="left"></Sidebar>
                </div>
            );
        }
    }
    
  2. 在子组件中接收参数并使用

    import React from "react";
    //1.引入prop-types进行属性校验
    import PropTypes from 'prop-types'
    
    /**
     * 定义一个函数式组件
     * @param props :2.通过形参接收来自父组件的参数
     * @returns {JSX.Element}
     * @constructor
     */
    export default function Sidebar(props) {
        console.log(props)
        //3.解析参数
        let {bg, position} = props
    
        var obj1 = {
            left: 0
        }
        var obj2 = {
            right: 0
        }
        var obj = {
            background: bg,
            width: "200px",
            position: "fixed"
        }
        //4.根据需求合并对象,并使用
        var styleObj = position === 'left' ? {...obj, ...obj1} : {...obj, ...obj2}
        console.log(styleObj)
        return (
            <div style={styleObj}>
                <ul>
                    <li>1</li>
                    <li>1</li>
                    <li>1</li>
                    <li>1</li>
                    <li>1</li>
                    <li>1</li>
                    <li>1</li>
                </ul>
            </div>
        )
    }
    // 5.定义属性的校验要求
    Sidebar.propTypes = {
        bg: PropTypes.string,
        position: PropTypes.string,
    }
    // 6.设置属性的默认值
    Sidebar.defaultProps = {
        bg: 'blue',
        position: 'right',
    }
    
  3. 实现效果

6.4.属性vs状态

相似点:都是纯js对象,都会触发render更新,都具有确定性(状态/属性相同,结果相同)

不同点:

  1. 属性能从父组件获取,状态不能
  2. 属性可以由父组件修改,状态不能
  3. 属性能在内部设置默认值,状态也可以,设置方式不一样
  4. 属性不在组件内部修改(父组件修改),状态要在组件内部修改
  5. 属性能设置子组件初始值,状态不可以
  6. 属性可以修改子组件的值,状态不可以

state的主要作用是用于组件保存、控制、修改自己的可变状态。state 在组件内部初始化,可以被组件自身修改,而外部不能访问也不能修改。你可以认为 state 是一个局部的、只能被组件自身控制的数据源。state 中状态可以通过 this.setState 方法进行更新, setState 会导致组件的重新渲染。

props 的主要作用是让使用该组件的父组件可以传入参数来配置该组件。它是外部传进来的配置参数,组件内部无法控制也无法修改。除非外部组件主动传入新的 props ,否则组件的 props 永远保持不变。

没有 state 的组件叫无状态组件(stateless component),设置了 state 的叫做有状态组件(stateful component)。因为状态会带来管理的复杂性,我们尽量多地写无状态组件,尽量少地写有状态的组件。这样会降低代码维护的难度,尽量由上层组件控制子组件。也会在一定程度上增强组件的可复用性。

7.表单中的受控组件与非受控组件

7.1.非受控组件

React要编写一个非受控组件,可以使用ref来从DOM节点中获取表单数据,就是非受控组件。

例如,下面的代码使用非受控组件接受一个表单的值:

import React, {Component} from 'react';

export default class App extends Component {
    myusername = React.createRef()

    render() {
        return (
            <div>
                <h1>登录</h1>
                <input type="text" ref={this.myusername} value="stonebridge"/>
                <button onClick={() => {
                    console.log(this.myusername.current.value)
                }}>登录
                </button>
                <button onClick={() => {
                    this.myusername.current.value = ''
                }}>重置
                </button>
            </div>
        );
    }
}

此时要求在input输入框设置一个默认值,惯性思维设置value=“stonebridge”。但是在react中,这样设置会导致后续无法修改。这是非受控的组件行为(通过ref去访问节点,去设置节点的内容。这种方案就是非受控的)。

可以理解为:在react的JSX文件中的input标签并不是真正的dom中的原生dom节点中的input标签,而是react封装的子组件。此时设置value=“stonebridge”,并不是传统的dom节点中的input标签的value属性。而是给react中的input子组件传递了这么一个属性,如果此时通过value="stonebridge"固定值后,所以react中的input子组件不能随便改变,必须完全收到父组件的控制,此时就是一种完全受控的状态。

此时如果通过defaultValue设置默认值,此时就脱离父组件控制(这种方式通过拿到原生dom节点,拿到输入框的值,设置输入框的值,因为跟react的状态没有直接联系,此时就是非受控方案。)

defaultValue :非受控的时候使用

value:受控的时候使用

<input type="text" ref={this.myusername} defaultValue="stonebridge"/>

在React 渲染生命周期时,表单元素上的 value 将会覆盖 DOM 节点中的值,在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个defaultValue 属性,而不是value。同样, <input type="checkbox"><input type="radio"> 支持 defaultChecked , <select><textarea> 支持 defaultValue 。

因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。

非受控组件在将数据传递到其他组件的时候不方便

例如:

render() {
        return (
            <div>
                <h1>登录</h1>
                <input type="text" ref={this.myusername} defaultValue="stonebridge"/>
                <Child myvalue ={this.myusername.current.value}></Child>
            </div>
        );
    }

此时如果将input输入框的值传递给子组件Child的属性,当input输入框的值改变时并不会执行render()函数(执行render()函数更新的条件是调用setState函数,state发生改变时)。此时设置在input输入框的值改变时,同步给状态,这样的就太麻烦,可以直接采用受控组件,即state跟输入框绑定,不使用ref

7.2.受控组件

由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value ,这使得 React的 state 成为唯一数据源。由于handlechange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新。

对于受控组件来说,输入的值始终由 React 的state 驱动。你也可以将 value 传递给其他UI 元素,或者通过其他事件处理函数重置,但这意味着你需要编写更多的代码。

import React, {Component} from 'react';
import Navbar from "./Navbar/Navbar";

export default class App extends Component {
    //1.定义状态username,在表单元素的value和该状态绑定
    state = {
        username: 'stonebridge'
    }

    render() {
        return (
            <div>
                <h1>登录</h1>
                {/*2.在表单元素上设置了value属性和状态里的值绑定。当已经提供了value属性,如果不设置onChang处理函数,他就只会渲染成只读*/}
                <input type="text" value={this.state.username} onChange={(event) => {
                    //3.设置onChange函数,当修改表单时,完成状态改变。
                    this.setState({
                        username: event.target.value
                    })
                }}/>
                {/*4.通过状态轻松获取表单输入的值*/}
                <button onClick={() => {
                    console.log(this.state.username)
                }}>登录
                </button>
                {/*5.通过setState函数完成设置或者清空*/}
                <button onClick={() => {
                    this.setState({
                        username: ''
                    })
                }}>重置
                </button>
                {/*6.当状态改变时,会触发render()函数,完成对子组件的重新渲染,新的值也会传递过去*/}
                <Navbar username={this.state.username}></Navbar>
            </div>
        );
    }
}

注意: 另一种说法(广义范围的说法),React组件的数据渲染是否被调用者传递的props 完全控制,控制则
为受控组件,否则非受控组件。

7.3.受控组件的案例

7.3.1.受控组件的影院查询

import React, {Component} from "react";
import axios from "axios";

export default class Cinema extends Component {
    constructor(props) {
        super(props);
        this.state = {
            cinemaList: [],
            myText: '',
        }

        axios({
            url: "https://m.maizuo.com/gateway?cityId=440300&ticketFlag=0&k=6939037",
            method: "get",
            headers: {
                'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.2.1","e":"16821403713477519055454209"}',
                'X-Host': 'mall.film-ticket.cinema.list'
            }
        }).then(res => {
            console.log(res.data)
            this.setState({
                cinemaList: res.data.data.cinemas,
            })
        })
    }


    render() {
        return (
            <div>
                {/*1.当用户在输入框输入值,发生改变触发onChange,通过setState改变setState,改变了myText。导致render函数重新执行。*/}
                {/*2.render函数重新渲染导致 2.1.页面输入框的值发生了改变,2.2.触发了getCinemaList*/}
                {/*3.getCinemaList执行就会筛选出符合条件的值,将筛选的值返回并完成渲染*/}
                <input type="text" value={this.state.myText} onChange={(evt) => {
                    this.setState({
                        myText: evt.target.value
                    })
                }}/>
                {
                    this.getCinemaList().map(item =>
                        <dl key={item.cinemaId}>
                            <dt>{item.name}</dt>
                            <dt>{item.address}</dt>
                        </dl>
                    )
                }
            </div>
        )
    }

    getCinemaList() {
        return this.state.cinemaList.filter(item => item.name.toUpperCase().includes(this.state.myText.toUpperCase()) || item.address.toUpperCase().includes(this.state.myText.toUpperCase()))
    }
}

7.3.2.受控组件的todolist

import React, {Component} from "react";
import './css/01-index.css'

export default class App extends Component {
    state = {
        list: [{
            id: 1,
            myText: "aaaa",
            isChecked: true
        }, {
            id: 2,
            myText: "bbb",
            isChecked: false
        }, {
            id: 3,
            myText: "ccc",
            isChecked: false
        },],
        myText: ''
    }

    render() {
        return (
            <div>
                {/*1.在state中设置myText,用于和input输入框的值绑定;当用户在输入框输入值,发生改变触发onChange*/}
                {/*通过setState改变setState,改变了myText。导致render函数重新执行。*/}
                <input type='text' value={this.state.myText} onChange={(evt) => {
                    this.setState({
                        myText: evt.target.value
                    })
                }}/>
                <button onClick={this.handleClick}>add</button>
                <ul>
                    {
                        this.state.list.map((item, index) =>
                            <li key={item.id}>
                                {/*2.render函数重新渲染导致checkbox选择框,也会因为state改变重新渲染,当改变单选框改变时触发handChecked函数*/}
                                <input type='checkbox' checked={item.isChecked} onChange={() => {
                                    this.handChecked(index)
                                }}/>
                                <span dangerouslySetInnerHTML={
                                    {
                                        __html: item.myText
                                    }
                                } style={{textDecoration: item.isChecked ? 'line-through' : ''}}></span>
                                <button onClick={() => this.delClick(index)}>delete</button>
                            </li>
                        )
                    }
                </ul>
                {this.state.list.length === 0 &&
                    <div>暂无待办事件</div>
                }
                <div className={this.state.list.length === 0 ? '' : 'hidden'}>暂无待办事件</div>
            </div>
        )
    }

    handChecked = (index) => {
        //3.根据传递来的index找到this.state.list里的对象的isChecked属性,
        // 取反,通过setState设置state,render()重新完成渲染
        var newList = [...this.state.list]
        newList[index].isChecked = !newList[index].isChecked
        this.setState({
            list: newList
        })
    }


    handleClick = () => {
        let newList = [...this.state.list]
        newList.push({
            id: Math.random() * 100000,
            myText: this.state.myText
        })

        this.setState({
            list: newList,
            myText: ''
        })
    }

    delClick = (index) => {
        let newList = this.state.list.slice()
        newList.splice(index, 1)
        this.setState({
            list: newList
        })
    }
}

8.组件通信的方式

8.1.父子组件通信方式

8.1.1.传递数据(父传子)与传递方法(子传父)

设计组件时尽量使用来自父组件的属性来控制组件,设置成无状态组件,完全受父组件控制。

8.1.1.1.回调函数

父组件可以将一个函数作为props传递给子组件,子组件可以在合适的时候调用该函数并将数据传递回父组件。

  1. 父组件

    import React, {Component} from "react";
    import Navbar from "./component/Navbar";
    
    export default class App extends Component {
        state = {
            isShow: false
        }
        // 1.在父组件中定义函数handEvent,用于子组件触发调用
        handEvent = (number, username) => {
            this.setState({
                isShow: !this.state.isShow
            }, () => {
                console.log("isShow:" + this.state.isShow)
                console.log("number:" + number, "username:" + username)
            })
        }
    
        render() {
            return (
                <div>
                    {/*2.父组件将函数作为props传递给子组件*/}
                    <Navbar event={this.handEvent}></Navbar>
                </div>
            )
        }
    }
    
  2. 子组件Navbar.js

    import React, {Component} from 'react';
    
    export default class Navbar extends Component {
        render() {
            return (
                <div style={{background: "red"}}>
    
                    <button onClick={() => {
                        {
                            /*3.子组件可以在合适的时候调用该函数并将数据传递回父组件。*/
                        }
                        this.props.event(1111, "stonebridge")
                    }}>click
                    </button>
                    <span>navbar</span>
                </div>
            );
        }
    }
    
  3. 测试结果:

8.1.1.2.为什么不能使用非受控组件(有状态组件)

如果使用非受控组件,当父组件的状态改变时,需要将改变的信息传递给子组件,子组件需要进行接收,然后将接收的信息传递给子组件的state。如果在state里接收,只会在创建该子组件的时候创建state对象的接收成功,后面再次传递则无法接收;如果在render函数使用setState()函数接收就会导致state数据更新,然后render函数触发,再执行setState()方法,陷入死循环。

8.1.1.3.react中子组件为什么要设计成受控组件(无状态组件)

react中子组件为什么要设计成无状态组件?

React中有状态组件和无状态组件两种,通常建议尽可能将子组件设计为无状态组件,也就是使用函数组件。因为无状态组件不需要维护自己的状态,代码量少、执行效率高,可以更好地提升应用程序的性能。

无状态组件只需要负责渲染接收到的props,不需要自己维护状态,因此不会像有状态组件一样容易出现问题。此外,由于无状态组件不会执行生命周期函数,因此它们的渲染速度更快。

当然,如果子组件需要维护自己的状态,那么就需要使用有状态组件了。此时,建议使用ES6的class组件,可以方便地继承React.Component,并使用它提供的生命周期函数来处理组件的状态变化。

8.1.1.4.受控组件(无状态组件)的应用实例

注意点:

受控的子组件是无法改父组件传来的值(props)的,但是可以通过通知父组件来完成修改,然后再传递到子组件。

将非受控类组件改为函数组件

// 1.定义函数组件Tabbar,函数组件没有this的概念,直接使用参数props
const Tabbar = (props) => {
    //2.在函数组件中创建handleClick函数,用于回调父组件的事件,也可以直接调用props.event(index)
    function handleClick(index) {
        props.event(index)
    }

    return <div>
        <ul>
            {
                //3.根据父组件传递来的数据进行渲染导航栏,通过props.list即可获取数据
                props.list.map((item, index) =>
                    //4.根据点击导航栏按钮触发handleClick函数
                    <li onClick={() => handleClick(index)}
                        className={props.current === index ? 'active' : ''}
                        key={item.id}>{item.text}</li>
                )
            }
        </ul>
    </div>
}
export default Tabbar

8.1.2.ref标记

ref标记(父组件拿到子组件的引用,从而调用子组件的方法)

在父组件中清除子组件的input输入框的value值。this.refs.form.reset()

在 React 中,ref 是用于获取对 DOM 元素或组件实例的引用的属性。通常情况下,在 React 中不建议直接操作 DOM 元素,但是在某些情况下,我们需要访问 DOM 元素的引用,这时就可以使用 ref 属性。

ref 属性推荐的方式来使用:

使用 React.createRef() 方法创建 ref 对象:从 React 16.3 版本开始,React 推荐使用 createRef() 方法来创建 ref 对象,它是一种更加强类型的方式,可以通过 ref.current 来访问 DOM 元素的引用。

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }

  componentDidMount() {
    this.myRef.current.focus();
  }

  render() {
    return <input type="text" ref={this.myRef} />;
  }
}

使用 ref 的场景:

  1. 处理表单元素的值:可以使用 ref 来获取表单元素的值,例如文本框的输入值、复选框和单选框的选中状态等。
  2. 控制动画:可以使用 ref 来获取 DOM 元素的位置信息,从而实现控制动画的效果。
  3. 第三方库集成:有些第三方库需要操作 DOM 元素,可以使用 ref 来获取 DOM 元素的引用,并将其传递给第三方库。
8.1.2.1.使用受控组件(无状态组件)的完成表单域组件定义及使用
import React, {Component} from 'react';

/**
 * 1.自定义Filed组件,主要包含输入框的label元素和input输入框元素
 */
class Filed extends Component {
    render() {
        return (
            <div style={{background: 'yellow'}}>
                {/*2.根据父组件传递的属性里的label、type、value设置标签名称、input类型、默认值*/}
                {/*  当输入值时,触发onChange函数,将值传递到父组件*/}
                <label>{this.props.label}</label>
                <input type={this.props.type} onChange={(evt) => {this.props.onChangeEvent(evt.target.value)}} value={this.props.value}/>
            </div>
        )
    }
}


export default class App extends Component {
    // 3.设置默认state状态,username的值默认从本地拿取
    state = {
        username: localStorage.getItem('username') + '',
        password: ''
    }

    render() {
        return (
            <div>
                <h1>登录页面</h1>
                {/*4.使用子组件Filed,将参数username默认值、label、type传递给子组件。
                 并设置回调函数,子组件onChange函数触发,将子组件的数据更新到state中*/}
                <Filed label='用户名' type='text' onChangeEvent={
                    (value) => {
                        this.setState({
                            username: value
                        })
                    }
                } value={this.state.username}></Filed>

                <Filed label='密码' type='password' onChangeEvent={
                    (value) => {
                        this.setState({
                            password: value
                        })
                    }
                } value={this.state.password}></Filed>

                {/*5.点击登录按钮,通过state就可以获取最新的username和password*/}
                <button onClick={() => {
                    console.log(this.state)
                }}>登录
                </button>

                {/*6.点击重置按钮,将state数据清空即可*/}
                <button onClick={() => {
                    this.setState({
                        username: '',
                        password: ''
                    })
                }}>重置
                </button>
            </div>
        );
    }
}

以上可以看出,使用受控组件完成处理表单元素的值没有问题,但是在数据在父组件与子组件传递过程中,特别麻烦,故可以采用ref标记的方式完成处理表单元素的值。

8.1.2.2.使用ref引用完成处理表达元素的值
import React, {Component} from 'react';


/**
 * 1.自定义Filed组件,主要包含输入框的label元素和input输入框元素
 */
class Filed extends Component {
    //2.定义state用于存储用户输入的值
    state = {
        value: ''
    }

    //3.便于父组件通过ref清空Filed的state
    clear() {
        this.setState({
            value: ''
        })
    }

    //4.便于父组件通过ref设置Filed的state
    setValue(value) {
        this.setState(value)
    }

    render() {
        return (
            <div style={{background: 'yellow'}}>
                {/*5.获取来自父组件的props,获取label、type;
                当用户输入数据时将用户输入的值设置进入state
                通过value设置默认值,也是便于通过clear()和setValue(value)修改*/}
                <label>{this.props.label}</label>
                <input type={this.props.type} onChange={(evt) => {
                    this.setState({
                        value: evt.target.value
                    })
                }} value={this.state.value}/>
            </div>
        )
    }
}


export default class App extends Component {
    // 6.定义ref属性
    username = React.createRef();
    password = React.createRef();

    render() {
        return (
            <div>
                <h1>登录页面</h1>
                {/*//7.设置ref属性*/}
                <Filed label='用户名' type='text' ref={this.username}></Filed>
                <Filed label='密码' type='password' ref={this.password}></Filed>

                <button onClick={() => {
                    // 8.通过this.username.current.state.value可以获取子组件的值
                    console.log("username:" + this.username.current.state.value)
                    console.log("username:" + this.password.current.state.value)
                }}>登录
                </button>

                <button onClick={() => {
                    // 9.通过this.username.current.clear()调用子组件的子组件的clear()方法
                    this.username.current.clear()
                    this.password.current.clear()
                }}>重置
                </button>
            </div>
        );
    }
}

8.2.非父子组件通信方式

8.2.1.状态提升(中间人模式)

React中的状态提升概括来说,就是将多个组件需要共享的状态提升到它们最近的父组件上.在父组件上改变这个状态然后通过props分发给子组件.

实现效果:

8.2.2.发布订阅模式实现

8.2.2.1.发布订阅模式原理

注意点:publish触发一定要在subscribe调用之后,只有完成订阅后再发布才可以。

8.2.2.2. 订阅发布模式案例
import React, {Component} from 'react'
import axios from 'axios'
import './css/03-communination.css'


//调度中心
var bus = {

    list: [],
    //订阅
    subscribe(callback) {
        this.list.push(callback)
    },

    //发布
    publish(text) {
        //遍历所有的list, 将回调函数执行
        this.list.forEach(callback => {
            callback && callback(text)
        })
    }
}


export default class App extends Component {
    constructor() {
        super()
        this.state = {
            filmList: [],
        }
        axios.get(`/test.json`).then(res => {
            console.log(res.data.data.films)
            this.setState({
                filmList: res.data.data.films
            })
        })
    }

    render() {
        return (
            <div>
                {
                    this.state.filmList.map(item =>
                        <FilmItem key={item.filmId} {...item} ></FilmItem>
                    )
                }
                <FilmDetail></FilmDetail>
            </div>
        )
    }
}

/*受控组件*/
class FilmItem extends Component {
    render() {
        let {name, poster, grade, synopsis} = this.props
        return <div className="filmitem" onClick={() => {
            /**
             * 2.FilmItem中当点击item触发publish()完成发布,将数据传递到调度中心,
             * 并触发调度者的回调函数,将参数传递给回调函数
             */
            bus.publish(synopsis)
        }}>
            <img src={poster} alt={name}/>
            <h4>{name}</h4>
            <div>观众评分:{grade}</div>
        </div>
    }
}

class FilmDetail extends Component {
    constructor() {
        super()
        this.state = {
            info: ""
        }
        /**
         * 1.在FilmDetail完成订阅,设置回调函数,当被发布者触发时,
         * 将接收的参数设置到state中并在页面中完成渲染。
         */
        bus.subscribe((info) => {
            console.log("我再filmDetail中定义", info)
            this.setState({
                info: info
            })
        })
    }

    render() {
        return <div className="filmdetail">
            {this.state.info}
        </div>
    }
}

8.3.context状态树传参

React中的Context状态树是一种全局状态管理方式,它允许在组件树中的任何地方共享数据,而不必手动地将props通过多层组件进行传递。它可以避免props层层传递的麻烦,并使组件之间的通信更加简洁和高效。

Context状态树包含两个主要部分:

  1. Provider: Provider是Context状态树中的顶层组件,用于提供要共享的数据。它通过value属性将数据传递给下面的组件。
  2. Consumer: Consumer是Context状态树中的下层组件,用于接收提供的数据并使用它。它可以通过静态属性contextType或使用Context.Consumer组件来访问Context状态树中的数据。

注意:GlobalContext.Consumer内必须是回调函数,通过context方法改变根组件状态

context优缺点:

优点:跨组件访问数据
缺点:react组件树种某个上级组件shouldComponetUpdate 返回false,当context更新时,不会引起下级组件更新

8.4.插槽

8.4.1.基础概念

在 React 中,插槽是一种组件化技术,用于传递组件的子元素,允许在组件内部嵌入其他组件。

React 中的插槽通常使用子组件的 children 属性来实现。通过 this.props.children 属性,可以在组件内部访问到插入的子元素。这个属性是一个 React 元素或者是一个 React 元素数组,可以在组件内部使用 map() 或者 React.Children 工具方法进行遍历和处理。

在使用插槽的时候,可以将任意类型的 React 元素(包括字符串、数字、组件等)作为子元素插入到组件中。例如:

function Card(props) {
  return (
    <div className="card">
      <h2>{props.title}</h2>
      {props.children}
    </div>
  );
}

function App() {
  return (
    <Card title="My Card">
      <p>This is my card's content.</p>
      <ul>
        <li>Item 1</li>
        <li>Item 2</li>
      </ul>
    </Card>
  );
}

在这个例子中,Card 组件会将 props.children 渲染到组件的内部。因此,<p><ul> 元素都会作为子元素被渲染到 Card 组件中,这样可以很方便地组合和重用组件。

或者

//在子组件Child预留插槽
class Child extends Component {
    render() {
        return (
            <div>
                child
                {/*插槽*/}
                {this.props.children[2]}
                {this.props.children[1]}
                {this.props.children[0]}
            </div>
        )
    }
}

//父组件中在子组件Child中插入希望的片段
export default class App extends Component {
    render() {
        return (
            <div>
                <Child>
                    <span>1111111</span>
                    <div>2222222</div>
                    <div>3333333</div>
                </Child>
            </div>
        );
    }
}

8.4.2.利用插槽减少父子通信示例

import React, {Component} from 'react'

class Navbar extends Component {
    render() {
        return <div style={{background: "red"}}>
            {this.props.children}
            <span>navbar</span>
        </div>
    }
}

class Sidebar extends Component {
    render() {
        return <div style={{background: "yellow", width: "200px"}}>
            <ul>
                <li>11111</li>
                <li>11111</li>
                <li>11111</li>
                <li>11111</li>
                <li>11111</li>
            </ul>
        </div>
    }
}

export default class App extends Component {
    state = {
        isShow: false
    }


    render() {
        return (
            <div>
                <Navbar>
                    <button onClick={() => {
                        //例如插槽在子组件内部插入方法,该方法可以访问父组件的state.isShow,子组件不需要和父组件进行交流
                        this.setState({
                            isShow: !this.state.isShow
                        })
                    }}>click
                    </button>
                </Navbar>

                {this.state.isShow && <Sidebar/>}
            </div>
        )
    }
}

9.生命周期

React组件在创建、更新和销毁时会触发一系列的生命周期函数,这些生命周期函数的调用顺序构成了组件的生命周期。React生命周期的目的是让开发者能够在组件的不同生命周期阶段中执行适当的操作,比如初始化组件的状态、更新DOM等等。

9.1.挂载阶段(Mounting)

生命周期是针对类而言的,对于函数式组件是只有属性这个概念,没有生命周期。

9.1.1.componentWillMount()

在组件挂载到DOM前调用,可以用来进行一些初始化操作。只会被调用一次。可以进行页面渲染器前(render()执行)的最后一次渲染状态的修改。

从 React 17 开始,该方法被标记为过时,建议使用 componentDidMount 替代它componentDidMount 方法同样只会被调用一次,但是它的调用时机是在组件挂载后,即DOM节点已经被渲染完成后。

9.1.2.render()

渲染组件的内容,在componentWillMount和componentDidMount出发,后面更新状态也会再次调用完成页面渲染。

不要在render中修改state

9.1.3.componentDidMount()

组件挂载到DOM上之后所执行的函数。具体来说,它是在组件渲染完成并将要被添加到页面中时执行的函数。只会被调用一次。

可以进行以下操作:

  1. 数据请求axios

  2. 订阅函数调用

  3. 启动时间监听(例如:setInterval)

  4. 基于创建的完的dom进行初始化(第三方JS库,例如:BetterScroll)

    当dom渲染结束后,如果通过document.querySelectorAll(“li”)就可以获取所有li标签,如果componentWillMount()调用只会得到一个空数组。

9.1.4.示例

export default class App extends Component {
    state = {
        myName: "stone bridge"
    }

    componentWillMount() {
        console.log("第一次will mount", this.state.myname, document.getElementById("myName"))
        this.setState({
            myName: "Kerwin"
        })
    }

    componentDidMount() {
        console.log("第一次did mount", document.getElementById("myName"))
    }
    
    render() {
        console.log("render")
        return (
            <div>
                <span id="myName">{this.state.myName}</span>
            </div>
        )
    }
}

9.2.更新阶段(Updating)

9.2.1.componentWillReceiveProps

最先获得父组件的属性的生命周期函数,必须在构建父子组件关系时才能使用,当父组件state更新时,无论是否给子组件传递属性,子组件都会被动触发componentWillReceiveProps函数,而state更新时不会触发componentWillReceiveProps函数

在旧版本的 React 中,componentWillReceiveProps 是一个生命周期函数,用于在组件接收到新的 props 之前进行处理。它会在组件接收到新的 props 时被调用,并传入新的 props 作为参数。

9.2.1.1.案例1.componentWillReceiveProps的触发
import React, {Component} from 'react';

class Child extends Component {

    /**
     * @param nextProps :最新的 props,老属性来自this.props
     * @param nextContext
     * 最先获得父组件的属性,可以利用属性进行逻辑处理或者ajax处理等
     */
    componentWillReceiveProps(nextProps, nextContext) {
        console.log("componentWillReceiveProps;最新的text:" + nextProps.text + ";原始的text:" + this.props.text)
        this.setState({
            title: "stonebridge" + nextProps.text
        }, () => {
            console.log(this.state.title);
        })
    }

    state = {
        title: "1111111"
    }

    render() {
        return <div>child</div>
    }
}

export default class App extends Component {
    state = {
        text: "1111111"
    }

    render() {
        return (
            <div>
                {this.state.text}
                <button onClick={() => {
                    this.setState({
                        text: "2222222222222"
                    })
                }}>Click</button>
                <Child text={this.state.text}></Child>
            </div>
        );
    }
}
9.2.2.2.案例2.componentWillReceiveProps应用

该案例中子组件初始化时,触发componentDidMount函数一次,后续的父组件更新触发子组件的componentWillReceiveProps函数。

如果用componentDidUpdate换掉componentWillReceiveProps就会出现页面渲染再次调用就会再次触发。

import React, {Component} from 'react'
import axios from 'axios'

export default class App extends Component {
    state = {
        type: 1
    }

    render() {
        return (
            <div>
                <ul>

                    <li onClick={() => {
                        this.setState({
                            type: 1
                        })
                    }}>正在热映
                    </li>

                    <li onClick={() => {
                        this.setState({
                            type: 2
                        })
                    }}>即将上映
                    </li>

                </ul>

                <FilmList type={this.state.type}></FilmList>
            </div>
        )
    }
}


class FilmList extends Component {
    state = {
        list: []
    }

    //子组件初始化时调用,且只会执行一次
    componentDidMount() {
        this.requestDate(this.props.type)
    }

    /**
     * 每次父组件修改state时触发(type改变)都会触发
     * @param nextProps
     * @constructor
     */
    UNSAFE_componentWillReceiveProps(nextProps) {
        this.requestDate(nextProps.type)
    }


    requestDate(type) {
        if (type === 1) {
            //请求卖座正在热映的数据
            console.log("请求卖座正在热映的数据")
            axios({
                url: "https://m.maizuo.com/gateway?cityId=110100&pageNum=1&pageSize=10&type=1&k=6369301",
                headers: {
                    'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.2.0","e":"16395416565231270166529","bc":"110100"}',
                    'X-Host': 'mall.film-ticket.film.list'
                }
            }).then(res => {
                console.log(res.data.data.films)
                this.setState({
                    list: res.data.data.films
                })
            })
        } else {
            //请求卖座即将上映的数据
            console.log("请求卖座即将上映的数据")
            axios({
                url: "https://m.maizuo.com/gateway?cityId=110100&pageNum=1&pageSize=10&type=2&k=8077848",
                headers: {
                    'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.2.0","e":"16395416565231270166529","bc":"110100"}',
                    'X-Host': 'mall.film-ticket.film.list'
                }
            }).then(res => {
                console.log(res.data.data.films)
                this.setState({
                    list: res.data.data.films
                })
            })
        }
    }


    render() {
        return <ul>
            {
                this.state.list.map(item =>
                    <li key={item.filmId}>{item.name}</li>
                )
            }
        </ul>
    }
}

9.2.2.shouldComponentUpdate

在组件即将重新渲染之前被调用。它接收两个参数:nextPropsnextState,用于访问组件即将更新的 props 和 state。你可以在该函数中根据新的 props 和 state 来判断是否需要进行组件的重新渲染。

shouldComponentUpdate 返回一个布尔值,用于指示是否应该更新组件。如果返回 true,则组件将继续重新渲染;如果返回 false,则组件将停止更新,即不会调用后续的生命周期函数(UNSAFE_componentWillUpdate,rendercomponentDidUpdate等)。通过减少无效的虚拟dom的对比,提高react的性能。

9.2.2.1.案例1

代码作用:有一个按钮,点击可以修改this.state.myName,为了防止用户重复点击,当用户第二次点击时,进行现在状态和之前状态对比,如果没有改变则shouldComponentUpdate函数返回false,则后续操作就会停止

import React, {Component} from 'react';

export default class App extends Component {
    state = {
        myName: "kerwin"
    }

    render() {
        console.log("render")
        return (
            <div>
                <button onClick={() => {
                    this.setState({
                        myName: "stonebridge"
                    })
                }}>click
                </button>

                {this.state.myName}
            </div>
        );
    }

    /**
     * 在这里进行判断,如果老状态不不同于新状态就更新。减少无效的虚拟dom的对比,提高react的性能。
     * @param nextProps :最新的 props,老属性来自this.props
     * @param nextState :最新的 state,老状态来自this.state
     * @param nextContext
     */
    shouldComponentUpdate(nextProps, nextState, nextContext) {
        if (JSON.stringify(this.state) !== JSON.stringify(nextState)) {
            return true;
        } else {
            return false;
        }
    }

    UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) {
        console.log("UNSAFE_componentWillUpdate")
    }

    componentDidUpdate(prevProps, prevState, snapshot) {
        console.log("componentDidUpdate")
    }
}

在shouldComponentUpdate中进行新旧state进行对比,如果state没有更新就不进行组件冲洗渲染。

9.2.2.2.案例2

当父组件中有多个子组件,当用户进行操作时,仅仅只需要重新渲染少量的组件,则可以通过shouldComponentUpdate函数判断受影响的组件,从而进行渲染。

import React, {Component} from 'react'

class Box extends Component {

    /**
     * @param nextProps :最新的 props,老属性来自this.props
     * @param nextState :最新的 state,老状态来自this.state
     * @param nextContext
     */
    shouldComponentUpdate(nextProps, nextState, nextContext) {
        /**
         * 如果:this.props.index === this.props.current 成立
         *      说明该组件的序号和原先被选择的组件序号相同或者首次渲染时,此时可能已经选别的组件了,因此需要后续diff比较和重新渲染。
         * 如果:this.props.index === nextProps.current 成立
         *      说明该组件的序号和新选择的组件序号相同,说明该组件被新选中,需要重新渲染。
         */
        if (this.props.index === this.props.current || this.props.index === nextProps.current) {
            console.log("在shouldComponentUpdate函数中的判断结果:true");
            return true
        } else {
            console.log("在shouldComponentUpdate函数中的判断结果:false");
            return false
        }
    }

    /**
     * 在render中进行this.props.index === this.props.current的判断;
     * 此时的this.props.current不是shouldComponentUpdate中的this.props.current,而是nextProps.current,即组件中props中真正的current
     * @returns {JSX.Element}
     */
    render() {
        return <div style={{
            width: "100px",
            height: "100px",
            border: this.props.index === this.props.current ? '1px solid red' : '1px solid gray',
            margin: "10px",
            float: 'left'
        }}>
        </div>
    }
}



export default class App extends Component {
    state = {
        list: ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09"],
        current: 0
    }

    render() {
        return (
            <div>
                <input type="number" onChange={(evt) => {
                    console.log(evt.target.value)
                    this.setState({
                        current: Number(evt.target.value)
                    })
                }} value={this.state.current}/>

                <div style={{overflow: "hidden"}}>
                    {
                        this.state.list.map((item, index) =>
                            <Box key={item} current={this.state.current} index={index}/>
                        )
                    }
                </div>
            </div>
        )
    }
}

9.2.3.componentWillUpdate

每次state数据改变就会触发一次,在组件即将更新时调用。

9.2.4.render

当组件的 props 或 state 发生变化时,React 会自动调用 render 方法来重新渲染组件,并更新相应的 DOM。

9.2.5.componentDidUpdate

它在组件更新(重新渲染)之后被调用。它在首次渲染后不会被触发,只有在组件的 props 或 state 发生变化,导致组件重新渲染时才会被调用。此时可以拿到更新后的dom。

componentDidUpdate 接收两个参数:prevPropsprevState,用于访问组件更新前的 props 和 state。通过比较前后的值,你可以执行一些特定的操作,例如根据变化的 props 执行网络请求、更新组件内部的状态等。

警惕调用后页面渲染再次调用就会再次触发,陷入死循环。例如在9.2.2.2.案例中替换掉componentWillReceiveProps就会出现该情况。

componentDidUpdate(prevProps, prevState, snapshot) {
    console.log("componentDidUpdate", document.getElementById("myname").innerHTML)
}

9.2.6.示例1

import React, {Component} from 'react';

export default class App extends Component {
    state = {
        myName: "Stone bridge"
    }

    render() {
        console.log("render")
        return (
            <div>
                <button onClick={() => { this.setState({ myName: "张三"}) }}>click</button>
                {this.state.myName}
                <span id="myname">{this.state.myName}</span>
            </div>
        );
    }

    componentWillUpdate() {
        console.log("componentWillUpdate", document.getElementById("myname").innerHTML)
    }

    componentDidUpdate(prevProps, prevState, snapshot) {
        console.log("componentDidUpdate", document.getElementById("myname").innerHTML)
    }

}

9.3.卸载阶段(Unmounting)

9.3.1.componentWillUnmount

componentWillUnmount 是 React 组件的一个生命周期函数,它在组件即将被从 DOM 中移除之前调用。在该函数中,你可以执行一些清理操作,例如取消定时器、取消订阅或销毁其他资源,以避免内存泄漏或无效的操作。

当组件被卸载时(例如通过调用 ReactDOM.unmountComponentAtNode() 或在组件从父组件中移除时),React 会自动调用 componentWillUnmount。在该函数中,你可以执行以下操作:

  1. 取消定时器:如果组件在 componentDidMount 中创建了定时器,那么在 componentWillUnmount 中应该清除(使用 clearTimeout 或 clearInterval)以避免内存泄漏。
  2. 取消订阅:如果组件在 componentDidMount 中订阅了外部事件或数据源(如全局事件订阅、WebSocket 连接等),那么在 componentWillUnmount 中应该取消订阅,以免造成资源浪费或不必要的数据更新。
  3. 销毁其他资源:如果组件创建了其他需要手动销毁的资源(如手动创建的 DOM 元素、第三方库的实例等),则应在 componentWillUnmount 中进行销毁。

在 componentWillUnmount 中,不应该执行与组件状态或 props 相关的操作,因为组件即将被销毁,这些操作已经没有意义。如果需要在组件被更新前执行清理操作,可以使用 componentDidUpdate 来实现。

需要注意的是,componentWillUnmount 不会在组件被刷新(re-render)时调用。它只会在组件被从 DOM 中完全移除时调用。如果你希望在每次组件更新前执行一些操作,可以使用 componentDidUpdate。

示例:

import React, {Component} from 'react'

export default class App extends Component {
    state = {
        isCreated: true
    }

    render() {
        return (
            <div>
                <button onClick={() => {
                    this.setState({
                        isCreated: !this.state.isCreated
                    })
                }}>click
                </button>
                {/* {this.state.isCreated?<Child/>:""} */}
                {this.state.isCreated && <Child/>}
            </div>
        )
    }
}

class Child extends Component {
    render() {
        return <div>
            child
        </div>
    }

    componentDidMount() {
        window.onresize = () => {
            console.log("resize")
        }

        this.timer = setInterval(() => {
            console.log("111")
        }, 1000)
    }

    /**
     * 在销毁生命周期清理窗口监听,定时器
     */
    componentWillUnmount() {
        console.log("componentWillUnmount")
        window.onresize = null
        clearInterval(this.timer)
    }
}

9.4.老生命周期的问题

  1. componentWillMount ,在ssr中这个方法将会被多次调用, 所以会重复触发多遍,同时在这里如果绑定事件, 将无法解绑,导致内存泄漏, 变得不够安全高效逐步废弃。

  2. componentWillReceiveProps 外部组件多次频繁更新传入多次不同的 props,会导致不必要的异步请求。

    只要父组件执行this.setState{},无论是否与子组件有关系,子组件都会执行componentWillReceiveProps 生命周期函数。

  3. componetWillupdate 更新前记录 DOM 状态, 可能会做一些处理,与componentDidUpdate相隔时间如果过 长, 会导致状态不太信

9.5.新生命周期的替代

9.5.1.getDerivedStateFromProps

getDerivedStateFromProps第一次的初始化组件以及后续的更新过程中(包括自身状态更新以及父传子) 都会执行, 返回一个对象作为新的state,返回的值的数据和state合并,返回null则说明不需要在这里更新state。getDerivedStateFromProps是一个类函数,所以无法获取this,要想获取state只能根据函数形参获取。

getDerivedStateFromProps可以在页面初始化时,在render之前最后一次修改state,在getDerivedStateFromProps函数里通过return返回的值的数据和state合并;所以可以在初始化时使用getDerivedStateFromProps生命周期函数执行初始化时以及每次state渲染时都要执行的逻辑。

9.5.1.1.案例1:

getDerivedStateFromProps可以在页面初始化时以及后续每次state更新时触发调用。

import React, {Component} from 'react';

export default class App extends Component {
    state = {
        myName: "zhangsan",
        age: 11
    }

    /**
     *
     * @param nextProps :原先的Props
     * @param nextState :原先的State
     * @returns {{myName: string}}
     */
    static getDerivedStateFromProps(nextProps, nextState) {
        console.log("getDerivedStateFromProps;nextState:", nextState.myName)
        //返回的数据会和state里的数据合并
        return {
            myName: "stonebridge"
        };
    }

    render() {
        return (
            <div>
                <button onClick={() => {
                    this.setState({
                        myname: "xiaoming",
                        age: 8888
                    })
                }}>click
                </button>
                app-{this.state.myName}-{this.state.age}
            </div>
        );
    }
}

21165359405

9.5.1.2.案例2:

getDerivedStateFromProps需要配合componentDidUpdate完成取代componentWillReceiveProps

  1. 当父组件将状态传递到子组件时,通过getDerivedStateFromProps将状态的return合并到子组件的state中。
  2. 在componentDidUpdate中通过进行状态是否改变,如果改变了则向后台发送异步请求,否则不发送请求,这样当重复点击时,不会重复发送请求。
import React, {Component} from 'react'
import axios from 'axios'

export default class App extends Component {
    state = {
        type: 1
    }

    render() {
        return (
            <div>
                <ul>
                    <li onClick={() => {this.setState({type: 1 })}}>正在热映</li>
                    <li onClick={() => {this.setState({type: 2 })}}>即将上映</li>
                </ul>
                
                <FilmList type={this.state.type}></FilmList>
            </div>
        )
    }
}


class FilmList extends Component {
    state = {
        list: [],
        type: 1
    }

    //初始化-执行一次
    componentDidMount() {
        this.request()
    }

    /**
     * 1.在生命周期函数getDerivedStateFromProps中无法发起异步请求,因为return会立即执行,不会等待ajax返回;也无法进行this.setState{}的操作,因为拿不到this。
     * 所以getDerivedStateFromProps取代componentWillReceiveProps是需要配合componentDidUpdate完成。
     * @param nextProps
     * @param nextState
     * @returns {{type}}
     */
    static getDerivedStateFromProps(nextProps, nextState) {
        console.log("getDrivedStateFromProps", nextProps)
        return {
            type: nextProps.type
        }
    }

    componentDidUpdate(prevProps, prevState) {
        console.log(this.state.type, prevState.type)
        //2.进行type判断,如果type更新了,则发送ajax请求,如果type没有变则不发送ajax请求;如果重复点击也不会重复更新。
        if (this.state.type === prevState.type) {
            return
        } else {
            this.request()
        }
    }

    request() {
        if (this.state.type === 1) {
            console.log("请求卖座正在热映的数据")
            axios({
                url: "https://m.maizuo.com/gateway?cityId=110100&pageNum=1&pageSize=10&type=1&k=6369301",
                headers: {
                    'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.2.0","e":"16395416565231270166529","bc":"110100"}',
                    'X-Host': 'mall.film-ticket.film.list'
                }
            }).then(res => {
                console.log(res.data.data.films)
                this.setState({
                    list: res.data.data.films
                })
            })
        } else {
            console.log("请求卖座即将上映的数据")
            axios({
                url: "https://m.maizuo.com/gateway?cityId=110100&pageNum=1&pageSize=10&type=2&k=8077848",
                headers: {
                    'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.2.0","e":"16395416565231270166529","bc":"110100"}',
                    'X-Host': 'mall.film-ticket.film.list'
                }
            }).then(res => {
                console.log(res.data.data.films)
                this.setState({
                    list: res.data.data.films
                })
            })
        }
    }

    render() {
        return <ul>
            {this.state.list.map(item => <li key={item.filmId}>{item.name}</li>)}
        </ul>
    }
}

9.5.2.getSnapshotBeforeUpdate

getSnapshotBeforeUpdate 取代了componetWillUpdate ,触发时间为update发生的时候,在**render之后 dom渲染之前(getSnapshotBeforeUpdate 在render之后,componentDidUpdate之前执行)**返回一个值,作为componentDidUpdate的第三个参数

9.5.2.1.案例1

getSnapshotBeforeUpdate 在render之后,componentDidUpdate之前执行,getSnapshotBeforeUpdate 返回的值在componentDidUpdate第三个形参中接收。

import React, {Component} from 'react';

export default class App extends Component {
    state = {
        mytext: "1111"
    }

    render() {
        console.log("render")
        return (
            <div>
                <button onClick={() => {this.setState({myText: "222222"})}}>click</button>
                {this.state.myText}
            </div>
        );
    }

    componentDidUpdate(prevProps, prevState, snapshot) {
        console.log("componentDidUpdate,snapshot:" + snapshot)
    }

    getSnapshotBeforeUpdate(prevProps, prevState) {
        console.log("getSnapshotBeforeUpdate")
        return 100;
    }
}

9.5.2.2.案例2

import React, {Component} from 'react';
import './css/04-css.css'

export default class App extends Component {
    myref = React.createRef()

    state = {
        list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
    }

    render() {
        return (
            <div>
                <button onClick={() => {
                    this.setState({
                        list: [...[16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], ...this.state.list]
                    })
                }}>来10封邮件
                </button>
                <h1>邮箱应用</h1>
                <div ref={this.myref} style={{height: "200px", overflow: "auto"}}>
                    <ul>
                        {
                            this.state.list.map((item, index) => <li key={item} style={{
                                height: "40px",
                                background: "yellow"
                            }}>{item}</li>)
                        }
                    </ul>
                </div>

            </div>
        );
    }

    getSnapshotBeforeUpdate(prevProps, prevState) {
        //获取容器高度
        console.log("getSnapshotBeforeUpdate,记录未更新前的容器高度:" + this.myref.current.scrollHeight)
        return this.myref.current.scrollHeight
    }

    componentDidUpdate(prevProps, prevState, snapshot) {
        console.log("componentDidUpdate,记录已更新前的容器高度:" + this.myref.current.scrollHeight)
        this.myref.current.scrollTop += this.myref.current.scrollHeight - snapshot
    }
}

9.6.react中性能优化的方案

  1. shouldComponentUpdate

    需要开发者自身进行判断

    控制组件自身或者子组件是否需要更新,尤其在子组件非常多的情况下, 需要进行优化。

  2. PureComponent

    PureComponent会帮你比较新props跟旧的props, 新的state和老的state(值相等,或者对象含有相同的属性、且属性值相等 ),决定shouldcomponentUpdate 返回true 或者 false, 从而决定要不要呼叫 render function。

    注意:

    如果你的 state 或 props 『永远都会变』,那 PureComponent 并不会比较快,因为 shallowEqual 也需要花时间。

    引入PureComponent即可,不需要shouldComponentUpdate进行手动的state的前后比较。

    import React, {PureComponent} from 'react'
    
    export default class App extends PureComponent {
        state = {
            myname: "kerwin"
        }
    
        render() {
            console.log("render")
            return (
                <div>
                    <button onClick={() => {
                        this.setState({
                            myname: "xiaoming"
                        })
                    }}>click
                    </button>
    
                    {this.state.myname}
                </div>
            )
        }
    
        // scu 性能优化函数
        // shouldComponentUpdate(nextProps,nextState){
        //     // return true; //应该更新
        //     //return false;; //阻止更新
        //     // this.state  老的状态
        //     // nextState   新的状态
        //     if(JSON.stringify(this.state)!== JSON.stringify(nextState)){
        //         return true
        //     }
    
        //     return false
        // }
    
        UNSAFE_componentWillUpdate() {
            console.log("UNSAFE_componentWillUpdate")
        }
    
        componentDidUpdate() {
            console.log("componentDidUpdate")
        }
    }
    

9.7.swiper实现

安装swiper

npm install swiper

9.7.1.同步swiper

同步swiper即数据在list中是固定的。

import React, {Component} from 'react';
//1.引入Swiper核心组件和Navigation
import Swiper, {Navigation, Pagination} from "swiper";
//2.引入Swiper的css
import 'swiper/swiper-bundle.css'
//3.使用 Swiper库或插件Navigation,Pagination
Swiper.use([Navigation, Pagination])

export default class App extends Component {
    //4.此时数据是固定的
    state = {
        list: ["111", "222", "333"]
    }

    //5.组件挂载到DOM上之后所执行的componentDidMount函数
    //初始化swiper对象(className=swiper),并设置pagination分页(className=swiper-pagination)
    componentDidMount() {
        new Swiper('.swiper', {
            pagination: {
                el: '.swiper-pagination'
            }
        })
    }

    render() {
        return (
            <div>
                {/*6.html构造要符合官网要求*/}
                <div className="swiper">
                    <div className="swiper-wrapper" style={{height: '200px', background: 'yellow'}}>
                        {
                            this.state.list.map((item, index) => 
                                                <div className='swiper-slide' key={item}>{item}</div>
                                               )
                        }
                    </div>
                    {/*如果需要分页器 */}
                    <div className="swiper-pagination"></div>
                </div>
            </div>
        );
    }
}

9.7.2.异步swiper

异步swiper即数据是后续加载的

import React, {Component} from 'react';
//1.引入Swiper核心组件和Navigation
import Swiper, {Navigation, Pagination} from "swiper";
//2.引入Swiper的css
import 'swiper/swiper-bundle.css'
//3.使用 Swiper库或插件Navigation,Pagination
Swiper.use([Navigation, Pagination])

export default class App extends Component {
    //4.此时数据是空,后续加载
    state = {
        list: []
    }

    //5.组件挂载到DOM上之后所执行的componentDidMount函数
    //通过定时器更新数据
    componentDidMount() {
        setTimeout(() => {
            this.setState({
                list: ["aaa", "bbbb", "cccc"]
            })
        }, 2000)
    }

    //6.在组件更新(重新渲染)之后被调用。它在首次渲染后不会被触发,只有在组件的 props或state发生变化,导致组件重新渲染时才会被调用。
    //componentDidMount中通过定时器更新数据,然后在componentDidUpdate。
    //初始化swiper对象(className=swiper),并设置pagination分页(className=swiper-pagination)
    componentDidUpdate(prevProps, prevState, snapshot) {
        new Swiper('.swiper', {
            pagination: {
                el: '.swiper-pagination'
            }
        })
    }

    render() {
        return (
            <div>
                {/*7.html构造要符合官网要求*/}
                <div className="swiper">
                    <div className="swiper-wrapper" style={{height: '200px', background: 'yellow'}}>
                        {
                            this.state.list.map((item, index) => <div className='swiper-slide' key={item}>{item}</div>)
                        }
                    </div>
                    {/*如果需要分页器 */}
                    <div className="swiper-pagination"></div>
                </div>
            </div>
        );
    }
}

9.7.3.swiper组件

9.7.3.1.定义swiper组件核心
import React, {Component} from 'react';
//1.引入Swiper核心组件和Navigation
import Swiper, {Navigation, Pagination} from "swiper";
//2.引入Swiper的css
import 'swiper/swiper-bundle.min.css'
//3.使用 Swiper库或插件Navigation,Pagination
Swiper.use([Navigation, Pagination])

export default class KSwiper extends Component {

    //5.组件挂载到DOM上之后所执行的componentDidMount函数
    //初始化swiper对象(className=swiper),并设置pagination分页(className=swiper-pagination)
    componentDidMount() {
        new Swiper('.swiper', {
            pagination: {
                el: '.swiper-pagination',
            },
            loop: true
        })
    }

    render() {
        return (
            <div>
                {/*6.html构造要符合官网要求,通过插槽把位置预留给使用者。*/}
                <div className="swiper">
                    <div className="swiper-wrapper">
                        {this.props.children}
                    </div>
                    {/*7.设置分页器 */}
                    <div className="swiper-pagination"></div>
                </div>
            </div>
        )
    }
}
9.7.3.2.定义SwiperItem轮播图的模块
import React, {Component} from 'react';
//创建轮播组件的单个轮播模块的,为其设置正确的类名swiper-slide
export default class KSwiperItem extends Component {
    render() {
        return (
            <div className="swiper-slide">
                {this.props.children}
            </div>
        )
    }
}
9.7.3.3.使用swiper组件
import React, {Component} from 'react';
import KSwiper from "./swiper/Swiper";
import SwiperItem from "./swiper/SwiperItem";
import axios from 'axios'
//1.使用者使用该组件
export default class App extends Component {
    state = {
        list: []
    }

    //2.组件挂载到DOM上之后所执行的componentDidMount函数,通过ajax获取数据
    componentDidMount() {
        axios.get(`/test.json`).then(res => {
            this.setState({
                list: res.data.data.films
            }, () => {
                console.log(res.data.data.films)
            })
        })
    }

    render() {
        return (
            <div>
                {/*使用轮播组件KSwiper;1.轮播组件预留插槽,使用SwiperItem渲染单个模块;2.通过loop设置是否循环*/}
                <KSwiper loop={true}>
                    {
                        this.state.list.map((item, index) =>
                            <SwiperItem key={item.filmId}>
                                <img src={item.poster}
                                     alt={item.name}
                                     style={{width: "30%"}}/>
                            </SwiperItem>
                        )
                    }
                </KSwiper>
            </div>
        );
    }
}
{Navigation, Pagination} from "swiper";
//2.引入Swiper的css
import 'swiper/swiper-bundle.css'
//3.使用 Swiper库或插件Navigation,Pagination
Swiper.use([Navigation, Pagination])

export default class App extends Component {
    //4.此时数据是空,后续加载
    state = {
        list: []
    }

    //5.组件挂载到DOM上之后所执行的componentDidMount函数
    //通过定时器更新数据
    componentDidMount() {
        setTimeout(() => {
            this.setState({
                list: ["aaa", "bbbb", "cccc"]
            })
        }, 2000)
    }

    //6.在组件更新(重新渲染)之后被调用。它在首次渲染后不会被触发,只有在组件的 props或state发生变化,导致组件重新渲染时才会被调用。
    //componentDidMount中通过定时器更新数据,然后在componentDidUpdate。
    //初始化swiper对象(className=swiper),并设置pagination分页(className=swiper-pagination)
    componentDidUpdate(prevProps, prevState, snapshot) {
        new Swiper('.swiper', {
            pagination: {
                el: '.swiper-pagination'
            }
        })
    }

    render() {
        return (
            <div>
                {/*7.html构造要符合官网要求*/}
                <div className="swiper">
                    <div className="swiper-wrapper" style={{height: '200px', background: 'yellow'}}>
                        {
                            this.state.list.map((item, index) => <div className='swiper-slide' key={item}>{item}</div>)
                        }
                    </div>
                    {/*如果需要分页器 */}
                    <div className="swiper-pagination"></div>
                </div>
            </div>
        );
    }
}

9.7.3.swiper组件

9.7.3.1.定义swiper组件核心
import React, {Component} from 'react';
//1.引入Swiper核心组件和Navigation
import Swiper, {Navigation, Pagination} from "swiper";
//2.引入Swiper的css
import 'swiper/swiper-bundle.min.css'
//3.使用 Swiper库或插件Navigation,Pagination
Swiper.use([Navigation, Pagination])

export default class KSwiper extends Component {

    //5.组件挂载到DOM上之后所执行的componentDidMount函数
    //初始化swiper对象(className=swiper),并设置pagination分页(className=swiper-pagination)
    componentDidMount() {
        new Swiper('.swiper', {
            pagination: {
                el: '.swiper-pagination',
            },
            loop: true
        })
    }

    render() {
        return (
            <div>
                {/*6.html构造要符合官网要求,通过插槽把位置预留给使用者。*/}
                <div className="swiper">
                    <div className="swiper-wrapper">
                        {this.props.children}
                    </div>
                    {/*7.设置分页器 */}
                    <div className="swiper-pagination"></div>
                </div>
            </div>
        )
    }
}
9.7.3.2.定义SwiperItem轮播图的模块
import React, {Component} from 'react';
//创建轮播组件的单个轮播模块的,为其设置正确的类名swiper-slide
export default class KSwiperItem extends Component {
    render() {
        return (
            <div className="swiper-slide">
                {this.props.children}
            </div>
        )
    }
}
9.7.3.3.使用swiper组件
import React, {Component} from 'react';
import KSwiper from "./swiper/Swiper";
import SwiperItem from "./swiper/SwiperItem";
import axios from 'axios'
//1.使用者使用该组件
export default class App extends Component {
    state = {
        list: []
    }

    //2.组件挂载到DOM上之后所执行的componentDidMount函数,通过ajax获取数据
    componentDidMount() {
        axios.get(`/test.json`).then(res => {
            this.setState({
                list: res.data.data.films
            }, () => {
                console.log(res.data.data.films)
            })
        })
    }

    render() {
        return (
            <div>
                {/*使用轮播组件KSwiper;1.轮播组件预留插槽,使用SwiperItem渲染单个模块;2.通过loop设置是否循环*/}
                <KSwiper loop={true}>
                    {
                        this.state.list.map((item, index) =>
                            <SwiperItem key={item.filmId}>
                                <img src={item.poster}
                                     alt={item.name}
                                     style={{width: "30%"}}/>
                            </SwiperItem>
                        )
                    }
                </KSwiper>
            </div>
        );
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值