从零单排JavaScript第一期

来源:互联网 发布:mac系统怎么安装ae插件 编辑:程序博客网 时间:2024/04/30 08:05

故事背景


在很久很久以前……咳咳,倘若你已经接触过其他语言,但和我一样还没有接触过JavaScript,那咱们就一起到JavaScript的奇妙世界旅行吧。虽然这不是面向编程零基础的童鞋们的文章,但是我会努力写得让这类童鞋也看得懂我们在做什么,了解编程大致的流程到底是怎么个样子的(你们只用看代码部分的中文注释即可),如果把编程比喻为摄影,那么编程语言只是手中的那台拍摄设备,有的人拿部手机就能拍出震撼人心的作品,而有的人即使拿台莱卡拍出来的也只是...呵呵,所以真正的主角是你的洞察力,是你的脑海中形成的那个"画面",就像拍摄前你至少得知道快门按钮怎么按,变焦又怎么变(如果是用的变焦镜头的话)等等这些普适于基本所有相机的用法一般,编程语言也是如此,种类再多,但也万变不离其宗,而这些东西我不会在文中一一介绍,因此说本系列文章不是面向编程零基础的童鞋们的,但假若在和我们一起旅行的过程中你们能感到编程这件事本身是可爱迷人的反派角色,噢,不,平易近人的(不知道这个梗的,无视就好了:P),那就够啦。文章将以记叙的方式展现没接触过JavaScript的我是如何用JavaScript一步步做出想要的那一个个web前端特效的。那么首先先让脑海中不断涌现一个又一个有趣想法,然后再开始把它们一一实现出来!

好的~所以只要你知道餐厅没有鱼丸,也没有粗面,就不要再尝试去点鱼丸粗面(不要吐槽怎么又来一个梗啊摔,也不要吐槽假若有几碗做好了没卖出去之类奇奇怪怪的情况啊摔~),那就赶紧和我们一起出发吧。

p.s.由于主要讲解关于JavaScript代码部分的思路,所以HTML和CSS的代码不会在文中贴出来,但会相应的提到一些相关问题。在最后我会给出Demo地址。

今日任务公告栏


炉石传说中的那个匹配轮盘给我留下了深刻的印象,所以这次我打算做一个具备轮盘效果的折叠列表。当鼠标移动到对应列上时就自动展开该列,处于渐变淡出的列不会被展开,属于未激活状态。需要通过鼠标滚轮转动轮盘使渐变淡出的列滑动到焦点区域处于激活状态,如图1-1所示,当前处于激活状态的列为a列 b列 c列和d列,而e列 f列 g列为未激活状态。

启程——投石问路


首先搭出列表的大致外观,通过javascript来生成这个列表,代码(仅javascript的代码)如下:

 /*获得Id为panel的html标签,这里的id为panel的标签是一个div标签,它是整个列表的父元素*/
  var panel = document.getElementById("panel");

  var html="";
  var num = 15; 

/*通过一个for循环生成列表的各列*/
  for(var i=0; i<num ;i++){ 
    html += "<div id=data"+i+" class='list'>"+i+"</div>";
  }

/*然后赋值给panel标签,最终在页面上显示出来*/
  panel.innerHTML = html;

效果如下图1-2所示。

接下来再考虑如何做出轮盘效果之前,我们需要看看javascript的鼠标滚轮事件是怎么一回事的,在找度娘查询一番后,得知javascript的滚轮事件在firefox中和其他如IE,Chrome等浏览器稍有不同(下面代码中的scroll是我们再触发滚轮后调用的函数),另外我只想让这个列表响应鼠标滚轮事件(即当鼠标在列表上时滚轮才有效果),所以在下面的代码中用的是panel而不是document(此处的panel就是前面代码中的变量panel),代码如下:

/*在firefox中通过addEventListener来绑定firefox的滚轮事件DOMMouseScroll*/
if(document.addEventListener){
    panel.addEventListener('DOMMouseScroll',scroll,false);
}

