组件化思想的应用
尽可能的将页面拆分成一个个小的、可复用的组件。这样让我们的代码更加方便组织和管理,并且扩展性也更强。
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
才会使用,因此即使你给Provider
的value
传入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 函数中接收的
state
和props
都保证为最新。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>