- React快速入门(二)组件与函数
- React脚手架
- React组件化开发
- setState原理
- React更新机制
- 使用ref
- 受控/非受控组件
- 高阶函数
- Portals/fragment/StrictMode
React快速入门(二)组件与函数
React脚手架
- 脚手架让项目从搭建到开发,再到部署,整个流程变得快速和便捷
- 创建:
create-react-app 项目名称(全小写)
cd 项目名称
yarn start
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函数的返回值
- 当render被调用时,它会检查this.props和 this.state的变化并返回以下类型之—:
- React元素:
- 通常通过JSX创建
- 例如:< div />会被React渲染为DOM节点,< MyComponent/>会被React渲染为自定义组件
- 无论是< div/>还是< MyComponent />均为 React元素
- 数组或 fragments:使得render方法可以返回多个元素
- Portals:可以渲染子节点到不同的DOM子树中
- 字符串或数值类型:它们在 DOM中会被渲染为文本节点
- 布尔类型或 null:什么都不渲染
函数组件
- 函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容。
- 函数组件有自己的特点:
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数
- this关键字不能指向组件实例(因为没有组件实例);
- 没有内部状态(state) ;
生命周期
-
Constructor
- 如果不初始化state或不进行方法绑定,则不需要为React组件实现构造函数。
- constructor中通常只做两件事情:
- 通过给this.state赋值对象来初始化内部的state
- 为事件绑定实例(this)
-
componentDidMount
- componentDidMount()会在组件挂载后(插入DOM树中)立即调用。
- 依赖于DOM的操作可以在这里进行
- 在此处发送网络请求最好的地方(官方建议)
- 可以在此处添加一些订阅(会在componentWillUnmount取消订阅)
-
componentDidUpdate
- componentDidUpdate()会在更新后会被立即调用,首次渲染不会执行此方法。
- 当组件更新后,可以在此处对 DOM进行操作;如果对更新前后的props进行了比较,也可以选择在此处进行网络请求
-
componentWillUnmount
- componentWillUnmount()会在组件卸载及销毁之前直接调用
- 在此方法中执行必要的清理操作;例如,清除 timer,取消网络请求或清除在componentDidMount()中创建的订阅等;
-
getDerivedStateFromProps: state的值在任何时候都依赖于props时使用;该方法返回—个对象来更新state;
-
getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的—些信息(比如说滚动位置);
-
shouldComponentUpdate:该生命周期函数很常用,做性能优化
React组件间通信
- 父子组件
- 父组件通过属性=值的形式来传递给子组件数据
- 子组件通过props参数获取父组件传递过来的数据
//父组件
<MainBanner banners={banners} title="轮播图"/>//子组件
import React, { Component } from 'react'
import PropTypes from "prop-types"export class MainBanner extends Component {constructor(props) {super(props)this.state = {}}render() {const { title, banners } = this.propsreturn (<div className='banner'>...</div>)}
}// MainBanner传入的props类型进行验证
MainBanner.propTypes = {banners: PropTypes.array.isRequired,title: PropTypes.string
}// MainBanner传入的props的默认值
MainBanner.defaultProps = {banners: [],title: "默认标题"
}export default MainBanner
- 子父组件
- 通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可
//父组件
export class App extends Component {constructor() {super()this.state = {counter: 100}}changeCounter(count) {this.setState({ counter: this.state.counter + count })}render() {const { counter } = this.statereturn (<div><h2>当前计数: {counter}</h2><AddCounter addClick={(count) => this.changeCounter(count)}/><SubCounter subClick={(count) => this.changeCounter(count)}/></div>)}
}
//子组件
export class AddCounter extends Component {addCount(count) {this.props.addClick(count)}render() {return (<div><button onClick={e => this.addCount(1)}>+1</button><button onClick={e => this.addCount(5)}>+5</button><button onClick={e => this.addCount(10)}>+10</button></div>)}
}
React中的插槽(slot)实现
-
组件的children子元素
-
props属性传递React元素
//父组件
export class App extends Component {render() {const btn = <button>按钮2</button>return (<div>{/* 1.使用children实现插槽 */}<NavBar><button>按钮</button><h2>哈哈哈</h2><i>斜体文本</i></NavBar>{/* 2.使用props实现插槽 */}<NavBarTwo leftSlot={btn}centerSlot={<h2>呵呵呵</h2>}rightSlot={<i>斜体2</i>}/></div>)}
}
//子组件一 -- 使用children 注意一个和多个时的children类型
export class NavBar extends Component {render() {const { children } = this.propsconsole.log(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>)}
}
//子组件二 -- 使用props
export class NavBarTwo extends Component {render() {const { leftSlot, centerSlot, rightSlot } = this.propsreturn (<div className='nav-bar'><div className="left">{leftSlot}</div><div className="center">{centerSlot}</div><div className="right">{rightSlot}</div></div>)}
}
Context应用场景
-
如果层级很多的话,一层层传递是非常麻烦,并且代码是非常冗余的:
- React提供了—个APl: Context;
- Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递props;
- Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言;
-
Context的使用
- React.createContext
- 创建一个需要共享的Context对象;如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的Provider中读取到当前的context值(就近原则)
//defaultValue
const ThemeContext = React.createContext({ color: "blue", size: 10 })
const UserContext = React.createContext()
- Context.Provider
- Provider接收一个value属性,传递给消费组件;一个Provider可以和多个消费组件有对应关系;多个Provider也可以嵌套使用,里层的会覆盖外层的数据;当Provider的value值发生变化时,它内部的所有消费组件都会重新渲染
<UserContext.Provider value={{nickname: "kobe", age: 30}}><ThemeContext.Provider value={{color: "red", size: "30"}}>
<Home {...info}/></ThemeContext.Provider>
</UserContext.Provider>
- Class.contextType
- 挂裁在class 上的contextType属性会被重赋值为一个ReactcreateContext()创建的Context对象,能使用this.context来消费最近Context 上的那个值
HomeInfo.contextType = ThemeContext
- Context.Consumer
- 使用:当使用value的组件是一个函数式组件时;当组件中需要使用多个Context时
export class HomeInfo extends Component {render() {return (<div><h2>HomeInfo: {this.context.color}</h2><UserContext.Consumer>{value => {return <h2>Info User: {value.nickname}</h2>}}</UserContext.Consumer></div>)}
}
- 全局事件总线event-bus,eventBus.emit,eventBus.on,eventBus.off
setState原理
- setState方法是从Component中继承过来的监听数据变化的方法
- setState的三种写法:
changeText() {// 1.setState基本使用this.setState({message: "你好啊, 李银河"})// 2.setState可以传入一个回调函数// 好处一: 可以在回调函数中编写新的state的逻辑// 好处二: 当前的回调函数会将之前的state和props传递进来this.setState((state, props) => {// 1.编写一些对新的state处理逻辑// 2.可以获取之前的state和props值console.log(this.state.message, this.props)return {message: "你好啊, 李银河"}})// 3.setState在React的事件处理中是一个异步调用// 如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码// 那么可以在setState中传入第二个参数: callbackthis.setState({ message: "你好啊, 李银河" }, () => {console.log("++++++:", this.state.message)})console.log("------:", this.state.message)}
- setState更新是异步的,可以显著的提升性能
- 如果每次调用setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新
- 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步
- state和props不能保持—致性,会在开发中产生很多的问题
React更新机制
- 发生改变时React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI
- React对这个算法进行了优化,将其优化成了O(n):
- 同层节点之间相互比较,不会跨节点比较;
- 不同类型的节点,产生不同的树结构;
- 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定
- shouldComponentUpdate
- 该方法有两个参数:
- 参数一: nextProps修改之后,最新的props属性
- 参数二: nextState修改之后,最新的state属性
- 该方法返回值是一个boolean类型:
- 返回值为true,那么就需要调用render方法
- 返回值为false,那么就不需要调用render方法
- 默认返回的是true,也就是只要state发生改变,就会调用render方法;
- 该方法有两个参数:
- shouldComponentUpdate -> PureComponent(类组件,内部自动对状态进行判断)
- memo(函数组件);
const Profile = memo(function(props) {}
- memo(函数组件);
使用ref
- 传入字符串
- 使用时通过this.refs.传入的字符串格式获取对应的元素
- 传入一个对象
- 对象是通过React.createRef()方式创建出来的;使用时获取到创建的对象其中有一个current属性就是对应的元素;
- 传入一个函数
- 该函数会在DOM被挂载时进行回调,这个函数会传入一个元素对象,我们可以自己保存;使用时,直接拿到之前保存的元素对象即可
export class App extends PureComponent {constructor() {super()this.state = {}this.titleRef = createRef()this.titleEl = null}getNativeDOM() {// 1.方式一: 在React元素上绑定一个ref字符串console.log(this.refs.why)// 2.方式二: 提前创建好ref对象, createRef(), 将创建出来的对象绑定到元素console.log(this.titleRef.current)// 3.方式三: 传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素传入console.log(this.titleEl)}render() {return (<div><h2 ref="why">Hello World</h2><h2 ref={this.titleRef}>你好啊,李银河</h2><h2 ref={el => this.titleEl = el}>你好啊, 师姐</h2><button onClick={e => this.getNativeDOM()}>获取DOM</button></div>)}
}
- ref 的值根据节点的类型而有所不同:
- 当ref属性用于HTML元素时,构造函数中使用React.createRef()创建的 ref 接收底层DOM元素作为其current属性
- 当ref属性用于自定义 class组件时,ref对象接收组件的挂载实例作为其current属性
- 不能在函数组件上使用ref 属性,因为他们没有实例,使用React.forwardRef
const HelloWorld = forwardRef(function(props, ref) {}
受控/非受控组件
受控组件
- 在React中,可变状态(mutable state)通常保存在组件的state属性中,并且只能通过使用setState()来更新。
- 我们将两者结合起来,使React的state成为“唯—数据源”
- 渲染表单的React 组件还控制着用户输入过程中表单发生的操作
- 被React 以这种方式控制取值的表单输入元素就叫做“受控组件”
- 基础:
export class App extends PureComponent {constructor() {super()this.state = {username: "coderwhy"}}inputChange(event) {console.log("inputChange:", event.target.value)this.setState({ username: event.target.value })}render() {const { username } = this.statereturn (<div>{/* 受控组件 */}<input type="checkbox" value={username} onChange={e => this.inputChange(e)}/>{/* 非受控组件 */}<input type="text" /><h2>username: {username}</h2></div>)}
}
- 进阶
export class App extends PureComponent {constructor() {super()this.state = {username: "",password: "",isAgree: false,hobbies: [{ value: "sing", text: "唱", isChecked: false },{ value: "dance", text: "跳", isChecked: false },{ value: "rap", text: "rap", isChecked: false }],fruit: ["orange"]}}handleSubmitClick(event) {// 1.阻止默认的行为event.preventDefault()// 2.获取到所有的表单数据, 对数据进行处理console.log("获取所有的输入内容")console.log(this.state.username, this.state.password)const hobbies = this.state.hobbies.filter(item => item.isChecked).map(item => item.value)console.log("获取爱好: ", hobbies)// 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios)}handleInputChange(event) {this.setState({[event.target.name]: event.target.value})}handleAgreeChange(event) {this.setState({ isAgree: event.target.checked })}handleHobbiesChange(event, index) {const hobbies = [...this.state.hobbies]hobbies[index].isChecked = event.target.checkedthis.setState({ hobbies })}handleFruitChange(event) {const options = Array.from(event.target.selectedOptions)const values = options.map(item => item.value)this.setState({ fruit: values })// 额外补充: Array.from(可迭代对象)// Array.from(arguments)const values2 = Array.from(event.target.selectedOptions, item => item.value)console.log(values2)}render() {const { username, password, isAgree, hobbies, fruit } = this.statereturn (<div><form onSubmit={e => this.handleSubmitClick(e)}>{/* 1.用户名和密码 */}<div><label htmlFor="username">用户: <input id='username' type="text" name='username' value={username} onChange={e => this.handleInputChange(e)}/></label><label htmlFor="password">密码: <input id='password' type="password" name='password' value={password} onChange={e => this.handleInputChange(e)}/></label></div>{/* 2.checkbox单选 */}<label htmlFor="agree"><input id='agree' type="checkbox" checked={isAgree} onChange={e => this.handleAgreeChange(e)}/>同意协议</label>{/* 3.checkbox多选 */}<div>您的爱好:{hobbies.map((item, index) => {return (<label htmlFor={item.value} key={item.value}><input type="checkbox"id={item.value} checked={item.isChecked}onChange={e => this.handleHobbiesChange(e, index)}/><span>{item.text}</span></label>)})}</div>{/* 4.select */}<select value={fruit} onChange={e => this.handleFruitChange(e)} multiple><option value="apple">苹果</option><option value="orange">橘子</option><option value="banana">香蕉</option></select><div><button type='submit'>注册</button></div></form></div>)}
}
非受控组件
- 使用非受控组件,表单数据交由DOM节点来处理,使用ref来获取,使用defaultValue来设置默认值
高阶函数
- 满足以下条件之一:接受一个或多个函数做为输入,输出一个函数
- 高阶组件(HOC)官方定义:高阶组件是参数为组件,返回值为新组件的函数
// 定义一个高阶组件
function hoc(Cpn) {// 1.定义类组件class NewCpn extends PureComponent {render() {return <Cpn name="why"/>}}return NewCpn// 定义函数组件// function NewCpn2(props) {// }// return NewCpn2
}class HelloWorld extends PureComponent {render() {return <h1>Hello World</h1>}
}const HelloWorldHOC = hoc(HelloWorld)export class App extends PureComponent {render() {return (<div><HelloWorldHOC/></div>)}
}
- 应用:装饰器模式,用于增强现有组件的功能
- props的增强–给一些需要特殊数据的组件, 注入props
- 利用高阶组件来共享Context
- 渲染判断鉴权
- 生命周期劫持
-
早期的React有提供组件之间的一种复用方式是mixin,目前已经不再建议使用:
- Mixin可能会相互依赖,相互耦合,不利于代码维护
- 不同的Mixin中的方法可能会相互冲突
- Mixin非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
-
HOC也有自己的一些缺陷:
- HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难
- HOC可以劫持props,在不遵守约定的情况下也可能造成冲突
Portals/fragment/StrictMode
- Portal(类似Vue3中Teleport)提供了一种将子节点渲染到存在于父组件以外的DOM节点的优秀的方案:
- 第一个参数(child)是任何可渲染的React子元素,例如一个元素,字符串或fragment
- 第二个参数(container)是一个DOM元素
{createPortal(<h2>App H2</h2>, document.querySelector("#why")) }
- 我们希望可以不渲染这样一个div则使用Fragment(类似Vue中的telemplate)
- Fragment 允许将子列表分组,而无需向DOM添加额外节点
- 如果我们需要在Fragment中添加key,那么就不能使用短语法
<></>
<Fragment><h2>{item.title}</h2><p>{item.content}</p>
</Fragment>
- StrictMode是一个用来突出显示应用程序中潜在问题的工具:
- 与Fragment一样,StrictMode不会渲染任何可见的Ul
- 它为其后代元素触发额外的检查和警告;
- 严格模式检查仅在开发模式下运行;它们不会影响生产构建
<StrictMode><Home/>
</StrictMode>
- 识别不安全的生命周期
- 使用过时的ref APl
- 检查意外的副作用
- 这个组件的constructor会被调用两次
- 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用
- 在生产环境中,是不会被调用两次的
- 使用废弃的findDOMNode方法
- 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了
- 检测过时的context APl
- 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context的