/*在其它如IE,Chrome等浏览器中的滚轮事件为onmousewheel,由于这类浏览器也支持addEventListener的事件绑定方法,所以此处就不要再写else了*/
panel.onmousewheel = scroll;

此时当我们滚动鼠标滚轮时便会触发scroll函数了,那么现在的问题是,如何得知滚轮的滚动方向呢?关于滚动方向的获取,firefox和其他如IE,Chrome等浏览器依然是不同的,firefox中是detail,而IE,Chrome等是wheelDelta。测试代码如下:

function scroll(e){
    e = e || window.event;


/*IE,Opera,Chrome等浏览器*/
    if(e.wheelDelta){
      alert(e.wheelDelta);
    }


/*firefox*/
    else if(e.detail){ 
      alert(e.detail);
    }


}

经过我的测试,滚轮上滚时detail的值是-3,而wheelDelta是120,当滚轮下滚时detail的值是3,而wheelDelta是-120,可见不仅取值不同,而且同样的滚动方向正负也不同。注意到那个变量e了吗,刚开始的时候我是直接使用的event.detail和event.wheelDelta,这时候在有些浏览器中是获取不到值的,这是因为在不同的浏览器中的事件event和window.event是有区别的,所以为了兼容性就写成了上面代码那样。虽然问题解决了,但总觉得缺点什么,那我们进一步探究探究event和window.event区别的吧,为了方便,就直接把这个滚轮触发后调用的scroll函数用来做测试吧,测试代码如下:

function scroll(e){

   alert("e: "+e)
   alert("event: "+event);
   alert("window.event: "+window.event);

}

这里特别要注意的一点是,IE浏览器自古以来一直和其他浏览器格格不入,说好听点就是不随主流有个性,说难听点就是给开发人员填堵,直到后来的版本(是IE9还是IE10还是IE11,这个不清楚了,希望有知道的朋友能说一下:P),进行了很大程度的改变,而我这次用来测试的IE是IE11,所以结果极可能会和以前版本不一样。另外我的chrome版本是36,firefox的版本为31。

在Chrome中获取到测试代码中的这三个值皆为[object WheelEvent],在IE11中这三个值皆为[object  MouseWheelEvent],在firefox中首先会直接报错提示event is not defined,也即未定义,至于e的值则为[object MouseScrollEvent],但window.event的值为undefined,也即没有这个值。通过w3c的资料(点击这里打开关于w3c介绍window的页面) 我们可知window表示的是一个全局变量,所以结论就是,目前IE和Chrome二者把事件对象作为了全局变量来保存的,所以通过window.event能够获取到,同时也会把事件对象传递给对应的事件处理函数,所以在通过传入函数的参数e,我们也能获取到值,而firefox仅是通过把事件对象传递给对应的事件处理函数,并没有把事件对象作为一个全局对象,因此通过函数的传入参数e能获取到值,而通过window.event却是undefined也即没有这个值。至于那个单独的event,我的推断是它相当于是IE和Chrome等等浏览自己的一个对window.event的简写定义,因为在firefox中调用event不是提示获得值undefined而是直接浏览器报错说event is not defined(如果我理解错了,麻烦知道的朋友指正一下,谢谢:P)

了解了JavaScript中的鼠标滚轮事件之后,我们继续来考虑轮盘效果的实现。

首先我想到的是把整个列表看作一个整体进行移动,如下图1-3所示,黑框部分是整个列表,红框内是列表可视部分,而在红框外的通过CSS设置overflow属性让其隐藏掉,此时滚动滚轮时,让黑框部分进行左或右的平移。当该列表中的第一列(从左往右数第一个方块)进入红框时,则把列表的最后一列(从左往右数最后一个方块)放到第一个之前使其成为整个列表的第一列。同理反向滚动让列表最后一列进入红框部分,在后面朝另一个方向滚动这部分情况就不再赘述了。

