React (三) 创建安装脚手架,类组件与函数式组件;生命周期;父子通信props;插槽;非父子通信Context

文章目录

  • 一、脚手架的创建与安装
    • [1. 认识脚手架](#1. 认识脚手架)
    • [2. 安装脚手架](#2. 安装脚手架)
    • [3. 创建react项目](#3. 创建react项目)
    • [4. 项目结构](#4. 项目结构)
  • 二、从0编写
  • 三、组件化开发
    • [1. 什么是组件化开发](#1. 什么是组件化开发)
    • [2. 类组件](#2. 类组件)
    • [3. render函数](#3. render函数)
    • [4. 函数式组件](#4. 函数式组件)
  • 四、生命周期
    • [1. 挂载Mount](#1. 挂载Mount)
    • [2. 更新Update](#2. 更新Update)
    • [3. 卸载Unmount](#3. 卸载Unmount)
    • [4. 不常用的生命周期](#4. 不常用的生命周期)
  • 五、父子组件通信
    • [1. 父传子](#1. 父传子)
    • [2. 父传子数据的props类型限制](#2. 父传子数据的props类型限制)
    • [3. 子传父](#3. 子传父)
    • [4、{...object} 解构 传递对象数据---拓展](#4、{...object} 解构 传递对象数据---拓展)
  • 六、插槽
    • [1. 组件子元素实现插槽效果](#1. 组件子元素实现插槽效果)
    • [2. props实现插槽效果](#2. props实现插槽效果)
    • [3. 作用域插槽](#3. 作用域插槽)
  • 七、非父子组件通信Context

一、脚手架的创建与安装

1. 认识脚手架

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

脚手架(scaffold)就是一种工具,帮我们可以快速生成一个通用的项目目录结构,并将所需的工程环境配置好。让项目从搭建到开发,再到部署,整个流程变得快速和便捷;

2. 安装脚手架

React的脚手架:create-react-app, 简称cra

(1)提前安装node环境.(这个可参考之前安装Vue的记录配置node环境

(2)执行命令安装脚手架:npm install create-react-app -g;

运行create-react-app --version查看安装的版本,显示版本就说明脚手架安装成功。

(执行命令在powershell里也可以,在git bash里也可以)

3. 创建react项目

在对应的文件夹下执行命令create-react-app 项目名,创建项目。
注意:项目名称不能包含大写字母

运行项目:npm run start

4. 项目结构

javascript 复制代码
|--public
|    |-- favucin.ico     // 标签页的icon图标
|    |-- index.html      // 入口文件
|    |-- logo192.png     // 在manifest.json文件里被调用
|    |-- logo512.png     // 在manifest.json文件里被调用
|    |-- manifest.json  // 和web app配置相关
|    |-- robots.txt     //指定本网站哪些文件可以或者无法被爬取
|--src
|    |-- App.css     // App组件相关的样式
|    |-- App.js      // App组件的代码文件
|    |-- App.test.js // App组件的测试代码文件
|    |-- index.css   // 全局的样式文件
|    |-- index.js    // 整个应用程序的入口文件
|    |-- logo.svg    // 启动项目时的react图标
|    |-- reportWebVitals.js  //
|    |-- setupTest.js    //测试初始化文件
|-- package.json        // 对整个应用程序的描述:应用名称、版本号、一些依赖包

logo192.png, logo512.png,manifest.json文件都与PWA相关,PWA(国内应用较少)了解即可。

二、从0编写

将src下的文件都删掉,运行项目,提示缺少index.js文件,新建src/index.js

javascript 复制代码
import React from "react"
import ReactDOM from "react-dom/client" // 旧版是从react里导入ReactDOM

class App extends React.Component {
  constructor() {
    super()
    this.state = {
      msg: 'HelloWorld'
    }
  }
  render () {
    const { msg } = this.state
    return (
      <h2>{msg}</h2>
    )
  }
}
// 这里的#root是index.html文件里的
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)

与之前写的一样,都是创建类组件,然后渲染到桌面上。

可将App组件拆分到App.jsx文件里:

三、组件化开发

1. 什么是组件化开发

组件化就是分而治之;如果一个页面里的所有功能和逻辑处理都放在一起,就会很难维护。如果将一个页面拆分为一个个小的功能块,有利于之后页面的管理和扩展。

用组件化的思想来构建应用:

  • 一个完整的页面可以分为很多组件;
  • 每个组件都用于实现页面的一个功能块
  • 每个组件都可以继续细分,组件本身又可以进行复用

最终,任何的应用都会被抽象成一棵组件树

2. 类组件

定义类组件的要求:

  • 组件的名称是大写字符开头(无论类组件还是函数组件)
  • 类组件需要继承React.Component(后边优化时,可继承Pure)
  • 类组件必须实现render函数

使用class定义一个类组件:

javascript 复制代码
import React from "react";  // 导入的React是个对象,里面有React.Component属性
class App extends React.Component {
  // constructor是可选的,通常在构造函数里初始化一些数据
  constructor() {
    super()
    // 维护的是组件内部的数据
    this.state = {
      msg: 'Hello World'
    }
  }
  // 组件内必须要实现的方法
  render () {
    return <h2>{this.state.msg}</h2>
  }
}

也可以这样继承: 反正继承的都是Component

javascript 复制代码
import { Component } from "react";
class App extends Component {
  render () {
    return <h2>Hello</h2>
  }
}

3. render函数

(1) render函数什么时候被调用

页面初次加载时,函数render会被调用渲染页面。

propsstate发生变化时,render会再次被调用。(props后面会学,state里的数据是通过调用this.setState进行修改)

(2) 返回值有哪几类

  • React元素
    通过JSX创建的就是React元素。JSX本质上就是调用React.createElement(),创建一个React的元素。

  • 数组或fragments :返回多个元素
    fragments后边再学

    javascript 复制代码
     // 2.组件或者fragments(后续学习)
        return ["abc", "cba", "nba"]
        return [
           <h1>h1元素</h1>,
           <h2>h2元素</h2>,
           <div>哈哈哈</div>
        ]
  • Portals (还没学):可以渲染子节点到不同的DOM子树中。

  • 字符串或数值类型:在DOM中渲染为文本节点

  • 布尔类型或null :什么都不渲染

    javascript 复制代码
    return "Hello World" // 界面渲染 HelloWorld
    return true          // 什么都不渲染

4. 函数式组件

(1) 函数组件是使用function来进行定义的函数,这个函数返回的内容和类组件中render函数返回的一致。

(2) 函数组件的特点:

  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
  • this关键字不能指向组件实例(因为没有组件实例)
  • 没有内部状态(state),即使自己定义了state数据,每次调用state返回的数据都是初始数据。也就是无法进行状态维护
javascript 复制代码
// 函数式组件
function App () {
  const state = { name: 'tom' } // 每次调用App组件,state里的数据值都是tom
  // 返回值:和类组件中的render函数返回的值类型一致。
  return <h2>Hello World</h2>
}

四、生命周期

生命周期 就是从创建到销毁的这个过程;

在生命周期这个过程中,可以划分为很多阶段:

  • 挂载阶段(Mount): 组件第一次在Dom树中被渲染的过程
  • 更新阶段(Update):组件状态发生变化,重新更新渲染的过程
  • 卸载阶段(Unmount):组件从Dom树中被移除的过程

React为了告诉我们当前组件处于哪些阶段,会在对应的阶段调用某些函数,这些函数就是生命周期函数

  • Constructor

    如果不初始化 state 或不进行方法绑定(为事件绑定实例this),则不需要为 React 组件实现构造函数。

  • componentDidMount

    依赖于DOM的操作可以在这里进行;

    在此处发送网络请求就最好的地方;(官方建议)

    可以在此处添加一些订阅

  • componentDidUpdate

    componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行此方法。

    当组件更新后,可以在此处对 DOM 进行操作;

  • componentWillUnmount

    该生命周期函数会在组件卸载及销毁之前直接调用。

    在此方法中执行必要的清理操作 ( 比如 清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅等)

谈生命周期函数,主要指的是类组件,因为函数式组件没有生命周期函数。

1. 挂载Mount

挂载步骤:
(1) 创建组件实例(constructor) 。创建组件实例会先执行对应类组件里的构造函数constructor

每执行一次<HelloWordl/>,相当于new一个class HelloWorld extends Component{}的实例 (就像java一样)

(2) 执行render方法,渲染界面
(3) 挂载完毕,执行生命周期函数componentDidMount

javascript 复制代码
class HelloWorld extends React.Component {
  // 1.执行构造方法
  constructor() {
    super()
    console.log('HW, constructor');
    this.state = { msg: 'HelloWorld' }
  }
  // 2. 执行render函数
  render () {
    console.log('HW, render');
    let { msg } = this.state
    return (
      <h2>{msg}</h2>
    )
  }
  // 3. 挂载完成
  componentDidMount () {
    console.log('HW, componentDidMount');
  }
}

挂载阶段,依次打印:HW, constructor,HW, render,HW, componentDidMount

2. 更新Update

从图里可以看出,触发更新有三种方式,现在只说setState()
(1) setState()修改数据
(2) 重新调用render函数
(3) 更新完成,调用生命周期函数componentDidUpdate

javascript 复制代码
  // 组件的DOM更新完成
  componentDidUpdate () {
    console.log('HW, componentDidUpdate');
  }

3. 卸载Unmount

HelloWorld组件

javascript 复制代码
  // 5. 组件将从DOM树中被移除:卸载组件
  componentWillUnmount () {
    console.log('HW, componentWillUnmount');
  }

App组件:点击按钮时,隐藏组件

javascript 复制代码
 render () {
   let { isShow } = this.state
   return (
     <div>
       <button onClick={e => this.changeHWShow()}>切换</button>
       {isShow && <HelloWorld />}
     </div>
   )
 }
 changeHWShow () {
  this.setState({
    isShow: !this.state.isShow
  })
}

4. 不常用的生命周期

shouldComponentUpdate下一篇博客会说。

具体查看官方文档:官方文档

五、父子组件通信

1. 父传子

  • 父组件通过属性=值的形式来传递给子组件数据
  • 子组件通过props参数获取父组件传递过来的数据(不能换名,只能叫props)

需求:父组件Content给子组件Banner传递数据:

父组件Content:

javascript 复制代码
 this.state = {
   banners: ['新歌曲', '新歌单', '新MV'],
   title: '轮播图'
 }
render () {
  let { banners, title } = this.state
  return (
    <div>
       <Banner banners={banners} title={title} />
    </div>
  )
}

<Banner banners={banners} />相当于在new实例时,传递了参数。所以Banner的构造函数需要接收这个参数。

子组件Banner:

javascript 复制代码
export class Banner extends Component {
  constructor(props) {
  // props接收之后传给super()
    super(props)
    console.log('Banner接收:', props);
  }
  
  render () {
  // 从props属性里可以读取父组件传递过来的值
    let { banners, title } = this.props
    return (
      <div  className='banner'>
        <h3>{title}</h3>
        {banners.map(item => {
          return <li key={item}>{item}</li>
        })}
      </div>
    )
  }
}

其实,如果没有constructor构造函数(2-6行代码),也会默认通过props帮忙接收父组件的数据。render函数里仍然可以通过this.props接收数据。

2. 父传子数据的props类型限制

(1) 设置数据类型限制

javascript 复制代码
// 1. 引入 prop-types
import PropTypes from 'prop-types'
class Banner extends Component {
 ...
}
// 2. 限制类型 
// 要求title是字符串,且必须传值(如果没传,且Banner组件也没有设置title默认值,即使Banner不使用title,也会报错);
// 要求banners是数组类型的数据
Banner.propTypes = {
  title: PropTypes.string.isRequired,
  banners: PropTypes.array
}

PropTypes的其他限制:NPM:prop-types

(2) 设置数据默认值

  • 在组件内用static关键字;
  • 在组件外用defaultProps
javascript 复制代码
class Banner extends Component {
  // 方式一:设置默认值
  static defaultProps = {
    title: '默认标题',
    banners: []
  }
}
// 方式二
Banner.defaultProps = {
  banners: [],
  title: "默认标题"
}

测试

为了方便观察,给banner添加了样式

Banner组件:

html 复制代码
  <!--规范传值-->
  <Banner banners={banners} title={title} />
   <!--啥也不传-->
  <Banner />

3. 子传父

需求:

其实还是父组件给子组件传递一个函数,子组件调用这个函数,并将数据通过参数的形式传递给父组件。

4、{...object} 解构 传递对象数据---拓展

父组件的state里有一个对象数据要传递给子组件。

javascript 复制代码
this.state = {
     info: { stuName: 'tom', age: 18 }
}
// props传递数据
 <Home stuName={info.stuName} age={info.age} />
 {/* 等价于 */}
 <Home {...info} /> 

六、插槽

比如导航区的组件NavBar;每个页面的导航组件结构大体一致(分为左中右三个部分),但内容不一样。

在Vue里可以通过插槽实现不同样式的导航组件。

React里并没有插槽这个概念。对于这种需要插槽的情况,React有两种方案可以实现:

  • 组件的children子元素
  • props属性传递React元素

1. 组件子元素实现插槽效果

App.jsx:

html 复制代码
<!--将button,h2,i等标签传递给NavBar,NavBar标签包裹的就叫子元素-->
  <NavBar>
    <button>按钮</button>
    <h2>HelloWorld</h2>
    <i>斜体文字</i>
  </NavBar>

子组件可以通过this.props.children接收父组件传递过来的react元素。当有多个元素时,children是一个数组,包含这些元素;当只有一个元素时,children的值就是这个元素。

nav-bar.jsx:

javascript 复制代码
// 子组件通过props接收react元素
 render () {
   let { children } = this.props
   console.log('children', children);
   return (
     <div className='nav-bar'>
       <div className="left">{children[0]}</div>
       <div className="center">{children[1]}</div>
       <div className="right">{children[2]}</div>
     </div>
   )
 }


如果只传button

缺点 : <NAvBar>标签里子元素(react元素)的书写顺序决定了元素在children里的索引值。 子组件通过children的索引值获取传入的元素,容易出错。

2. props实现插槽效果

和之前props传值一样,只不过这次props传递的是react元素。

通过具体的属性名,可以在传入元素和获取元素时更加精准。

3. 作用域插槽

适用场景 :结构由父组件决定,但是结构里用的值在子组件中。

子组件有标题titles数据,父组件传递结构,决定这些标题如何展示。比如说展示按钮

传递react元素:

问题是这样渲染出来的按钮内容都是哈哈哈, 所以需要子组件将标题内容传给父组件。

怎么传?还是通过回调函数传参的方式,itemType改成一个函数。

App.jsx:

html 复制代码
 <TabControl
   itemType={(item) => <button>{item}</button>}
 />

子组件:

javascript 复制代码
render () {
  let { titles } = this.state
  let { itemType } = this.props // 接收函数
  return (
    <div className='tab-control'>
      {titles.map((item, index) => {
        return (
          <div className='item' key={index} >
            {/* 调用函数并传参 */}
            {itemType(item)}
          </div>
        )
       })
      }
    </div >
  )
}

就很绝,甚至可以根据不同的title内容返回不同的页面结构

javascript 复制代码
  getTabItem(item) {
    if (item === "流行") {
      return <span>{item}</span>    // 标签 
    } else if (item === "新款") {
      return <button>{item}</button> // 按钮
    } else {
      return <i>{item}</i>  // 斜体文字
    }
  }
...
itemType={item => this.getTabItem(item)}

七、非父子组件通信Context

在组件树中,顶层Provider(父组件)提供数据,下边的作为消费者Consumer(后代组件)接收数据,或者通过指定的方式接收数据。

1、Context上下文的使用(类组件)

通过props实现父给孙组件传值,会打扰到中间的组件。而且比较麻烦。

组件关系 App> Home> HomeInfo

Context使用步骤:

(1) 使用React.createContext创建一个context

src/context/theme-context.js:

javascript 复制代码
import React from "react";
// 1. 创建一个Context
const ThemeContext = React.createContext()
export default ThemeContext

(2) 引入上下文,并为后代提供数据

App组件中:

javascript 复制代码
import ThemeContext from './contex

{/* 2.通过ThemeContext中的Provider中value属性为后代提供数据 */}
<ThemeContext.Provider value={{ color: 'red', price: '50' }}>
   <Home />
 </ThemeContext.Provider>

给子组件Home包裹标签<ThemeContext>, (这里演示非父子组件传值,所以包裹Home。也可以在Home组件里给孙组件HomeInfo包裹标签,包裹到的组件及后代组件都可以用到数据)

(3) 在组件中指定要读取的Context类型,并读取数据

HomeInfo组件:

javascript 复制代码
class HomeInfo extends Component {
  render () {
    // 4. 第四步:读取数据
    console.log('上下文:', this.context); 
		...
  }
}
// 3. 第三步:context可能有很多,设置组件的要读取哪一类的context
HomeInfo.contextType = ThemeContext

3、Context的使用(函数式组件)

(1) 函数组件读取Context数据---Context.consumer

定义函数组件HomeBanner,并在Home组件中使用

Home组件:

javascript 复制代码
  render () {
    return (
      <div>
        <h2>Home组件</h2>
        <HomeBanner />
      </div>
    )
  }

注意之前在App组件中,已经用Context的组件将Home组件包裹了。

HomeBanner组件:

使用context名.Consumer读取数据。
Context.Consumer 标签需要 函数作为子元素(function as child);

这个函数接收当前的 context 值,返回一个 React 节点

javascript 复制代码
import ThemeContext from "./context/theme-context"
function HomeBanner () {
  return (
    <div>
      {/* 函数式组件中使用Context共享的数据 */}
      <ThemeContext.Consumer>
        {
           {/* value就是context的值*/}
          value => {
            return <h3>颜色:{value.color}</h3>
          }
        }
      </ThemeContext.Consumer>
    </div>
  )
}

(2) 组件读取多个Context的数据

在类组件中,组件名.contextType = xxx只能指定一种context类型,如果该组件想读取多个context的数据,可以借助Context.Consumer

(3) defaultValue

什么时候使用默认值defaultValue呢?当组件未被Context组件包裹,但又想使用该Context的数据时。比如Profile组件

步骤一: 在context文件里指定默认值

步骤二: 组件中使用

控制台打印结果:{color: 'blue', price: 10}

(4) ContextAPI总结

  • React.createContext

    创建一个需要共享的Context对象:ThemeContext = React.createContext({ color: "blue", price: 10 })
    defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值

  • Context.Provider

    每个 Context 对象都会返回一个 Provider React 组件;Provider 接收一个 value 属性,传递给消费组件(Consumer);<ThemeContext.Provider value={``{ color: 'red', price: '50' }}>

    当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;

  • Class.contextType

    挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象:
    Profile.contextType = ThemeContext

  • Context.Consumer

    这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context。

    这里需要 函数作为子元素(function as child);

    这个函数接收当前的 context 值,返回一个 React 节点;

什么时候使用Context.Consumer呢?

1.当使用value的组件是一个函数式组件时;

2.当组件中需要使用多个Context时;

相关推荐
小曲曲28 分钟前
接口上传视频和oss直传视频到阿里云组件
javascript·阿里云·音视频
学不会•2 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
EasyNTS3 小时前
H.264/H.265播放器EasyPlayer.js视频流媒体播放器关于websocket1006的异常断连
javascript·h.265·h.264
活宝小娜4 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点4 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow4 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o4 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
刚刚好ā5 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
沉默璇年6 小时前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder6 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript