重学React之组件化开发(上)

简介: 重学React之组件化开发

组件化思想的应用


尽可能的将页面拆分成一个个小的、可复用的组件。这样让我们的代码更加方便组织和管理,并且扩展性也更强。


React的组件相对于Vue更加的灵活和多样,按照不同的方式可以分成很多类组件:


  • 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component)。


  • 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component)。


  • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)。


这些概念有很多重叠,但是他们最主要是关注数据逻辑和UI展示的分离:


  • 函数组件、无状态组件、展示型组件主要关注UI的展示。


  • 类组件、有状态组件、容器型组件主要关注数据逻辑。


类组件


类组件的定义有如下要求:


  • 组件的名称是大写字符开头(无论类组件还是函数组件)


  • 类组件需要继承自 React.Component


  • 类组件必须实现render函数


使用class定义一个组件:


  • constructor是可选的,我们通常在constructor中初始化一些数据。


  • this.state中维护的就是我们组件内部的数据。


  • render() 方法是 class 组件中唯一必须实现的方法。 其中render函数可以返回一下值:


  • React 元素。


  • 数组或 fragments:使得 render 方法可以返回多个元素。


  • Portals:可以渲染子节点到不同的 DOM 子树中。


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


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


函数组件


函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容。


普通的函数式组件(非hooks):


  • 没有生命周期,也会被更新并挂载,但是没有生命周期函数。


  • 没有this(组件实例)。


  • 没有内部状态(state)。


生命周期


下面来介绍一下常用的生命周期函数。


Constructor


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


constructor中通常只做两件事情:


  • 通过给 this.state 赋值对象来初始化内部的state。


  • 为事件绑定实例(this)。


componentDidMount


componentDidMount() 会在组件挂载后(插入 DOM 树中)立即调用。


componentDidMount中通常进行哪里操作呢?


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


  • 在此处发送网络请求就最好的地方。


  • 可以在此处添加一些订阅(会在componentWillUnmount取消订阅)。


componentDidUpdate


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


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


  • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求。(例如,当 props 未发生变化时,则不会执行网络请求)。


  • componentDidUpdate()直接调用 setState() ,但请注意它必须被包裹在一个条件语句里。否则会导致死循环。


componentWillUnmount


componentWillUnmount() 会在组件卸载及销毁之前直接调用。


  • 在此方法中执行必要的清理操作。 例如,清除 timer,取消网络请求或清除


  • 在 componentDidMount() 中创建的订阅等。


  • componentWillUnmount()不应调用 setState() ,因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。


组件通信


父组件向子组件传递信息


将数据当成子组件的属性,然后子组件的props接收。


  • 函数组件接收 函数的第一个参数就是props参数对象,可以拿到传递的属性数据。


function MyButton(props) {
      console.log(props)
      return (
        <h1>{props.name}</h1>
      )
    }


  • 类组件接收 通过在构造函数中调用super(props),即可获取props。


class MyButton extends React.Component {
      // 也可以不写,react自动传入
      // constructor(props) {
      //   super(props)
      // }
      render() {
        return (
            <h1>{this.props.name}</h1>
        )
      }
    }


我们在传递props时,可以给props指定默认值,并且指定props的类型。


  • 指定默认值 通过class属性defaultProps,设置props的默认值。


static defaultProps = {
    name: "=====",
    age: 30
  }


  • 指定props的类型 安装


npm install prop-types --save


引入prop-types库


import PropTypes from 'prop-types';


函数组件


import React from 'react';
    import propTypes from 'prop-types'
    export default function AppTest(props) {
      return (
        <div>
          <h1>{props.name}</h1>
        </div>
      )
    }
    AppTest.defaultProps = {
      name: 11, // 这里会报警告
    }
    // 他也会检查默认值的类型是否正确
    AppTest.propTypes = {
      name: propTypes.string
    }


类组件


import React from 'react';
    import propTypes from 'prop-types'
    export default class AppTest extends React.Component{
      render() {
        return (
          <div>
            <h1>{this.props.name}</h1>
          </div>
        )
      }
    }
    AppTest.defaultProps = {
      name: 11, // 这里会报警告
    }
    // 他也会检查默认值的类型是否正确
    AppTest.propTypes = {
      name: propTypes.string
    }


子组件向父组件传递信息


其实就类似于回调函数,子组件通过this.props.事件调用函数,而父组件定义函数。然后将参数传递给父组件。


//父组件
      state = {
        color: 'red'
      }
      changeColor = color => {
        this.setState({
          color: color
        })
      }
      render() {
        // console.log(this)
        return (
          <div>
            <h1 style={{ color: this.state.color }} >llm</h1>
            <Son changeColor={this.changeColor.bind(this)}></Son>
          </div>
        )
      }
      //子组件
     render() {
        return (
          <div>
            <button onClick={() => { this.props.changeColor('blue') }}>蓝色</button>
            <button onClick={() => { this.props.changeColor('red') }}>红色</button>
          </div>
        )
      }


插槽


react中没有插槽这一概念,默认自定义组件中不能有内容。