这个方案需要通过代码获得当前列表的第一列与红框左边框之间的距离值以及最后一列与红框右边框之间的距离值,然后是关于在红框中的渐变效果,可以通过在红框中增加两个背景色为白色的div层分别靠住左边框和右边框,其作用就是覆盖作用,效果是一个向左渐变不透明显出白色,另一个向右渐变不透明显出白色,这两个覆盖层盖住后便似乎是渐变透明消失了(当然前提得是在列表的背景色为白色的情况下),然后被盖住的部分鼠标移动上去也不会触发列表展开的效果,只有中间没有被盖住的才行。这时候假若我展开了其中一个列表极有可能展开的内容会延长进入覆盖层下面,导致这部分内容被覆盖层盖住形成变成渐变消失的视觉效果,因此除了计算好宽度距离之外,还需要把在会展开某列时就把某列的列移动到某位置,确保不会发生这样的覆盖情况,想到这里有没有觉得这个方法太费事了呢,那么还有没有其他方案来实现这个轮盘特效呢?

你们有其他的方法吗?如果有的话,就留言分享一下吧:P,现在倒是我还有种办法,我们可以通过设定id值(用class也行,但是在不依靠jq的情况下就纯js的话,在获取到具备对应的class属性的标签不如通过获取id方便),比如说有四列我需要显示出来,它们的id分别为data0,data1,data2 ,data3,然后对这四个id设置CSS3中的transform属性,使其为函数translateX,接着通过该函数来对这些标签进行一个从左往右的排列,而其它的列所属的标签则直接设置他们的CSS属性display为none就好了。当我滚动鼠标触发事件函数时,就会对当前各列的id值进行替换,如下图所示:

此时页面上显示的轮盘列表效果则为前面的那幅图1-2,当我滚动滚轮触发事件函数时,该函数会对所有的列的id值重新赋值,如下图:

这时候在页面上显示的轮盘列表效果是这样的:

对各列id进行重新赋值的函数如下代码所示:

  /*该函数为当把轮盘从左往右转动时所触发的*/
function pre(){
    var list = panel.getElementsByTagName("div");
    var id;
    for(var i=0;i<num; i++){

/*id值为data2这类形式,我们只需要对其中的数字部分进行处理,而在js中可以通过slice来对字符串进行分割*/
      id = parseInt(list[i].id.slice(4));
      id = id + 1;
      if(id >= num)
          id = 0;
      list[i].id = "data"+id;
    }
    return false;
  }

  /*该函数为当把轮盘从右往左转动时所触发的*/
function next(){
    var list = panel.getElementsByTagName("div");
    var id;
    for(var i=0;i<num; i++){
      id = parseInt(list[i].id.slice(4));
      id = id - 1;
      if(id < 0)
          id = num-1;
      list[i].id = "data"+id;
    }
    return false;
  }

注意到上面代码中的parseInt了吗?由于javascript是弱类型语言,代码中的id值又是从一个字符串中截取出来的内容为数字的字符串,此时进行加法计算可能会出现比如这样的情况:2+4 = 24,即把2和4看作了字符串结合在了一起,所以通过parseInt进行了一个转换表明了这是一个数字。

接下来我们把滚轮事件的函数写出来:

  function scroll(e){
    e = e || window.event;
   if(e.wheelDelta){//IE,Opera,Chrome
     if(e.wheelDelta > 0){
       pre();
     }
     else if(e.wheelDelta < 0){
       next();
     }
    }
    else if(e.detail){//firefox
     if(e.detail < 0){
       pre();
     }
     else if(e.detail > 0){
       next();
     }
    }
  }

好了现在当我们滚动鼠标滚轮的时候就能调用相应的函数去修改列表中各列的id值了。在运行的时候,我会发现有两个问题,第一个问题就是由于各列默认情况下是没有被设置translateX值的,只有需要显示出来的列会通过translateX设定它们的位置取值,这样的后果就是当我们进行滑动轮盘时会如下图这样列9和列8重合,原因就是列9是刚被赋值了具有显示效果的id值,但列9本身是没有translateX值的,这样在被增加了一个translateX值后,它就直接会在所设定值的位置显示出来(在下图中也即列9会在列8本来所在的位置显示出来),而此时列8正在向转动轮盘之前的列7的位置移动中,一个直接在某位置显示出来,一个正从某位置开始移动走,因此就出现了下图这样的重合效果,同理往另一个方向滑动轮盘就是最左边的列出现重合效果。

