-
组件化定基本概念
-
组件化思想
分而治之的思想,简单来说就是讲一个复杂的大问题拆解成一个一个小的问题然后再解决每个小问题最后再放到整体中 -
什么是组件化开发
就是将一个页面拆分成若干个组件,每个组件都用于实现页面的一个功能块,每个组件又可以进行细分,组件的奔上又可以在多个地方复用,如下图: -
React组件化
按照不同的方式可以分成很多类组件:- 根据组件的定义方式,可以分为:函数组件(Functional Component )和类组件(Class Component);
- 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component )和有状态组件(Stateful Component);
- 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component); 按照数据逻辑和UI展示的分离又可分为:
- 函数组件、无状态组件、展示型组件主要关注UI的展示;
- 类组件、有状态组件、容器型组件主要关注数据逻辑; 还有很多组件的其他概念:比如异步组件、高阶组件等
-
-
类组件
- 类组件的定义有如下要求
- 组件的名称是大写字符开头(无论类组件还是函数组件)
- 类组件需要继承自 React.Component/React.PureComponent
- 类组件必须实现render函数
- 使用class定义一个组件:
- constructor是可选的,我们通常在constructor中初始化一些数据;
- this.state中维护的就是我们组件内部的数据;
- render() 方法是 class 组件中唯一必须实现的方法;
- 代码示例
import React, { Component } from 'react'; export default class App extends Component { constructor() { super(); this.state = { message: "你好啊,React" } } render() { return ( <div> <span>我是类组件</span> {/* alt + shift + f: 对代码进行格式化 */} <h2>{this.state.message}</h2> </div> ) } } - 类组件的定义有如下要求
-
函数组件
- 函数组件是使用function来进行定义的函数,只是这个函数会返回和类组件中render函数返回一样的内容。
- 函数组件的特点
- 没有生命周期,也会被更新并挂载,但是没有生命周期函数;
- 没有this(组件实例);
- 没有内部状态(state);
- 代码示例
/** * 函数式组件的特点: * 1.没有this对象 * 2.没有内部的状态 */ export default function App() { return ( <div> <span>我是function的组件: App组件</span> <h2>你好啊,土豆</h2> </div> ) } -
小知识点补充
-
render函数的返回值
- React元素(通过JSX创建)
- 数组或 fragments:使得render方法可以返回多个元素。(fragments:React.Fragment,如果不想返回数组可以将子元素用<React.Fragment></React.Fragment>包裹,简写形式:<></>)
- Portals(ReactDOM.createPortals(child, container)):可以渲染子节点到不同的 DOM 子树中,可以用于视觉上弹出的容器,如对话框、提示框等
- 字符串或数值类型:它们在 DOM 中会被渲染为文本节点
- 布尔类型或 null:什么都不渲染。
-
生命周期
有从创建到销毁的整个过程,这个过程称之为是生命周期;
生命周期图解:- 常用生命周期
- Constructor
如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数。
Constructor中只做两件事:- 通过给 this.state 赋值对象来初始化内部的state
- 为事件绑定实例(this)
代码示例:
constructor() { super(); this.state = { counter: 0, isShow: true } console.log("执行了组件的constructor方法"); } - componentDidMount
在组件挂载后(插入DOM树中)立即调用。
主要做的事情:- 依赖于DOM的操作可以在这里进行;
- 在此处发送网络请求就最好的地方;(官方建议)
- 可以在此处添加一些订阅(会在componentWillUnmount取消订阅);
代码示例:
componentDidMount() { console.log("执行了组件的componentDidMount方法"); } - componentDidUpdate
会在更新后会被立即调用,首次渲染不会执行此方法。
主要做的事情:- 当组件更新后,可以在此处对 DOM 进行操作;
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)。
代码示例:
componentDidUpdate(prevProps, prevState, snapshot) { console.log("执行了组件的componentDidUpdate方法"); } - componentWillUnmount
会在组件卸载及销毁之前直接调用。
主要做的事情:- 在此方法中执行必要的清理操作;例如,清除 timer,取消网络请求或清除在 componentDidMount()中创建的订阅等;
代码示例:
componentWillUnmount() { console.log("调用了Cpn的componentWillUnmount方法"); } - 在此方法中执行必要的清理操作;例如,清除 timer,取消网络请求或清除在 componentDidMount()中创建的订阅等;
- Constructor
- 不常用生命周期
- getDerivedStateFromProps:state 的值在任何时候都依赖于props时使用;该方法返回一个对象来更新state;
- getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置);
- shouldComponentUpdate:该生命周期函数很常用(性能优化的时候下面有讲)
- 常用生命周期
-
组件之间的嵌套
将一个大组件拆分成若干个小组件最后再组合在一起,最终形成我们的程序如下图: -
组件之间的通信
- 父组件传递子组件
- 父组件通过 属性=值 的形式来传递给子组件数据;
- 子组件通过 props 参数获取父组件传递过来的数据;
- 示例代码:
//类组件中的传递 class ChildCpn extends Component { constructor() { super(); } componentWillMount() { } componentDidMount() { console.log(this.props, "componentDidMount"); } render() { // console.log(this.props, "render"); const {name, age, height} = this.props; return ( <h2>子组件展示数据: {name + " " + age + " " + height}</h2> ) } } export default class App extends Component { render() { return ( <div> <ChildCpn name="why" age="18" height="1.88"/> <ChildCpn name="kobe" age="40" height="1.98"/> </div> ) } } //------函数组件中的传递------- function ChildCpn(props) { const { name, age, height } = props; return ( <h2>{name + age + height}</h2> ) } export default class App extends Component { render() { return ( <div> <ChildCpn name="why" age="18" height="1.88" /> <ChildCpn name="kobe" age="40" height="1.98" /> </div> ) } } - 子组件传递父组件
- 同样是通过props传递消息,只是让父组件给子组件传递一个回调函数,在子组件中调用这个函数即可
- 示例代码:
class CounterButton extends Component { render() { const {onClick} = this.props; return <button onClick={onClick}>+1</button> } } export default class App extends Component { constructor(props) { super(props); this.state = { counter: 0 } } render() { return ( <div> <h2>当前计数: {this.state.counter}</h2> <button onClick={e => this.increment()}>+</button> <CounterButton onClick={e => this.increment()} name="why"/> </div> ) } increment() { this.setState({ counter: this.state.counter + 1 }) } } - 数据验证参数propTypes
如果项目中默认继承了Flow(JavaScript的静态类型检查工具)或者TypeScript可以不用propTypes
基本用法如下示例代码:class ChildCpn2 extends Component { // es6中的class fields写法 static propTypes = { } static defaultProps = { } } ChildCpn.propTypes = { name: PropTypes.string.isRequired, age: PropTypes.number, height: PropTypes.number, names: PropTypes.array } ChildCpn.defaultProps = { name: "why", age: 30, height: 1.98, names: ["aaa", "bbb"] }
- 父组件传递子组件
-
Context
- 应用场景
- 数据需要在多个组件中进行共享
- 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。这个时候也可以使用
Context
- 相关Api
- React.createContext
- 创建一个需要共享的Context对象:
- 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context值;
- defaultValue是组件在顶层查找过程中没有找到对应的Provider,那么就使用默认值
- 示例代码:
// 创建Context对象 const UserContext = React.createContext({ nickname: "aaaa", level: -1 }) - Context.Provider(主要用在类组件)
- 每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅context 的变化:
- Provider 接收一个 value 属性,传递给消费组件;
- 一个 Provider 可以和多个消费组件有对应关系;
- 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
- 当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;
- 示例代码:
render() { return ( <div> <UserContext.Provider value={this.state}> <Profile /> </UserContext.Provider> </div> ) } - Class.contextType
- 挂载在 class 上的 contextType 属性会被重赋值为一个React.createContext() 创建的 Context 对象:
- 这能让你使用 this.context 来消费最近 Context 上的那个值;
- 你可以在任何生命周期中访问到它,包括 render 函数中;
- 示例代码:
// 创建Context对象 const UserContext = React.createContext({ nickname: "aaaa", level: -1 }) class ProfileHeader extends Component { render() { console.log(this.context); // jsx -> return ( <div> <h2>用户昵称: {this.context.nickname}</h2> <h2>用户等级: {this.context.level}</h2> </div> ) } } ProfileHeader.contextType = UserContext; - Context.Consumer(主要用在函数式组件)
- React组件也可以订阅到context变更。这能让你在函数式组件中完成订阅 context。
- 这里需要 函数作为子元素(function as child)这种做法;
- 这个函数接收当前的context值,返回一个React 节点
- 示例代码:
function ProfileHeader() { return ( <UserContext.Consumer> { value => { return ( <div> <h2>用户昵称: {value.nickname}</h2> <h2>用户等级: {value.level}</h2> </div> ) } } </UserContext.Consumer> ) } - 完整代码示例(包含嵌套context)
import React, { Component } from 'react'; // 创建Context对象 const UserContext = React.createContext({ nickname: "aaaa", level: -1 }) const ThemeContext = React.createContext({ color: "black" }) function ProfileHeader() { // jsx -> 嵌套的方式 return ( <UserContext.Consumer> { value => { return ( <ThemeContext.Consumer> { theme => { return ( <div> <h2 style={{color: theme.color}}>用户昵称: {value.nickname}</h2> <h2>用户等级: {value.level}</h2> <h2>颜色: {theme.color}</h2> </div> ) } } </ThemeContext.Consumer> ) } } </UserContext.Consumer> ) } function Profile(props) { return ( <div> <ProfileHeader /> <ul> <li>设置1</li> <li>设置2</li> <li>设置3</li> <li>设置4</li> </ul> </div> ) } export default class App extends Component { constructor(props) { super(props); this.state = { nickname: "kobe", level: 99 } } render() { return ( <div> <UserContext.Provider value={this.state}> <ThemeContext.Provider value={{ color: "red" }}> <Profile /> </ThemeContext.Provider> </UserContext.Provider> </div> ) } }
- React.createContext
- 应用场景
-
setState
setState使用原因
有些页面希望修改state之后页面可以重新刷新,setState方法调用后会执行render方法- 为什么能调用
setState方法(组件中并没有setState的方法实现) 原因很简单,setState方法是从Component/PureComponent中继承过来的。
源码示例:// prototype: prototype 属性允许您向对象添加属性和方法(js语法object.prototype.name=value) Component.prototype.setState = function(partialState, callback) { invariant( typeof partialState === 'object' || typeof partialState === 'function' || partialState == null, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.', ); this.updater.enqueueSetState(this, partialState, callback, 'setState'); }; setState异步更新上述代码看着像是count连续加了三遍,其实最后的结果是只加了1,应为function incrementMultiple() { const currentCount = this.state.count; this.setState({count: currentCount + 1}); this.setState({count: currentCount + 1}); this.setState({count: currentCount + 1}); }setState不会立马改变state的值辉县放到一个队列中,等到了一定的时机然后再合并state然后在引发更新操作如下代码:相当于:function updateName() { this.setState({FirstName: 'Morgan'}); this.setState({LastName: 'Cheng'}); }为什么要做异步更新,原因很简单:function updateName() { this.setState({FirstName: 'Morgan', LastName: 'Cheng'}); }
节省性能
setState引发的组件更新过程,包含生命周期函数有四个。- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
每一次setState调用都走一圈生命周期,光是想一想也会觉得会带来性能的问题,其实这四个函数都是纯函数,性能应该还好,但是render函数返回的结果会拿去做Virtual DOM比较和更新DOM树,这个就比较费时间。
目前React会将setState的效果放在队列中,积攒着一次引发更新过程,为的就是把Virtual DOM和DOM树操作降到最小,用于提高性能。
如何获取异步的结果:
setState的回调:setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;格式如下:setState(partialState, callback)
this.setState({ message: "你好啊,李银河" }, () => { console.log(this.state.message); })componentDidUpdate中获取
setState也不一定是异步,例如componentDidUpdate() { // 方式二: 获取异步更新的state console.log(this.state.message); }- 在setTimeout中的更新:
changeText() { // 情况一: 将setState放入到定时器中 setTimeout(() => { this.setState({ message: "你好啊,李银河" }) console.log(this.state.message); }, 0); } - 原生DOM事件:
document.getElementById("btn").addEventListener("click", (e) => { this.setState({ message: "你好啊,李银河" }) console.log(this.state.message); })
- 数据合并
如下代码:通过setState去修改message,是不会对name产生影响的;constructor(props) { super(props); this.state = { message: "Hello World", name: "coderwhy" } } changeText() { this.setState({ message: "你好啊,李银河" }); // Object.assign({}, this.state, {message: "你好啊,李银河"}) }冲源码可以看出来最后一步是做了合并的操作的
- React更新机制
- 对比不同类型的元素
- 当节点为不同的元素,React会拆卸原有的树,并且建立起新的树:
- 当一个元素从
<a>变成<img>,从<Article>变成<Comment>,或从<Button>变成<div>都会触发一个完整的重建流程;当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行componentWillUnmount()方法; - 当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行
componentWillMount()方法,紧接着componentDidMount()方法;
- 对比同一类型的元素
当比对两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。- 组件会保持不变,React会更新该组件的props,并且调用
componentWillReceiveProps()和componentWillUpdate()方法; - 下一步,调用
render()方法,diff 算法将在之前的结果以及新的结果中进行递归;
- 组件会保持不变,React会更新该组件的props,并且调用
- 对子节点进行递归
在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个mutation。
情况1:
前面两个比较是完全相同的,所以不会产生mutation;最后一个比较,产生一个mutation,将其插入到新的DOM树中即可;
情况2
React会对每一个子元素产生一个mutation,而不是保持<li>星际穿越</li>和<li>盗梦空间</li>的不变;
- 对比不同类型的元素
-
keys的优化
当子元素(这里的li)拥有 key 时,React使用key来匹配原有树上的子元素以及最新树上的子元素:在下面这种场景下,key为111和222的元素仅仅进行位移,不需要进行任何的修改;将key为333的元素插入到最前面的位置即可;所以上述的情况二就可以加上key值,结果就是只会针对插入的元素操作其他两个元素虽然位置有变化,但是不需要做任何修改
key值注意点
1. key应该是唯一的;
2. key不要使用随机数(随机数在下一次render时,会重新生成一个数字);
3. 使用index作为key,对性能是没有优化的;(如果用index,当元素位置下移对应的key值也变了所以还是会被修改) -
shouldComponentUpdate
只要是修改了App中的数据,所有的组件都需要重新render,进行diff算法,性能必然是很低的:shouldComponentUpdate方法中可以返回false不刷新render,shouldComponentUpdate会接受两个参数nextProps 修改之后,最新的props属性,nextState 修改之后,最新的state属性开发者可以通过判断前后的状态值判断需不需要更新render -
PureComponent
类继承PureComponent之后开发者可以不实现shouldComponentUpdate,React会帮你做一层浅比较 -
shallowEqual方法
源码及注释 -
高阶组件memo
Memo内部和PureComponent一样使用Object.is用于前对比,如果传入的props内存地址不变的话,那就不会渲染了(或者说复用最近的一次渲染)。具体用法:import React, { PureComponent, memo } from 'react'; // Header const MemoHeader = memo(function Header() { console.log("Header被调用"); return <h2>我是Header组件</h2> }) -
不可变数据的力量
上文可以知道继承PureComponent之后每次setState会进行一次浅比较如果你的state里面方林一个对象,而你仅仅通过this.state.object.key = value的方式修改对象的值那么对象的引用地址是不会改变的,所以即使state变了并且setState了还是不会执行render方法 -
事件总线(EventBus)
主要用在跨组件之间的事件传递
具体用法:- yarn add events
- 创建EventEmitter对象:eventBus对象;
- 发出事件:eventBus.emit("事件名称", 参数列表);
- 监听事件:eventBus.addListener("事件名称", 监听函数);
- 移除事件:eventBus.removeListener("事件名称", 监听函数);
-
ref使用
- 传入字符串,使用时通过 this.refs.传入的字符串格式获取对应的元素;
- 传入一个对象,对象是通过 React.createRef() 方式创建出来的,使用时获取到创建的对象其中有一个current属性就是对应的元素;
- 传入一个函数该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;使用时,直接拿到之前保存的元素对象即可;
- 传入字符串,使用时通过 this.refs.传入的字符串格式获取对应的元素;
-
-
受控组件
简单来说就是一些表单元素的值通过存到state中然后再复制的形式,就是受控组件如下代码
constructor(props) { super(props); this.state = { username: "" } } render() { return ( <div> <form onSubmit={e => this.handleSubmit(e)}> <label htmlFor="username"> 用户: {/* 受控组件 */} <input type="text" id="username" onChange={e => this.handleChange(e)} value={this.state.username}/> </label> <input type="submit" value="提交"/> </form> </div> ) } handleSubmit(event) { event.preventDefault(); console.log(this.state.username); } -
非受控组件
表单元素通过使用ref来从DOM节点中获取表单数据(React多数情况下推荐使用受控组件)
constructor(props) { super(props); this.usernameRef = createRef(); } render() { return ( <div> <form onSubmit={e => this.handleSubmit(e)}> <label htmlFor="username"> 用户: <input type="text" id="username" ref={this.usernameRef}/> </label> <input type="submit" value="提交"/> </form> </div> ) } handleSubmit(event) { event.preventDefault(); console.log(this.usernameRef.current.value); } -
高阶函数
官方的定义:高阶组件是参数为组件,返回值为新组件的函数;
- 调用:
const EnhanceComponent = enhanceComponent2(App); - 定义:
function enhanceComponent(WrappedComponent) { class NewComponent extends PureComponent { render() { return <WrappedComponent {...this.props}/> } } NewComponent.displayName = "Kobe"; return NewComponent; } function enhanceComponent2(WrappedComponent) { function NewComponent(props) { return <WrappedComponent {...props}/> } //组件的名称都可以通过displayName来修改 NewComponent.displayName = "Kobe"; return NewComponent; }- 高级组件作用:
- props的增强
// 定义一个高阶组件 function enhanceRegionProps(WrappedComponent) { return props => { return <WrappedComponent {...props} region="中国"/> } } class Home extends PureComponent { render() { return <h2>Home: {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}</h2> } } class About extends PureComponent { render() { return <h2>About: {`昵称: ${this.props.nickname} 等级: ${this.props.level} 区域: ${this.props.region}`}</h2> } } const EnhanceHome = enhanceRegionProps(Home); const EnhanceAbout = enhanceRegionProps(About); class App extends PureComponent { render() { return ( <div> App <EnhanceHome nickname="coderwhy" level={90}/> <EnhanceAbout nickname="kobe" level={99}/> </div> ) } }- 渲染判断鉴权(可用来写根容器(父类))
import React, { PureComponent } from 'react'; class LoginPage extends PureComponent { render() { return <h2>LoginPage</h2> } } function withAuth(WrappedComponent) { const NewCpn = props => { const {isLogin} = props; if (isLogin) { return <WrappedComponent {...props}/> } else { return <LoginPage/> } } NewCpn.displayName = "AuthCpn" return NewCpn; } // 购物车组件 class CartPage extends PureComponent { render() { return <h2>CartPage</h2> } } const AuthCartPage = withAuth(CartPage); export default class App extends PureComponent { render() { return ( <div> <AuthCartPage isLogin={true}/> </div> ) } }- 生命周期劫持
import React, { PureComponent } from 'react'; function withRenderTime(WrappedComponent) { return class extends PureComponent { // 即将渲染获取一个时间 beginTime UNSAFE_componentWillMount() { this.beginTime = Date.now(); } // 渲染完成再获取一个时间 endTime componentDidMount() { this.endTime = Date.now(); const interval = this.endTime - this.beginTime; console.log(`${WrappedComponent.name}渲染时间: ${interval}`) } render() { return <WrappedComponent {...this.props}/> } } } class Home extends PureComponent { render() { return <h2>Home</h2> } } class About extends PureComponent { render() { return <h2>About</h2> } } const TimeHome = withRenderTime(Home); const TimeAbout = withRenderTime(About); export default class App extends PureComponent { render() { return ( <div> <TimeHome /> <TimeAbout /> </div> ) } }
- 调用:
-
Portals的使用
某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到id为root的DOM元 素上的)。这个时候可以使用
Portals写法如下ReactDOM.createPortal(child,container)- 第一个参数(child)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment;
- 第二个参数(container)是一个 DOM 元素;
自定义model案列(猜测antd的model组件也是使用
Portals创建了一个独立于当前挂载到的DOM元素中的组件):
import React, { PureComponent } from 'react'; import ReactDOM from 'react-dom'; class Modal extends PureComponent { render() { return ReactDOM.createPortal( this.props.children, document.getElementById("modal") ) } } ReactDOM.createPortal(child,container) class Home extends PureComponent { render() { return ( <div> <h2>Home</h2> <Modal> <h2>Title</h2> </Modal> </div> ) } } export default class App extends PureComponent { render() { return ( <div> <Home/> </div> ) } } -
fragment的使用
如果单纯的先返回两个按钮,但是react又只支持输出一个元素,然后又不想使用div包裹就可以使用
fragment
完整写法:import React, { Fragment } from 'react'; <Fragment key={item.name}> <div>{item.name}</div> <p>{item.age}</p> <hr/> </Fragment>简写形式
<> <div>{item.name}</div> <p>{item.age}</p> <hr/> </> -
StrictMode的使用
StrictMode 是一个用来突出显示应用程序中潜在问题的工具。
用法:<StrictMode> <Home/> </StrictMode>严格模式检查的是什么
- 识别不安全的生命周期:
- 使用过时的ref API
- 使用废弃的findDOMNode方法
- 检查意外的副作用 如:constructor是否会被调用两次;
- 检测过时的context API