当前位置: 首页 > Web前端 > JavaScript

两周一个小组件的List组件

时间:2023-03-27 11:32:08 JavaScript

前言两周一个组件的系列文章将介绍迷你版react组件的构建过程和一些心得。react组件mini版会参考社区优秀开源的Component库,了解其组件的实现原理,然后吸取其中的精华应用到我的react组件mini版中,实践一下(俗称作为制造轮子)。主要是想通过这种记录和分享的方式督促自己去了解和学习那些优秀的组件库的实现,通过造轮子来提升自己(我个人认为造轮子是一个很累但是收获很大的过程)。List组件的思考与准备让我们进入今天的主题:List组件的实现过程。实现这个组件的初衷是项目开发过程中有这样的需求。有一堆元素按一定方向排列,但容器空间不足,可以支持滚动显示多余的内容或者切换左右箭头显示多余的内容。然后经过一番了解,现有的组件库好像没有特别适合这个需求的组件,而且我觉得实现这个功能也不是很复杂,既然可以自己写一个,何乐而不为呢??那是。.自己拿一个!首先是图片!来看看我们最终的实现效果:只需要几行代码就可以实现这个效果:{renderButton()}{renderCard()}{renderCardWithResize()}constrenderButton=()=>{constbuttonTextArr=Array.from({length:30},(v,k)=>`button${k+1}`)returnbuttonTextArr.map(item=>{item})}constrenderCard=()=>{constCardTextArr=Array.from({length:30},(v,k)=>`Card${k+1}`)returnCardTextArr.map(item=>{item}

)}constrenderCardWithResize=()=>{constCardTextArr=Array.from({length:7},(v,k)=>`Card${k+1}`)返回CardTextArr.map(item=>{item}
)}效果还不错!接下来,我们来看看如何实现这样一个迷你组件:首先,我们要明确组件的最终功能,也就是明确用途。在这里我希望迷你列表组件可以作为一个容器来包装任何长的内容它可以支持滚动和左右箭头之间的切换。总结起来就是三个功能点:支持滚动;元素全屏切换:点击切换按钮,实现全屏切换效果。监听容器大小变化,当空间足够充分显示和切换时,阻止滚动;并且作为容器,其内容的方向应该是可控的(即支持水平和垂直方向)。明确目标后,下一步就是找出技术难点,思考可行的技术方案。在这个列表组件中,如何实现内容的切换和滚动效果是难点之一,实现这种效果有3种可行的技术方案:通过控制css样式的left(top)属性,实现切换和模拟滚动;通过css3的transform属性实现切换和模拟滚动;使用原生的scroll事件和scrollto方法实现;最终,在这三种方案中,我更喜欢方案2,相比方案1,方案2采用的transform有比剩下更好的性能优势,而方案3会更多地依赖nativemethods,定制化少。方案确定后,如何实现?实现过程定义了一个容器元素,存储它的ref并取消它的滚动:.node-list{position:relative;溢出:隐藏;框大小:边框框;宽度:100%;height:100%;}
定义了一个子元素容器(实际内容的容器,控制transform的目标元素)并存储其ref://children节点
处理容器组件的children属性(子元素,真正需要渲染的内容),这里我们稍微包裹一下这个处理子元素的方法:functionparseTabList(children){returnchildren.map((node,index)=>{if(React.isValidElement(node)){constkey=node.key!==undefined?String(node.key):index;return{key,...node.props,node,};}returnnull;}).filter((node)=>node);}通过这个方法,我们将子元素转化为包含子元素信息的对象,存储在数组元素节点(用于稳健性考虑一下,我们还是使用isValidElement来验证react元素)然后渲染子元素,使用刚刚构建的子元素对象:constnodeRender=nodes.map((node)=>({node.node}));这样我们就可以通过ref获取到真正的子元素(dom),即通过useRef创建对应的ref容器,但是当子元素个数不确定的时候,我们可能需要采取一些策略来生成这样的一个ref容器并保存它:functionuseRefs(){constcacheRefs=useRef(newMap());functiongetRef(key){if(!cacheRefs.current.has(key)){cacheRefs.current.set(key,React.createRef());}返回cacheRefs.current.get(key);}functionremoveRef(key){cacheRefs.current.delete(key);}返回[getRef,removeRef];}const[getNodeRef,removeNodeRef]=useRefs();使用自定义的hook生成可独立复用的codeselection是个不错的主意,在一定程度上可以更接近原来的useRef,而多个refs的存储采用Map类型,关键是sub-元素点关键道具最后我们还应该渲染两个切换按钮元素:基于在我们的场景中,我们需要获取有关许多元素的位置和大小的信息。获取子元素的位置和大小信息:const[nodesSizes,setNodesSizes]=useState(newMap());setNodesSizes(()=>{constnewSizes=newMap();nodes.forEach(({key})=>{constNode=getRefBykey(key).current;if(Node){newSizes.set(key,{width:Node.offsetWidth,height:Node.offsetHeight,left:Node.offsetLeft,top:Node.offsetTop,});}});返回newSizes;});在nodeSizes状态下,它包含了每个子元素的实际宽高(包括边距)和位置(left和top在容器中的偏移量)。这里需要说明一下,offsetWidth属性(HTMLElement.offsetWidth只读属性返回一个元素的布局宽度作为一个整数。)和offsetHeight属性(HTMLElement.offsetHeight只读属性返回一个元素的高度,includingverticalpaddingandborders,asainteger.)从介绍来看,这两个属性表示的范围只有border-box。这里,因为我们在实际的子元素周围包裹了一个div元素,所以我们实际得到的是div元素的offsetWidth和offsetHeight,所以我们可以得到实际子元素的全宽和全高(包括边距)。获取可视区域的宽高:constoffsetWidth=nodesWrapperRef.current?.offsetWidth||0;constoffsetHeight=nodesWrapperRef.current?.offsetHeight||0;但是可视区域可能除了子元素还有切换操作按钮,所以实际可见区域的宽高要减去切换按钮的宽高(如果存在的话):setWrapperWidth(offsetWidth-(isOperationHidden?0:newOperationWidth*2));setWrapperHeight(offsetHeight-(isOperationHidden?0:newOperationHeight*2));获取所有内容(即滚动区域)的宽高:constnewWrapperScrollWidth=nodeListRef.current?.scrollWidth||0;constnewWrapperScrollHeight=nodeListRef.current?.scrollHeight||0;当我们需要获取的信息全部具备之后,我们就可以实现滚动逻辑不再困难了。在我们的transform方案中,滚动效果其实就是控制transform属性的变化(根据排列方向的不同,控制transformX或者transformY)来监听元素的wheel事件,执行改变transform的逻辑:useTouchMove(nodesWrapperRef,(offsetX,offsetY)=>{functiondoMove(setState,offset){setState((value)=>{constnewValue=alignInRange(value+offset,transformMin,transformMax);returnnewValue;});}if(水平){//如果位置足够则跳过滚动}//clearTouchMoving();返回true;});这里我们使用自定义的hook将wheel事件监听的逻辑包裹在自定义的hookuseTouchMove中,内部代码这里就不贴了,主要作用是监听外层容器的wheel事件,计算滚动距离和direction作为对应的offset,作为doMove方法的参数。从函数doMove代码中不难看出,doMove方法是将滚轮的滚动距离转换成对应的transform属性值,但是这里还需要考虑一些边界条件,即当宽度可见区域的宽度和高度大于滚动区域的宽度和高度,即当容器的内容可以完全显示时,不应该响应滚轮事件;transform属性值的设置应该是有界的,即有最大值和最小值(对应元素滚动到顶部和底部),所以这里我们在设置transform属性的值时会先由alignInRange函数处理,主要判断最大值和最小值;如果(!水平){transformMin=Math.min(0,wrapperHeight-wrapperScrollHeight);变换最大值=0;}else{transformMin=Math.min(0,wrapperWidth-wrapperScrollWidth);变换最大值=0;}transform属性的最大值为0,最小值为可见区域的宽(高)减去滚动区域的宽(高),这里的最小值为负值,因为元素是滚动的左或上,对应于transformX和transformY均为负数。这样就实现了支持滚动的第一个目标。但是实现通过操作按钮切换显示内容的功能需要稍微复杂一些。首先,我们需要获取当前出现在可见区域的元素(完整出现的元素,只排除部分元素),我们知道当前的transform属性值和每个子元素的位置宽高信息,以及在可见区域的宽高的前提下,实现起来并不难:constlen=nodes.length;让endIndex=len-1;for(leti=0;itransformSize+basicSize){endIndex=i-1;休息;}}让startIndex=0;对于(leti=len-1;i>=0;i-=1){constoffset=tabOffsets.get(nodes[i].key)||默认大小;if(offset[position]transformSize+basicSize来确定child元素不在可见区域,其中transformSize表示当前变换值的绝对值,basicSize表示可见区域的宽(高)。并通过子元素offsetLeft-transformLeft+wrapperWidth){newTransform=-(nodeOffset.left+nodeOffset.width-wrapperWidth);}setTransformTop(0);setTransformLeft(alignInRange(newTransform,transformMin,transformMax));}以上就完成了我们的第二个功能点。至于第三个功能点,其实是最简单的。只需要监听resize事件,在事件回调中重新获取可见区域的大小和滚动区域的大小即可。实现起来比较简单,这里就不赘述了。以上就是本次分享的全部内容,更多内容将在后续文章中分享。