利用props.children来实现


  • 如果标签内只有一个元素,那么props.children代表就是该元素


  • 如果标签内有多个元素,那么props.children是这些元素组成的数组


  • 但是,我们可以在定义组件时,在内容中写上{props.children}来表示占位,当使用自定义标签时,标签里的内容会自动取代{props.children}。


  • 每个组件都可以获取到 props.children:它包含组件的开始标签和结束标签之间的内容。


import React from 'react'
    export default function SlotTest(props) {
      return (
        <div>
          <div>slotTest组件中的本身标签</div>
          {props.children}
        </div>
      )
    }
    import React from 'react';
    import ReactDOM from "react-dom"
    import SlotTest from './SlotTest'
    ReactDOM.render((
      <SlotTest>
        <p>p标签</p>
        <a href="#">a标签</a>
        <div>div标签</div>
      </SlotTest>
    ), document.getElementById("root"))


网络异常,图片无法展示
|


利用props传入插槽内容


  • 我们也可以不用children占位,可以自定义占位的,然后通过标签属性的形式传入内容。


import React from 'react'
    export default function SlotTest(props) {
      return (
        <div>
          <div>slotTest组件中的本身标签</div>
          <div>{props.leftSlot}</div>
          {props.rightSlot}
        </div>
      )
    }
    import React from 'react';
    import ReactDOM from "react-dom"
    import SlotTest from './SlotTest'
    ReactDOM.render((
      <SlotTest leftSlot={<div>left标签</div>} rightSlot={<div>right标签</div>}  />
    ), document.getElementById("root"))


网络异常,图片无法展示
|


跨组件通信


通过逐个组件传递props。


通过context。


使用context传递数据要经过以下几步


  • 创建context实例。注意: 这里的默认值仅会在Consumer在组件树中无法找到匹配的Provider才会使用,因此即使你给Providervalue传入undefined值时,Consumer也不会使用默认值。


注意:这里的默认值可以接受一个对象。


let context = React.createContext('这里可以传入默认值')


  • 调用Provider内部组件将值进行传递,利用value属性来传递值。 如果没写这一步,则默认找默认值。这里是将接收共享数据的组件包裹着。


<ThemeContext.Provider value="dark">
       <Toolbar />
    </ThemeContext.Provider>


  • 指定当前读取的是哪一个context值


  • React 会往上找到最近的 Provider,然后使用它的值。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。


  • 如果没有定义Provider,则他会使用创建context实例时指定的默认值。


static contextType = context;


完整demo


import React, { createContext } from 'react'
    const ParentContext = createContext({
      color: 'red',
      value: 'zh======================'
    })
    export default class Parent extends React.Component {
      render() {
        return (
          <div>
            <ParentContext.Provider
              value={{ color: 'skyblue', value: 'llm==================' }}
            >
              <Son />
            </ParentContext.Provider>
          </div>
        )
      }
    }
    class Son extends React.Component {
      render() {
        return (
          <div>
            {/* 方式一: */}
            {/* <p style={{ color: this.context.color }}>{this.context.value}</p> */}
            {/* 方式二: */}
            <ParentContext.Consumer>
              {(item) => <p style={{ color: item.color }}>{item.value}</p>}
            </ParentContext.Consumer>
          </div>
        )
      }
    }
    // 方式一
    // Son.contextType = ParentContext


那么我们就来看如何使用它吧?


  • 直接调用this.context,但是必须加上这个静态的属性 static contextType = context对象这种使用不可以用在函数式组件中。


render() {
    return <Button theme={this.context} />;
}


  • 直接通过context的Consumer属性结合函数使用。这种方式是可以在函数式组件中使用的。


这种方法需要一个函数作为子元素。这个函数接收当前的 context 值,并返回一个 React 节点。传递给函数的 value 值等价于组件树上方离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext()defaultValue


export default class MyContext extends Component {
  render() {
    return (
      <div>
        <ContextProp.Consumer>
          //利用函数获取该value值。
          {
            value => <div>{value}</div>
          }
        </ContextProp.Consumer>
      </div>
    )
  }
}


  • 消费多个context


// Theme context,默认的 theme 是 “light” 值
const ThemeContext = React.createContext('light');
// 用户登录 context
const UserContext = React.createContext({
  name: 'Guest',
});
class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props;
    // 提供初始 context 值的 App 组件
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    );
  }
}
function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  );
}
// 一个组件可能会消费多个 context
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  );
}


通过以上事例,我们可以看出,在对于多个context共享时,代码非常难写,难看。所以一般共享多个数据不会去使用它,都是使用redux。


setState


setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。


为什么setState更新是异步的


  • setState设计为异步,可以显著的提升性能。


  • 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的。


  • 最好的办法应该是获取到多个更新,之后进行批量更新。


  • 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步。state和props不能保持一致性,会在开发中产生很多的问题。


如何获取异步更新后的state


可以使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。


this.setState({
      pp: "++++++++++++"
    }, () => {
      console.log(this.state.pp) // "++++++++++++"
    })