这时候我们最先想到的解决办法当然是把此时最左和最右的列设定它俩CSS属性opacity的值为0,让它俩完全透明。

而第二个问题则是基于第一个问题的,假如我以极快的速度滑动滚轮过程中,这时候会出现如下图这样的情况:

停止滚动滚轮了然后列表才能恢复正常,上图这时候我是在极快的滚动鼠标滚轮转动方向是使轮盘从左往右滑动,这时候最右边的列由于没有了具备显示效果的id值因此会直接消失,当我滚动速度快的时候,就会从最右边开始很快的消失掉,而这时候左边新的列由于有滑动的动画效果于是还没移动过去,因此形成了上图这样的效果。后来我给每个列都设置了translateX值后发现还是不行,于是把所有列都显示出来,发现真正的原因是:当前显示在页面上的列表的最后一列都会在下一次滚动时直接挪动到第一列去,当极快速滚动滚轮时就还是前面说到的这个问题。所以现在看来似乎之前被我否决掉的那个第一种方案就不会出现这个问题。

如果不改变整个方案,也有些算不上解决办法的办法,比如我可以取消掉滚轮功能,改用按钮的方式进行轮盘的滚动,对了,鼠标滚轮滚动的幅度很大的情况下,尽管手指只是滚动了一次,但其实相当于滚动了多次,一次大的滚动幅度是由多次小幅度的滚动组合而成的,在这样的情况下也就不难理解为什么用滚轮这么容易发生重合的情况,而人为点击按钮可以最大程度避免了。

说到这里不知道你们有没有什么想法,在这么一分析滚轮的情况后,我直接找度娘询问了一番,还真有一个人提到了相关的办法,但他是用的jquery,可我现在就想用纯js实现啊,不然怎么好自称是从零单排js,他的大概思路是给滚轮事件触发时调用的函数里设置一个延迟执行,然后在该延迟函数调用前进行一个获取当前这个滚轮事件触发函数是否有延迟函数在执行有的话就取消掉。而这样做的效果就是,一次大幅度的鼠标滚轮滚动,在被细分为各个小幅度的滚动后,实际上只有最后一个小幅度的滚动中的转动轮盘函数有效。

所以接下来我们要解决的问题就是,如何通过js获得当前所在函数的延迟函数,并把它取消掉。我想到了可以通过全局变量的形式来对延迟函数进行保存,见下代码:

/*函数中的delayTime是一个初始值为null的全局变量*/   

  function scroll(e){
  if(delayTime!=null){
     clearTimeout(delayTime);
   }

/*通过setTimeout让函数在100毫秒后执行*/
delayTime = setTimeout(function(){
   e = e || window.event;
   if(e.wheelDelta){//IE,Opera,Chrome
     if(e.wheelDelta > 0){
       pre();
     }
     else if(e.wheelDelta < 0){
       next();
     }
    }
    else if(e.detail){//firefox
     if(e.detail < 0){
       pre();
     }
     else if(e.detail > 0){
       next();
     }
    }},100);
  }

到此为止,整个特效就完成了一半了,接下来是鼠标移动到可以被展开的列上时就把该列展开,由于此时我们各列的position属性为absolute,所以此时仅使用CSS的hover是没法把该列后边的列撑开的,而只会覆盖住。所以我们需要通过JS来对所展开的列的后边的列进行一个位移动作。当鼠标移入的某个列后触发javascript中鼠标的onmouseover事件,此时会根据当前鼠标移入的列进行判断,因为我之前设置的id为data3,data4,data5的列是可被展开的,所以对当前所移入的列进行判断,并执行相对应的动作即可。而鼠标移出是触发的onmouseout事件,其触发函数的功能同理于onmouseover事件的触发函数。

