最近在项目中遇到一个需求,需要滚动加载一个列表,类似微博的无限滚动。当时的第一反应是监视滚动事件,当判断滚动到达底部时,加载下一页。同时,我心里很清楚,监视滚动事件需要很好地拦截。我方便地搜索并分发了一个现成的插件vue-infinite-scroll,它的用法非常简单,所以我使用了它。需求上线后,我对它的实现很好奇,所以我研究了源代码。本文是一篇源代码分析笔记。
插件的使用
这是一个vue指令。根据github仓库上的介绍,用法相当简单,例如:
div class=' app ' v-无限滚动='loadMore '无限滚动-禁用='busy '无限滚动-距离=' 10 ' div class=' content '/div div class=' loading ' v-show=' busy ' loading./div/div . app { height : 1000 px;border: 1px纯红;宽度: 600 px;margin: 0 auto飞越:汽车;}.内容{ height: 1300px背景色: # CCC;宽度:80%;margin: 0 auto}.正在加载{ font-weight : bold;font-size : 20px;颜色:红色;文本对齐:中心;}var app=document.querySelector('。app’);new Vue({ el: app,directives: { InfiniteScroll,}),data : function(){ return { busy : false };},methods : { loadmore : function(){ var self=this;self.busy=trueconsole.log('正在加载.新日期());setTimeout(function(){ var target=document . queryselector ')。内容’);var height=target.clientHeighttarget . style . height=height 300 ' px ';console.log('end.新日期());self.busy=false}, 1000);}, },});这里,指令宿主元素本身设置为overflow:auto,内部元素用于支持滚动。滚动到底部时,内部元素的高度会增加,以模拟无限滚动。效果如下:
此外,可以将父元素设置为滚动,当它滚动到父元素的底部时,它会增加其高度并模拟拉下一页数据的操作。例如:
div class=' app ' div class=' content ' v-无限滚动=' loadmore '无限滚动-禁用=' busy '无限滚动-距离=' 10 '/div class=' loading ' v-show=' busy '正在加载.
源代码解析
下一步是看它是如何在内部实现的。像往常一样,从入口开始。因为这个插件是vue指令,所以条目非常简单:
命令输入
导出默认{bind (el,binding,vnode) {el [CTX]={el,vm:vnode。上下文,表达式3360绑定。value,//滚动到底部时需要的监听功能,通常用于加载下一页数据};const args=参数;//听听埃尔[CTX]的骑马活动。虚拟机。$ on ('hook: mounted ',function () {El [CTX]。虚拟机。$ next tick(function(){//判断元素是否已经在页面上if(Isattached(El)){//get all items } El[CTX]。bindTryCount=0;//每隔50ms旋转10次,判断元素是否已经vartry bind=function(){ if(El[CTX])。bindtrycount10)返回页面;//eslint-disable-line el[ctx]。bindTryCountif(Isattached(El)){ DoBind . call(El[CTX],args);} else { setTimeout(tryBind,50);} };try bind();});});},解除绑定(el) {//事件解除绑定if (El El [CTX] El [CTX]。CTX。阴囊目标。移除事件监听器(滚动),埃尔[CTX]。scrollistener);},};核心是在呈现宿主元素后执行doBind方法。我们猜测父元素的滚动事件将在doBind绑定中滚动。
IsAttached方法用于判断页面上是否已经呈现了一个元素,判断方法是检查是否存在标签名为HTML的组件元素:
//判断元素是否已经在页面上var被附加=函数(元素){ var currentNode=元素。父节点;while(当前节点){ if(当前节点。标记名==' HTML '){返回true} //11 表示DOM片段if(currentnode。nodetype===11){返回false}当前节点=当前节点。父节点;}返回false };参数解析与事件绑定
现在看看多宾德方法,逻辑比较多,不过都不难。
var DoBind=function(){ if(this。绑定)返回;//只绑定一次this.binded=truevar指令=thisvar元素=directive.el//节流器DelayExpr:截流间隔。设置在元素的属性上var throtterdelayexpr=element。GetAttribute('无限滚动-节流-延迟');var throtterdelay=200 if(throtterdelayexpr){//优先尝试组件上的throttleDelayExpr属性值,如差异无限-滚动-节流-延迟='myDelay'/div节流延迟=数字(指令。VM[throtterdelayexpr]| | throtterdelayexpr);if(isNaN(油门延迟)| |油门延迟0){油门延迟=200;} }指令。指令.节流延迟=节流延迟//监听滚动父元素的卷起时间,监听函数设置了函数截流指令。scrollecventarget=getscrollecventarget(元素);//设置了滚动的父元素指令。scrollistener=throttle(Docheck。绑定(指令),指令。油门延迟);指令。scroleventtarget。addeventlistener('滚动',指令。scrollistener);这个。虚拟机。$ on('销毁前挂钩: ',函数(){指令。scrolleceventtarget。removeeventlistener(' scroll ',指令。scrollistener);});//无限滚动禁用:是否禁用无限滚动//可以为表达式var disableexpr=元素。GetAttribute('无限滚动禁用');var disabled=false if(disable expr){ this。虚拟机。$ watch(disableexpr,function(value){ directive。disabled=值;//当使残废为错误的时,重启检查if(!值指令。立即检查){ Docheck。呼叫(指令);} });禁用=布尔(指令。VM[disibleexpr]);}指令。disabled=已禁用;//宿主元素到滚动父元素底部的距离阈值,小于这个值时,触发倾听事件监听函数var distanceExpr=元素。GetAttribute('无限滚动距离');定义变量距离=0;if(距离表达式){ distance=Number(方向)。VM[距离expr]| |距离expr);if(isNaN(distance)){ distance=0;} } directive . distance=distance//立即检查:是否在约束后立即检查一遍,也会在使残废失效时立即触发检查var immediate techchexpr=element。GetAttribute('无限滚动-立即-检查');var immediate check=true if(immediate expr){ immediate check=Boolean(指令。VM[immediate expr]);}指令。立即检查=立即检查;if(立即检查){ Docheck。呼叫(指令);} //当组件上设置的此事件触发时,执行一次检查var事件名称=元素。GetAttribute('无限滚动-监听事件');if(事件名称){指令。虚拟机。$ on(事件名称,函数(){ Docheck。call(指令));});}};整个看下来,核心就是利用各种参数控制文件检查的调用,包括时间间隔、残疾人、距离阈值、立即检查、组件事件。
文件检查因为会非常频繁的调用,所以用喉咙进行了截流,具体逻辑这里不再赘述。
在getScrollEventTarget查找滚动父元素时,有一个细节就是会从自身开始查找,这也就是我们上面的演示中可以将指令宿主元素赋值给滚动元素自身的原因:
//从自己做起,找到设置了滚动的父元素。Overflow-y是scroll或autovar get scrollecventarget=function(element){ var current node=element;//bugfix,参见http://w3help.org/zh-cn/causes/SD9013和http://stack overflow.com/questions/17016740/on croll-function-is-work-for-chrome//nodetype 1表示元素节点while(current node current node . tagname!=='HTML' currentNode.tagName!==' BODY ' current node . nodetype===1){ var overflow y=getcomputed style(current node)。飞越;if(overflow y==' scroll ' | | overflow y==' auto '){ return currentNode;} current node=current node . parent node;}返回窗口;};文件检查
这个函数用来判断是否已经滚动到底部,可以说是整个插件的核心逻辑。由于滚动元素可以是其本身,也可以是父元素,因此判断将分为两个分支。
var Docheck=function(force){ var scrollenventTarget=this . scrollenventTarget;//滚动父元素var element=this.el变化距离=这个距离;//距离阈值if (force!==true this.disabled)返回;var viewport scroll top=getscroll top(scroleventtarget);//隐藏在内容区域上方的像素数//viewportBottom:元素底部到文档顶部的距离坐标;VisibleHeight:无边框元素的高度var viewport bottom=viewport scroll top get visible height(scrolle vent target);var shouldTrigger=false//scroll element是它自己的if(scroll event target===element){//scroll height-没有滚动条的元素内容的总高度是元素内容区域加上内部边距加上任何溢出内容的大小。//shouldTrigger为true,这意味着它已经滚动到元素的底部足够多了。//参考https://hello github 2014 . github . io/2017/10/19/DOM-element-size-summary/should trigger=scrolvent target . scroll height-viewport bottom=distance;} else {//当前元素不是父元素,这通常意味着当前元素的高度高于滚动的父元素的高度。只有这样父元素才能滚动//getElementTop(element)-getElementTop(scroleventtarget)当前元素顶部与滚动父元素顶部之间的距离//带边框的offsetHeight元素的高度//ElementBottom:元素底部与文档顶部之间的距离坐标var element bottop=getElementTop(element)-getElementTop(scroleventtarget)element . offset heat viewport scroltop;shouldTrigger=viewportBottom距离=elementBottom} if(should trigger this . expression){ this . expression();//触发绑定无限滚动功能,通常是为了获取下一页数据。之后,scrollEventTarget.scrollHeight会变大} };这里涉及到很多维度值,包括scrollTop、offsetTop、clientHeight、scrollHeight等。如果不清楚,整个功能的逻辑就很难理解。具体含义请参考我之前写的一篇博客。
这里我用两张图来帮助理解上面的逻辑,相信会容易理解很多。
滚动元素本身就是
如下所示,我们的目标是判断元素是否在底部的距离阈值内滚动。很容易看出,距离公式底部的内容是:
const { scrollHeight,clientHeight,scrollTop }=scrollEventTargetconst current distance=scroll height-client height-scroll top;这是if分支函数的逻辑。当当前距离小于该距离时,我们可以加载下一页数据。
父元素设置滚动
此时没有scrollTop属性可以操作,但是上面的属性仍然可以用于元素的高度:scrollEventTarget.clientHeight可以用于滚动父元素的高度,element.offsetHeight可以用于子元素的内容高度,剩下的就是计算topGap。
我们知道DOM坐标有两种:文档坐标和视口坐标,我们只需要始终在其中一个坐标系中计算topGap。这里我们使用视口坐标。埃勒。getboundingclientect()。top可以知道一个元素到视口顶部的距离,所以topGap的计算公式是:
const TopGap=scrollenventTarget . GetBoundingClientRect()。top-element . getboundingclientrect()。顶部;总而言之,子元素底部和父元素底部之间的距离公式为:
const current distance=element . offset theight-scrollecventarget . client height-(scrollecventarget . getboundingclientrect()。top-element . getboundingclientrect()。顶部);这是函数的else分支逻辑。
以上是doCheck的核心检测逻辑。同时在文档化的时候对scrollEventTarget做了一些特殊的处理,留给大家看。
以上就是本文的全部内容。希望对大家的学习有帮助,支持我们。