内容简介:本文是『horseshoe·React专题』系列文章之一,后续会有更多专题推出来我的来我的个人博客 获得无与伦比的阅读体验
本文是『horseshoe·React专题』系列文章之一,后续会有更多专题推出
来我的 GitHub repo 阅读完整的专题文章
来我的个人博客 获得无与伦比的阅读体验
刀耕火种时期的前端,HTML描述页面结构,CSS描述样式,JavaScript描述功能。它们彼此是分离的。
然而这种方式却满足不了开发者对代码复用的需求。
近几年各大前端框架做了很多探索,其中组件化就是最璀璨的成果之一。
一个组件就是一个功能模块,所有的前端元素都封装在组件内部,对外只暴露有限的接口。这样开发者拿来就能用,通过接口与组件交互而不必知道组件的内部细节。
而React是前端框架里面组件化思想贯彻的最彻底的。
class组件和函数组件
在React中,构造一个组件既可以用class类,也可以仅用一个普通函数。
两者有什么区别呢?
class类不仅允许内部状态的存在,还有完整的生命周期钩子。
这让一个组件变的有生命力而且可以管理。
代价就是它的性能稍逊。
还有一点,class类组件必须继承React内置的组件类。因为那些生命周期钩子都是继承自React内置的组件类。
那你说,我不用生命周期钩子,可不可以不继承呢?
不可以,因为你必须要用生命周期钩子。
在一个class类组件中,render方法是必须的,没有了它就不可能返回UI,也就不能称其为一个组件。而render方法就是生命周期钩子之一。
import React, { Component } from 'react'; class App extends Component { state = { star: 1 }; render() { return ( <div>{this.state.star}</div> ); } } export default App; 复制代码
函数组件没有办法实例化,除了一些逻辑判断之外,它的功能只是返回UI。
所以函数组件常常被称为无状态组件。
然而这正是React强大之处,它构建组件可以如此随意,只需要一个普通函数返回一段JSX元素。在React中,有的时候函数和组件的界限会非常模糊。
有一种 设计模式 把React组件分为容器组件和展示组件,容器组件管理数据、状态和业务逻辑,展示组件仅仅负责接收props展示UI。
所以函数组件的使命肯定是做好展示组件咯。
import React from 'react'; function App(props) { return ( <div>{props.star}</div> ); } export default App; 复制代码
那么在React中函数和组件的区别到底在哪?
组件必须返回一段JSX元素,class类组件和函数组件都一样。
Component和PureComponent
前面说到class类组件有完整的生命周期钩子。这些生命周期钩子是从哪来的呢?毕竟class类组件就是原生的class类写法。
其实React内置了一个 Component
类,生命周期钩子都是从它这里来的,麻烦的地方就是每次都要继承。
PureComponent
类又是干嘛用的?
凭名字猜测,它是一个纯纯的组件类。
怎么个纯法子?
它自动帮开发者做了一些优化工作,使得组件看起来更加纯粹。
而组件性能优化的主要手段就是通过 shouldComponentUpdate
生命周期钩子。
我们来看看它是如何自动优化的。
从下面的例子看,React专门有一个方法来判断组件该不该更新。如果 typeof instance.shouldComponentUpdate === 'function'
,那这就是一个继承了 Component
类的组件,直接执行 shouldComponentUpdate
,返回true则更新,返回false则不更新。
如果 ctor.prototype.isPureReactComponent
,那这就是一个继承了 PureComponent
类的组件,这时React会将 oldProps
和 newProps
做一层浅比较,同时将 oldState
和 newState
做一层浅比较,只要有一个浅比较不相等,则返回true更新,否则返回false不更新。
function checkShouldComponentUpdate( workInProgress, oldProps, newProps, oldState, newState, newContext, ) { const instance = workInProgress.stateNode; const ctor = workInProgress.type; if (typeof instance.shouldComponentUpdate === 'function') { startPhaseTimer(workInProgress, 'shouldComponentUpdate'); const shouldUpdate = instance.shouldComponentUpdate( newProps, newState, newContext, ); stopPhaseTimer(); return shouldUpdate; } if (ctor.prototype && ctor.prototype.isPureReactComponent) { return ( !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) ); } return true; } 复制代码
shallowEqual
又干了什么呢?
- 首先用Object.is判断是否相等(React自己写的polyfill)。
- 如果Object.is判断不相等,再作引用类型的比较,比较属性key的长度,比较属性key的一一对应,比较属性value的引用。
总的来说,它只做了一层比较,所以才叫做浅比较。
聪明的你肯定要问了:为什么不递归呢(并发射一个傲娇脸)?
因为递归的深比较非常耗费性能。 PureComponent
类只是帮开发者适度优化性能,它还是要找到成本与收益的平衡点的。
PureComponent
类有其正确打开方式
例如像数组这样的数据结构,在不改变引用的情况下,使用数组的方法操作数组,然后再setState该数组,组件是不会更新的。
你说不对呀,老数组和新数组虽然是同一个引用,但是长度不一样了,浅比较是能识别出来的呀。
我们以上面的例子来看,pop方法会修改老数组,所以此时老数组和新数组是一模一样的,引用一样,长度一样。
React拿着这两份数据做浅比较,肯定返回true。
import React, { Component } from 'react'; class App extends Component { state = { list: [1, 2, 3], }; render() { const { list } = this.state; return ( <div> <button onClick={this.handleDelete}>Delete</button> <ul>{list.map(n => <li key={n}>{n}</li>)}</ul> </div> ); } handleDelete = () => { this.state.list.pop(); this.setState((prevState) => ({ list: prevState.list })); } } export default App; 复制代码
这就是会误导开发者的地方。
正确的做法是永远不修改原数据,生成新数据时依赖于原数据的浅拷贝,避免新数据和老数据指向同一个引用。
import React, { Component } from 'react'; class App extends Component { state = { list: [1, 2, 3], }; render() { const { list } = this.state; return ( <div> <button onClick={this.handleDelete}>Delete</button> <ul>{list.map(n => <li key={n}>{n}</li>)}</ul> </div> ); } handleDelete = () => { this.setState((prevState) => ({ list: [...prevState.list].pop() })); } } export default App; 复制代码
当然还有一种情况,采用传入时定义的方式给子组件传递props。
结果就是子组件做props的浅比较时永远返回false,因为每次的值都是重新定义的,绝非同一个引用。
那 PureComponent
类就失去它的意义了。
import React from 'react'; function App() { return ( <Child method={value => console.log(value)} /> ); } export default App; 复制代码
总之,要想使用 PureComponent
类,得时刻盯紧引用数据类型。
在继承了 PureComponent
类的组件里写 shouldComponentUpdate
生命周期钩子会怎么样?
小伙子果然骨骼清奇。
原则上,React并不推荐这种写法,并且在开发环境下会打印一个警告。
你要么偷个懒让React帮你做优化,要么自己做优化,这么干不是赤裸裸的挑衅么!
不过React早就预测到你会这么干的,所以它只能随你的脾气,只要组件里定义了 shouldComponentUpdate
生命周期钩子, PureComponent
类的自动优化就不再起作用了。
组件复用
话说代码复用还真不是因为开发者懒。
假如一个蓝色卡片组件,分别有十个地方要用到。而且这个开发者异常勤奋,把这段代码小心翼翼的复制到这十个地方。看起来大功告成了。但是有一天,产品经理说:“我希望卡片背景换成柠檬色,再删减掉一些信息。”
那么问题来了:该开发者是应该砍死产品经理还是应该考虑代码复用?
React世界中一切都是组件,组件思想的根本诉求又是什么呢?当然是代码复用。一个组件就好像一个集装箱,我不用知道里面装的什么货物,我只用知道它能上我的货轮。
集装箱是商业世界的伟大发明,组件也是前端世界的伟大发明。
React的代码复用都是以组件为单位的。
组件四海为家
最普通的组件复用的方式就是直接使用组件。
一个 <Card />
组件,我可以将它放在任何地方。它对外可以提供一些接口,以保证填充所在上下文要求的内容。开发者可以很方便的一处修改,处处生效。
高阶组件
首先我们回忆一下,什么是高阶函数?
定义非常简单:一个函数,它的参数是函数,或者它的返回值是函数。
基本就是鸡吃鸡或者鸡生鸡的意思。
那么高阶组件就好理解了。它的定义是:一个函数,传入一个组件,并且返回一个组件。
区别就是,高阶函数只要满足一个条件,高阶组件要满足两个条件。
高阶组件既是函数,也是组件,因为在React中一个函数只要返回JSX元素就是组件。但这个组件有点特殊,因为它不是用来堆砌UI的,而是一个组件装饰工厂。
话不多说,我们直接来看一下 react-redux
中的 connect
方法(极度简化的伪代码)。
connect
方法是用来将redux中的state作为props传给组件的。这就是一个典型的高阶组件,当然它还在外面套了一层函数,为了传值。
不使用 connect
方法的组件是这样导出的: export default App;
。
使用 connect
方法的组件是这样导出的: export default connect(mapState, mapDispatch)(App);
。
开发者首先传参执行connect,然后再传组件执行返回的函数,导出的组件就多了一些属性。
可以看出,这种类型的高阶组件主要是为了给目标组件传递一些props。
import React, { Component } from 'react'; function connect(mapStateToProps, mapDispatchToProps) { return function wrapWithConnect(WrappedComponent) { return class Connect extends Component { render() { return ( <WrappedComponent {...mapStateToProps} {...mapDispatchToProps} /> ); } } } } export default connect; 复制代码
说高阶组件是一个组件装饰工厂是有道理的,因为我们可以用装饰器的写法来部署高阶组件。
下面是 connect
方法的装饰器写法。
import React, { Component } from 'react'; @connect(mapStateToProps, mapDispatchToProps); class App extends Component { render() { return ( <div>app</div> ); } } export default App; 复制代码
另一种类型的高阶组件更加巧妙,它使用到了继承的特性。
返回的组件可以获得传入组件的所有属性和方法,如果复用组件需要对传入组件做一些侵入性比较强的改动,那么这种方式非常具有灵活性。
import React from 'react'; function HOC(WrappedComponent) { return class WithComponent extends WrappedComponent { render() { return ( <WrappedComponent /> ); } } } export default HOC; 复制代码
理解高阶组件的精髓是什么呢?我认为是理解复用的到底是什么。
假如我们造一个组件,然后用到不同的地方,那直接用就好了,组件本身就是可复用的。
关键在于组件之外的东西。我们需要重复的给一个组件传入props,或者我们需要重复的给组件增强一些功能。这些都可以理解为组件的装饰,高阶组件其实解决的是组件装饰的复用。
大多数时候,使用高阶组件的场景都有更简单的替代方案。
高阶组件之所以这么流行,是因为React生态系统中很多库都有高阶组件的身影,它们或许是为了提供一个优雅的API,或许是需要处理复杂的场景,或许是...炫技。
如果哪天你复用组件时产生了瓶颈,不妨来看看老朋友高阶组件,它有多强大完全取决于你。
忘了告诉你们,高阶组件简称HOC,高阶组件的第一种类型叫属性代理,第二种类型叫反向继承。不过让这些概念都见鬼去吧,它只会让初学者望而却步。
render props
高阶组件就是传入一个组件,然后返回一个组件。
那我能不能不通过函数的参数传递组件,比如说通过props传递呢?
这是一个绝好的思路。
它有一个名字叫 render props
。
当然, render props
也需要一个用来复用的容器。通过给这个容器传递render属性,它可以渲染不同的组件。
属性名是无所谓的,render或者rander都可以,关键它的值得是一个组件,或者说无状态组件。
import React, { Component } from 'react'; class WithRender extends Component { state = { star: 1 } render() { return ( <div>{this.props.render(this.state)}</div> ); } } 复制代码
react-router
中的 withRouter
方法就是用的 render props
。
const withRouter = (Component) => { return (props) => { return ( <Route children={routeComponentProps => <Component {...routeComponentProps} />} /> ); } } 复制代码
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Spring Security小教程 Vol 5.核心组件AuthenticationManager专题
- React专题:可变状态
- React专题:生命周期
- Redux专题:实用
- 微内核专题系列
- 自然语言处理专题
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。