/*鼠标移入某列触发onmouseover事件所调用的函数*/
function mouseIn(){
   var thisId = this.getAttribute("id");
   var isdata3 = thisId=="data3" ? true:false;
   var isdata4 = thisId=="data4" ? true:false;
   var isdata5 = thisId=="data5" ? true:false;
   var objTag = document.getElementById(thisId);
   var buffer;
   if(isdata3){
     buffer = ["data4","data5","data6","data7","data8"];
     objTag.style.width = "100px";
   }

   else if(isdata4){
     buffer = ["data5","data6","data7","data8"];
     objTag.style.width = "100px";
   }

   else if(isdata5){
     buffer = ["data6","data7","data8"];
     objTag.style.width = "100px";
   }
   else{
     buffer = [];
   }
   for(var i=0;i<buffer.length;i++){
       var obj = document.getElementById(buffer[i]);
       var pos = parseInt(obj.style.left);
       pos += 80;
       obj.style.left = pos;
   }
  }

/*鼠标移出某列触发onmouseout事件所调用的函数*/
function mouseOut(){
   var thisId = this.getAttribute("id");
   var isdata3 = thisId=="data3" ? true:false;
   var isdata4 = thisId=="data4" ? true:false;
   var isdata5 = thisId=="data5" ? true:false;
   var buffer;
   if(isdata3){
     buffer = ["data4","data5","data6","data7","data8"];
     document.getElementById(thisId).style.width = "40px";
   }

   else if(isdata4){
     buffer = ["data5","data6","data7","data8"];
     document.getElementById(thisId).style.width = "40px";
   }

   else if(isdata5){
     buffer = ["data6","data7","data8"];
     document.getElementById(thisId).style.width = "40px";
   }
   else{
     buffer = [];
   }
   for(var i=0;i<buffer.length;i++){
       var obj = document.getElementById(buffer[i]);
       var pos = parseInt(obj.style.left);
       pos -= 80;
       obj.style.left = pos;
   }
  }

然后通过CSS的伪类hover让鼠标移动到可展开的列上时更改该列中显示更多信息的span标签的display属性,让其可见即可。

到这里依然有个bug,就是当鼠标在处于展开列上时,滚动鼠标滑轮,这时候会对其他列的位置移动造成影响,也考虑过当鼠标滚轮滚动时进行一个检查看是否右展开列,有的话,就执行一次鼠标移出时触发的收缩展开列的函数,这个解决思路,也是存在缺陷的,仔细一想你就会发现,依然会导致后续列表的移位问题,原因就是当我滚动滑轮执行了一次收缩动作,但是这时候你只要稍微移动鼠标,便又会触发一次onmouseout事件的触发函数,我的推测就是在页面上看虽然鼠标已经不在前面那个列中了,但是内存中的记录却是我的鼠标依旧还在前面那个列的对象上,这时候我稍微移动鼠标会触发一次检查发现当前鼠标已经不在之前那个列上了,所以又会调用一次onmouseout(如果我的理解有误,希望知道的朋友能指正一下,谢谢:P)

关于这个问题暂时没有什么好的解决思路,另外一些美化贴图之类的我就不再本系列文章中去做了,要不以后再开一个“从零单排web设计”的系列?哈哈,总之本期就先到这里吧。

本期小结


在本期的从零单排javascript中,我们使用到了以下javascript的知识:

  • javascript中对html标签的选择方法,如getElementById,getElementsByTagName,getAttribute等等。
  • 鼠标滚轮事件的用法
  • 延迟函数settimeout的妙用
  • 鼠标移入和移出事件的用法

第一次一边记录一边写程序,现在回过头来看,好些地方没写到位,没描述清楚。在文章开头我说会写的让编程零基础的同学也看得懂我在做什么,能在看完后了解编程的大致流程是什么样的,可是写到一大半的时候,我发现这件事并没有想象的那么简单,因为总觉得不把一些基础的东西先介绍介绍,就很难讲明白,然后写着写着语言也开始生硬起来了。关于功能的总体实现思路,肯定有很多不足的地方,欢迎各位分享一下自己的思路看法:)

Demo演示地址请戳 >>> 传送门

0 0