内容简介:本文是『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专题:实用
- 微内核专题系列
- 自然语言处理专题
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
国际大学生程序设计竞赛例题解
郭嵩山 / 电子工业出版社 / 2006-5 / 32.0
《国际大学生程序设计竞赛例题解1:数论、计算几何、搜索算法专集》可以作为高等院校有关专业的研究生和本科学生参加国际大学生程序设计竞赛的辅导教材,也可作为高等院校有关专业相关课程的教材和教学参考书,也比较适合作为中学青少年信息学奥林匹克竞赛省级及省级以上优秀选手备战信息学奥林匹克竞赛的培训教材及训练题集。一起来看看 《国际大学生程序设计竞赛例题解》 这本书的介绍吧!