本文是『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} />} />
);
}
}
复制代码
React专题一览