前言
大家好呀,我是wangly19,这次分享的是我十月下的文章。正巧赶上了掘金的征文系列,希望大家能多多支持我。
在学习React的两个月内,我大部分时间反而在阅读vant-ui @vue3的源码,不得不说有了一些很大的帮助,不论是一些思想上的提升还是作用于项目中的体验,其实大部分都是借鉴而来的。在很多时候,在写一个组件时,大部分都是会进行拆分的,很少有会直接写在一个jsx或者是.vue文件中的。因此,组件的编写就变得非常的需要艺术性了。
我也开始反思过我自身的一些问题,以前也没有去考虑太多的组件性能和组件颗粒度的问题,现今不论是从业务还是其他方面都不得不去思考组件的方案了。QAQ。
其实到了这里很多朋友就要问了,为什么写React组件会去看vant的源码,这不是买哈密瓜去了西瓜摊子吗。在这里我说下原因,其实看vant的源码还是因为vue3的关系,主要是想看下composition和hook去写组件的差异化对比,从而去理解两个框架在不同方向上对业务逻辑的理解。整体阅读下来,其实composition组件和无状态组件写起来还是非常像的。唠叨话就不多说,进入正题吧。
一些不足之处或者说是错误点可以在评论区指点下作者。
考虑角度
在编写组件的时候,需要考虑的角度或多或少就那么几个,我们也都是在拆分,重组,拼接等几个方向去进行,其是总结起来在写组件的时候考虑点也可以总结成下面几点:
- 组件分类
- 耦合程度
- 划分程度
- 依赖程度
组件分类
展示类组件
大多数时候,我们的组件可以分为以下几种,最常见使用的就是展示类和交互类组件了,绝大部分业务中,都是由两者组成的。举个例子,如下图是JD购物的首页的商品选项就是一个展示类组件,它并不具有很复杂的交互性,它唯一做的就是通过props的值进行数据的显示,并没有进行更多更多深入行的逻辑交互。因此它就相当于展示性组件。
交互型组件
在举例子,JD购物的选择商品规格的界面就是一个典型的交互性组件,当你点击规格标签的同时,你的价格和已选中的数据会发生改变,且商品的状态也在实时判断,当商品缺货时,确认按钮就会禁止用户进行进一步的操作等等不同的状态。这就是一个交互型组件。
数据型组件
对于不可感知的数据类组件一般都是由框架或者是库提供的,如现在react-router和react-redux都是一个数据类组件,它们会在web应用 开始时就准备好需要的数据,然后供后续使用。因此它是属于一个聚合渠道的一个组件。
HOC高阶组件
从高阶函数到高阶组件,大多数的时候,为了能够复用某些逻辑,往往都会使用到HOC,随着业务的不断扩展,产生的宽容性也需要组件来进行支持,如果说某个逻辑的组件反复被用到,那么这个时候,就需要根据其视图的差异性考虑进行高阶组件的一个拆分,不论是vue2的mixin还是custom hook,在多数都是为开发者提供简单的逻辑复用。
耦合程度
通过上述几种的组件方式,对于组件已经有了初步的认识,那么就到了组件的耦合度这块了,理想化的情况是在java中,我们都知道,一个类里面有很多方法,我们希望通过方法来描述这个类的行为模式和特征,因此大多数时候,一个方法通常只做一见事情,并不会过多的进行干预。同样的,在组件中,render函数非常的杂乱,如果没有稍加注释可能就会形成阅读死区,其他人如果没有文档,非常难看懂代码的含义,因此也就多了很多前人栽树,后人就真的凉的情况,好的组件库它的代码大部分都是解耦过的,我们可以根据不同的组件类型作用进行一定的组件解耦。
实例
在vant ui中,我觉得它们现在的组件拆分非常的清晰,刚好最近Taro框架碰到了组件库的问题,就以一个popup组件来进行实例。
代码中可以看到,Popup分为两个部分组成,一部分是内容,一部分是overflow遮罩,我们通过不同的render方法进行声明,最后整理在return当中,每一个render方法只做一个逻辑的处理或者是其他关联逻辑的引入,彼此之间不去干涉,那么其他人在阅读组件的时候,能够从功能块入手,快速理清逻辑,进行Bug Fix 或者是 feature。
import React from 'react'
import { View } from '@tarojs/components'
import VtIcon from '@/components/vt-icon'
import VtOverflow from '@/components/vt-overflow'
import { CSSTransition } from 'react-transition-group'
import classNames from 'classnames'
import PropTypes from 'prop-types'
import '@/styles/vt-popup.scss'
function VtPopUp (props) {
const {
customStyle,
className,
overlayClass,
overlayStyle,
children,
overlay,
show,
zIndex,
position,
round,
transition,
} = props
/**
* overflow点击事件
*/
const handelOverFlowClick = () => {
console.log('click overflow')
}
/**
* 遮罩层
*/
const renderOverlay = () => {
if (overlay) {
return (
<VtOverflow
show={ show }
customStyle={ overlayStyle }
className={ classNames([
'vt-overflow-hidden',
overlayClass,
]) }
onClick={ handelOverFlowClick }
/>
)
}
}
/**
* TODO: 关闭图标
*/
const renderCloseIcon = () => {
}
/**
* Popup组件
*/
const renderPopup = () => {
return (
show ? <View
v-show={props.show}
style={{
zIndex,
...customStyle
}}
className={classNames(
[
`vt-popup`,
`vt-popup--${position}`,
round && `vt-popup--round`,
className,
]
)}
>
{children}
{/* {renderCloseIcon()} */}
</View> : null
)
}
const renderTransition = () => {
const name = position === 'center' ?
'vt-fade' :
`vt-popup-slide-${position}`;
return (
<CSSTransition
in={true}
timeout={1000}
unmountOnExit
classNames={ transition || name }
appear={true}//开启第一次执行展示的样式
>
{renderPopup()}
</CSSTransition>
)
}
return (
<>
{ renderOverlay() }
{ renderTransition() }
</>
)
}
VtPopUp.propTypes = {
className: PropTypes.string,
customStyle: PropTypes.object,
children: PropTypes.element,
show: PropTypes.bool, // 显示popup
zIndex: PropTypes.oneOfType([ // zIndex层级
PropTypes.string,
PropTypes.number,
]),
duration: PropTypes.oneOfType([ // 动画时长
PropTypes.string,
PropTypes.number,
]),
overlayClass: PropTypes.string, // 遮罩样式
overlayStyle: PropTypes.object, // 遮罩样式
overlay: PropTypes.bool, // 是否显示遮罩
lockScroll: PropTypes.bool,
lazyRender: PropTypes.bool, // 懒加载
closeOnClickOverlay: PropTypes.bool // 是否在点击遮罩层后关闭
}
VtPopUp.defaultProps = {
show: false,
overlay: true,
zIndex: 2,
position: 'center',
lockScroll: true,
lazyRender: true,
closeOnClickOverlay: true,
}
export default VtPopUp
依赖程度
说完了组件的耦合,那么说下组件的划分吧。我们都知道组件有全局组件,布局组件这两种使用形式。大多时候都是拆成子组件进行一个重复调用,不同的组件或多或少都会依赖props的值进行传递,那么这个时候就会出现一个头疼的问题,组件都有自己的变量域,子组件有子组件的state,父组件有父组件的state,props传递的data大部分都来自父组件的props,
因此这里就需要考虑就是当父组件state发生了更新,这个state的依赖程度究竟有多深,相比于脏数据,依赖重的组件往往会因为蝴蝶扇动翅膀,缺发生了很大的变化。
因此我们在写组件中,适当的进行计算属性的包裹,防止因为其他原因产生的变化。让只需要更新的组件更新。因此,如果你的组件依赖计算非常的重,性能消耗大,还是老老实实使用Memo包一下吧。
const realInfo = useMemo(() => {
// 组件内容, 只有
}, [props.payid, props.openid])
总结一下:
- 避免组件
props过渡重量化,props越多(不是数据越多,是依赖越多)越容易发生组件频繁render。 - 性能消耗大的组件可以适当使用
Memo Hook记忆化数据 - 大部分状态尽量在内部维护,而不是全部都传递下来
- 管理组件状态,未知状态不要滥用。
总结
感觉全文都是一些理解性的概念,React资深的你或许用不到,但其实大部分受限于业务的同学都没有去考虑过组件的一些拆分,数据的流向。往往可以看到一个jsx文件可以是1000 ~ 1500行的规模,这个时候,其实就非常的臃肿了,开发者知道吗?知道,但受限于业务,基本没时间去处理过多的组件方案,慢慢的堆叠起来,代码就变得非常的糟糕,过一个月回来一看,自己本身都需要去理清一些思路才知道代码的含义。React如此,Vue也如此,最终却还是有很多人在高呼React比Vue性能好。但这本身就是一个很难去回答的问题,同样的工具,不同开发者使用的情况不一样,创造的价值也不一样。
大铁锹有人用它去挖地,而同样的有人却用它做菜。
React就是这把铁锹,它只是一个工具,它本身并不带使用约束,一切的用途都是开发者自身去进行的。
如果觉得有帮助,可以点个赞,好想在要一个大礼包呀。这篇文章是十月下的一篇文章,如果哪里写的不对,欢迎在评论区交流,共同学习。
文末彩蛋QAQ。