componentDidUpdate(prevProps, prevState, prev3) { // 这上面的参数都获取的是以前的值
    console.log(this.state.pp) //  "++++++++++++"
  }


那么setState修改数据,如何让其同步获取呢


  • 将setState放在定时器中更新数据


handleClick = () => {
    setTimeout(() => {
        this.setState({
          name: 'llm'
        })
    }, 0)
    console.log(this.state.name) // "llm"
  }


  • 将setState放在原生的dom事件中更新数据


componentDidMount() {
    const btn = document.getElementById('btn')
    btn.addEventListener('click', () => {
      this.setState({
        name: 'llm'
      })
      console.log(this.state.name) // "llm"
    })
  }


setState参数介绍


  • 可以直接传入一个对象,来修改state的值。


this.state = {
      name: "kkk",
      age: 20
    }
    handleClick = () => {
        this.setState({
          name: "==="
        })
        // 不会立刻获取到更新后的值。
        console.log(this.state.name) // kkk
    }
  <div>
    <h1>{this.state.name}</h1>
    <button onClick={this.handleClick}>按钮</button>
  </div>


  • 也可以传入一个updater。 updater 函数中接收的 stateprops 都保证为最新。updater 的返回值会与 state 进行浅合并。


this.state = {
      name: "kkk",
      age: 20
    }
     handleClick = () => {
        this.setState((state, props) => {
          return {
            name: "===="
          }
        })
      }
  <div>
    <h1>{this.state.name}</h1>
    <button onClick={this.handleClick}>按钮</button>
  </div>



相关文章
|
9天前
|
前端开发 JavaScript API
React开发需要了解的10个库
本文首发于微信公众号“前端徐徐”,介绍了React及其常用库。React是由Meta开发的JavaScript库,用于构建动态用户界面,广泛应用于Facebook、Instagram等知名网站。文章详细讲解了Axios、Formik、React Helmet、React-Redux、React Router DOM、Dotenv、ESLint、Storybook、Framer Motion和React Bootstrap等库的使用方法和应用场景,帮助开发者提升开发效率和代码质量。
39 4
React开发需要了解的10个库
|
2月前
|
设计模式 存储 前端开发
React开发设计模式及原则概念问题之自定义Hooks的作用是什么,自定义Hooks设计时要遵循什么原则呢
React开发设计模式及原则概念问题之自定义Hooks的作用是什么,自定义Hooks设计时要遵循什么原则呢
|
13天前
|
前端开发 JavaScript 开发者
React 组件化开发最佳实践
【10月更文挑战第4天】React 组件化开发最佳实践
37 4
|
1月前
|
XML 移动开发 前端开发
使用duxapp开发 React Native App 事半功倍
对于Taro的壳子,或者原生React Native,都会存在 `android` `ios`这两个文件夹,而在duxapp中,这些文件夹的内容是自动生成的,那么对于需要在这些文件夹中修改的配置内容,例如包名、版本号、新架构开关等,都通过配置文件的方式配置了,而不需要需修改具体的文件
|
1月前
|
资源调度 JavaScript 前端开发
使用vite+react+ts+Ant Design开发后台管理项目(二)
使用vite+react+ts+Ant Design开发后台管理项目(二)
|
1月前
|
存储 前端开发 JavaScript
react 组件化
【9月更文挑战第2天】react 组件化
33 5
|
2月前
|
JavaScript 前端开发 安全
[译] 使用 TypeScript 开发 React Hooks
[译] 使用 TypeScript 开发 React Hooks
|
2月前
|
前端开发 JavaScript
React Server Component 使用问题之添加jsx的组件化能力,如何操作
React Server Component 使用问题之添加jsx的组件化能力,如何操作
|
2月前
|
开发者 自然语言处理 存储
语言不再是壁垒:掌握 JSF 国际化技巧,轻松构建多语言支持的 Web 应用
【8月更文挑战第31天】JavaServer Faces (JSF) 框架提供了强大的国际化 (I18N) 和本地化 (L10N) 支持,使开发者能轻松添加多语言功能。本文通过具体案例展示如何在 JSF 应用中实现多语言支持,包括创建项目、配置语言资源文件 (`messages_xx.properties`)、设置 `web.xml`、编写 Managed Bean (`LanguageBean`) 处理语言选择,以及使用 Facelets 页面 (`index.xhtml`) 显示多语言消息。通过这些步骤,你将学会如何配置 JSF 环境、编写语言资源文件,并实现动态语言切换。
37 0
|
2月前
|
开发者 Java
JSF EL 表达式:乘技术潮流之风,筑简洁开发之梦,触动开发者心弦的强大语言
【8月更文挑战第31天】JavaServer Faces (JSF) 的表达式语言 (EL) 是一种强大的工具,允许开发者在 JSF 页面和后台 bean 间进行简洁高效的数据绑定。本文介绍了 JSF EL 的基本概念及使用技巧,包括访问 bean 属性和方法、数据绑定、内置对象使用、条件判断和循环等,并分享了最佳实践建议,帮助提升开发效率和代码质量。
32 0