29

9. 三大系列

本篇一开始我们已经学了三大系列中的offset系列,三大系列分别是offset系列、scroll系列、client系列。学习这些有什么用呢?在后面的特效案例中,会大量的使用到获取元素的宽度、获取元素内部的宽度、获取元素距离顶部的距离等。这时候就需要用到三大系列,下面为大家一一讲解三大系列的用法。

9.1 offset 系列

第一章已经讲过了,详见第一章。

9.2 scroll 系列

scroll 是用来获取盒子内容的大小和位置。scroll家族有:scrollWidthscrollHeightscrollLeftscrollTop

1、onscroll 事件

前面DOM的时候,我们知道了触发事件,这里讲下onscroll事件。

对于有滚动条的盒子,可以使用onscroll注册滚动事件,每滚动一像素,就会触发该事件。

示例代码: [31-scroll系列-onscroll事件.html]

<!-- 样式部分 -->
<style>
    #box {
        width: 300px;
        height: 300px;
        border: 2px solid salmon;
        margin: 100px auto;
        /* 当内容超出盒子大小的时候 自动生成滚动条 */
        overflow: auto;
    }
</style>

<!-- html 部分 -->
<div id="box">
    我是内容我是内容我是内容我是内容我是内容我是内容我是内容
                            ...
                            ...
                            ...
    我是内容我是内容我是内容我是内容我是内容我是内容我是内容
</div>

<!-- js 部分 -->
<script>
    var box = document.getElementById('box');
    box.onscroll = function() {
        console.log("滚了!滚了");
    }
</script>

效果图:

image

2、scrollWidth 和 scrollHeight

scrollWidthscrollHeight是盒子内容的真实的宽度和高度。与和盒子大小无关,仅仅与盒子的内容有关系,不包括bordermargin,包括padding
scrollWidth = padding + width;

// 如果盒子里面的内容超出盒子高度的时候,这里的scrollHeight获取的就是内容的高度了
scrollHeight = padding + height;

示例代码: [32-scroll系列-scrollWidth&scrollHeight.html]

<!-- 样式部分 -->
<style>
    #box {
        width: 100px;
        height: 100px;
        border: 10px solid salmon;
        margin: 50px;
        padding: 10px;
    }
</style>

<!-- html 部分 -->
<div id="box">
    杨柳青青江水平,闻郎江上踏歌声。东边日出西边雨,道是无晴却有晴。
    杨柳青青江水平,闻郎江上踏歌声。东边日出西边雨,道是无晴却有晴。
</div>

<!-- js 部分 -->
<script>
    var box = document.getElementById('box');
    console.log(box.scrollWidth);    // 120
    console.log(box.scrollHeight);   // 241 获取的是内容的高度
</script>

效果图:

image

如果盒子里面的内容超出盒子高度的时候,这里的scrollHeight获取的就是内容的高度了

注意:

  • 在现代高版本浏览器中,scrollHeight,在内容没有超度盒子的情况下,获取到的高度是height+padding
  • 但是在IE8以下的时候,即使内容没有超出盒子,获取到的高度也是内容的高度。这里就不演示了,scrollHeight很少用到。

3、scrollTop 和 scrollLeft

scrollTop是盒子内容被滚动条卷去的头部的高度。scrollLeft是盒子内容被滚动条卷去的左侧的宽度。通常来说,scroll系列用的最多的地方就是用来获取页面被卷去的宽度和高度,非常的常用。

scrollTopscrollLeft存在兼容性

示例代码: [33-scroll系列-scrollTop&scrollLeft.html]

<!-- 样式部分 -->
<style>
    body {
        height: 5000px;
    }
</style>

<!-- js 部分 -->
<script>
    // 给页面注册滚动事件
    window.onscroll = function() {
        // 滚动条滚动一次,浏览器就会获取一次被卷去的头部的高度
        // 将高度赋值给title  
        // 获取scrollTop的时候是有兼容性的:现代浏览器用的是 window.pageYOffset
        // IE678 用的是 document.documentElement.scrollTop
        document.title = window.pageYOffset || document.documentElement.scrollTop;
    }
</script>

效果图:

image

完整版封装函数: [34-scroll系列-scrollTop&scrollLeft兼容性封装.html]

function getScroll() {
    // 返回的是一个对象,调用的时候 getScroll().top 获取页面被卷去的头部的距离
    return {
        left: window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft || 0,
        top: window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
    }
}

返回值是一个对象,需要获得卷去头部的距离,只需要调用getScroll.top即可。

scroll 系列图解:

image

示例代码:固定导航栏 [ 35-scroll系列-固定导航栏.html ]

  • 通过offsetHeight获取导航栏上部元素自身的高度,判断scrollTop的高度大于等于上部元素高度的时候,说明scrollTop到导航栏的位置了;
  • 到达导航栏位置,给导航栏绝对定位在页面顶部,同时因为固定定位,导航栏脱标,所以要获得导航栏下部的元素,将其margin-top设置为导航栏的高度,将位置空出来;
  • scrollTop小于上部元素高度的时候,导航栏去掉固定定位,同时将下部元素的margin-top设置为0

为什么一开始不直接拿导航栏到顶部的距离跟 scrollTop 比较呢?,因为导航栏固定定位之后位置就变了,恢复原来位置时的判断就不生效了

<!-- 样式部分 -->
<style>
    * {
        margin: 0;
        padding: 0;
        list-style: none;
    }
    html,
    body {
        width: 100%;
    }
    .header {
        height: 130px;
        background: #FBFBFB;
        font: 700 28px/130px serif;
        color: #666;
        text-align: center;
    }
    .nav {
        height: 60px;
        width: 100%;
        background: #B9E1DC;
        font: 700 24px/60px serif;
        color: #52524E;
        text-align: center;
    }
    ul {
        display: inline-block;
    }
    li {
        float: left;
        margin-left: 60px;
    }
    .content1,
    .content2,
    .content3 {
        height: 800px;
        background: #DFFCB5;
        font: 700 60px/800px serif;
        color: #52524E;
        text-align: center;
    }
    .content2 {
        background: #FFE1B6;
    }
    .content3 {
        background: #CDE3EB;
    }
    .fixed {
        position: fixed;
        top: 0;
        left: 0;
    }
</style>

<!-- html 部分 -->
<div class="header" id="header">
    顶部广告栏
</div>
<div class="nav" id="nav">
    <ul>
        <li>HOME</li>
        <li>ABOUT</li>
        <li>SERVICES</li>
        <li>TEAM</li>
        <li>CONTACT</li>
    </ul>
</div>
<div class="content1" id="con">
    内容1
</div>
<div class="content2">
    内容2
</div>
<div class="content3">
    内容3
</div>

<!-- js 部分 -->
<script>
    var header = document.getElementById('header');
    var nav = document.getElementById('nav');
    var content = document.getElementById('con');

    // 封装一个scrollTop兼容性函数
    function getScrollTop() {
        return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
    }

    // 给页面注册滚动事件
    window.onscroll = function() {
        // 判断广告栏header 与 滚动的scrollTop的值
        // 当scrollTop > header高度的时候 让导航栏 nav 固定定位
        var scrollTop = getScrollTop();
        if (scrollTop >= header.offsetHeight) {
            // 样式中有的类名这里一定不要忘了加上去,否则就会被替换掉
            nav.className = "nav fixed";
            // 一旦标题栏设置了固定定位之后,就脱离标准流了,下面的内容就会顶上来,
            // 所以要手动给下面的内容添加一个margin-top,将导航栏的位置留下来
            content.style.marginTop = nav.offsetHeight + "px";
        } else {
            // 当scrollTop < header高度的时候 让导航栏 nav 恢复到原来的位置
            // nav 取消固定定位,恢复到原来的位置,所以下面内容的margin-top也要去掉
            nav.className = "nav"; // 去掉固定定位的样式,保留之前的样式
            content.style.marginTop = 0;
        }
    };
</script>

效果图:

image

示例代码:两侧跟随小广告 [ 36-offset系列-两侧跟随小广告.html ]

  • 需求:屏幕滚动多少,两侧广告缓动等距离
  • 将两张图片绝对定位在屏幕两侧的中间
  • 通过滚动事件,实时获取scrollTop的值
  • 将获取到的scrollTop的值,通过缓动动画设置给两侧图片(需要将之前top的高度加上去)
<!-- html 部分 -->
<img src="../image/两侧固定小广告/advert.jpg" alt="" id="img1">
<img src="../image/两侧固定小广告/advert.jpg" alt="" id="img2">
<div>
    内 容
    . . .
    . . .
    
</div>

<!-- js 部分-->
<script>
    window.onload = function() {
        var imgs = document.getElementsByTagName('img');
        window.onscroll = function() {
            // 获取滚动条滚动距顶部的距离距离
            var scrollTop = getScrollTop();
            // 缓动跟随
            animate(imgs[0], scrollTop + 300);
            animate(imgs[1], scrollTop + 300);

        };
        // scrollTop兼容性处理
        function getScrollTop() {
            return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
        }
        // 缓动动画
        function animate(element, target) {
            clearInterval(element.timer);
            element.timer = setInterval(function() {
                var leader = element.offsetTop;
                var step = (target - leader) / 20;
                step = step > 0 ? Math.ceil(step) : Math.floor(step);
                leader += step;
                element.style.top = leader + 'px';
                if (Math.abs(target - leader) < Math.abs(step)) {
                    element.style.top = target + "px";
                    clearInterval(element.timer);
                }
            }, 15);
        }
    }
</script>

效果图:

image

示例代码:返回顶部 [ 37-offset系列-返回顶部.html ]

window.scrollTo(0, 0);让滚动条回到(0,0)位置。这是回到顶部的主要原理
  • 注册滚动条滚动事件,实时获取scrollTop的位置,判断当它距离大于等于800的时候,让回到顶部的按钮,缓动的显示出来。当scrollTop位置小于800的时候,让回到顶部的按钮,缓动的隐藏起来。
  • 给回到顶部按钮注册点击事件,当点击的时候,页面缓动的回到顶部。这里就要用到刚刚提到的知识点:window.scrollTo()
  • 重新创建一个缓动动画框架,目标位置target就是scrollTo(0,target),所以,target的值为0
  • 实现原理与最基本的缓动框架基本一样,只是将设置的值改为:window.scrollTo(0,leader);此时的leader还是一个未知数,leader其实就是当前滚动条的位置,所以,在滚动事件里,只要将leader实时获取滚动条位置即可。
<!-- 样式部分 -->
<style>
    body {
        background: #FDFCE0
    }
    div {
        margin: 200px auto;
        text-align: center;
    }
    img {
        width: 50px;
        height: 50px;
        background: url(../image/返回顶部/top.png);
        cursor: pointer;
        position: fixed;
        right: 50px;
        opacity: 0;
        bottom: 50px;
    }
</style>

<!-- html 部分 -->
<div>
    ...
    内容
    ...
</div>

<img src="../image/返回顶部/top.png" alt="" id="top">

<!-- js 部分 -->
<script src="../js/slow-animate-styles.js"></script>
<script>
    var img = document.getElementsByTagName('img')[0];
    var scrollTop;
    window.onscroll = function() {
        scrollTop = getScrollTop();
        // 当滚动条位置大于等于800 的时候,让回到顶部图标缓动的显示出来
        if (scrollTop >= 800) {
            slowAnimateStyles(img, {
                opacity: 100,

            });
            img.style.display = "block";
        } else {
            // 当位置小于800 的时候,回到顶部图标缓动的隐藏起来
            slowAnimateStyles(img, {
                opacity: 0,
            }, function() {
                img.style.display = "none";
            });

            // 在这里获取leader的位置
            leader = getScrollTop();
        }
    };
    // 点击img的时候,让滚动条回到顶部,这里有个知识点:window.scrollTo(0,0); 滚动条回到顶部
    // 需要单独创建一个缓动动画
    var timer = null;
    var target = 0; // 目标位置为0 即:window.scrollTo(0,target)
    var leader = 0; // 初始化leader
    img.onclick = function() {
        clearInterval(timer);
        timer = setInterval(function() {
            var step = (target - leader) / 10;
            step = step > 0 ? Math.ceil(step) : Math.floor(step);
            leader += step;
            // 此时的leader还是一个未知数,我们需要获取到当前滚动条的位置,然后赋值给leader,
            // 并且这个位置应该是实时变化的,我们只需要在上面的滚动事件里设置下leader即可
            window.scrollTo(0, leader);
            if (leader === 0) {
                clearInterval(timer);
            }
        }, 15);
    }

    // scrollTop兼容性处理
    function getScrollTop() {
        return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
    }
</script>

效果图:

image

示例代码:楼层跳跃 [ 38-offset系列-楼层跳跃.html ]

  • 其实这里的案例跟上面的返回顶部很类似,同样的运用到的是window.scrollTo()
  • 页面布局,背景继承body,和html100%,将背景的索引与左边导航栏绑定
  • 创建一个缓动动画框架,目标距离target,就是当前索引背景距离顶部的距离,leader就是滚动条此时的位置
<!-- html 部分 -->
<ul>
    <li>鞋子区域</li>
    <li>袜子区域</li>
    <li>裤子区域</li>
    <li>裙子区域</li>
    <li>帽子区域</li>
</ul>
<ol>
    <li>鞋子</li>
    <li>袜子</li>
    <li>裤子</li>
    <li>裙子</li>
    <li>帽子</li>
</ol>

<!-- js 部分 -->
<script>
    var colorArr = ["#B7F5DE", "#FFE9E3", "#CBF078", "#7CDFFF", "#F59292"];
    var ul = document.getElementsByTagName('ul')[0];
    var ol = document.getElementsByTagName('ol')[0];
    var ulLis = ul.getElementsByTagName('li');
    var olLis = ol.getElementsByTagName('li');
    var target = 0,
        leader = 0,
        timer = null;

    for (var i = 0; i < colorArr.length; i++) {
        // 动态设置背景颜色
        ulLis[i].style.background = colorArr[i];
        olLis[i].style.background = colorArr[i];
        // 将olLis属性绑定索引值
        olLis[i].index = i;
        olLis[i].onclick = function() {
            // 点击索引,获取ul下的当前li距离顶部的距离
            target = ulLis[this.index].offsetTop;
            clearInterval(timer);
            timer = setInterval(function() {
                var step = (target - leader) / 10;
                step = step > 0 ? Math.ceil(step) : Math.floor(step);
                leader += step;
                // 滚动条y方向到达leader位置
                window.scrollTo(0, leader);
                // 判断,清除定时器
                if (Math.abs(target - leader) <= Math.abs(step)) {
                    window.scrollTo(0, target);
                    clearInterval(timer);
                }
            }, 15);
        }
    }
    // 滚动事件,实时获取滚动条的位置
    window.onscroll = function() {
        // 实时获取leader的值
        leader = getScrollTop();
    }


    // scrollTop兼容性处理
    function getScrollTop() {
        return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;
    }
</script>

效果图:

image

9.3 client 系列

client家族用于获取盒子可视区的大小。client家族有clientWidthclientHeightclientLeftclientTop

1、clientWidth 和 clientHeight

  • clientWidth :获取网页可视区域宽度 ;clientHeight :获取网页可视区域高度;
  • 调用者不同,意义不同:

    • 盒子调用,指盒子本身;
    • html/body调用:可视区域大小
  • 不包括bordermargin

图解clientWidth和clientHeight:

image

clientWidth 和 clientHeight 兼容性封装:

function getClient() {
    return {
        width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth || 0,
        height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight || 0
    };
}

onresize事件:

onresize事件会在窗口被调整大小的时候发生。
window.onresize = function(){
    //事件处理程序
}

示例代码:模仿响应式布局 [ 39-client系列-模拟响应式.html ]

// 页面一进来的时候就执行一次,确定浏览器可视区域的宽度
responsive();
// 浏览器窗口调整触发事件
window.onresize = function() {
    responsive();
};

// 获取浏览器宽度
function responsive() {
    var pageWidth = getClientWidth();
    if (pageWidth >= 960) {
        //说明是pc
        document.body.style.backgroundColor = "#B7F5DE";
    } else if (pageWidth >= 640) {
        //说明是平板
        document.body.style.backgroundColor = "#CBF078";
    } else {
        // 说明是手机
        document.body.style.backgroundColor = "#F59292";
    }

}

// clientWidth 兼容性处理
function getClientWidth() {
    return window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth || 0;
}

效果图:

image

2、clientX 和 clientY

  • clientX:鼠标距离可视区域左侧距离(event调用) event:事件对象,下面会讲
  • clientY:鼠标距离可视区域上侧距离(event调用)

事件对象的时候,单独讲解

3、clientTop 和 clientLeft

  • clientTop:盒子的上部border的宽度
  • clientleft:盒子的左部border的宽度

用的很少很少,基本不会用到

9.4 screen 系列

clientWidth 获取的其实是浏览器窗口的宽度,想要获取用户显示的分辨率怎么办呢?

获取用户显示器分辨率有专门的方法:window.screen.widthwindow.screen.Height

示例代码:获取显示器分辨率 [ 40-screen系列-获取显示器分辨率.html ]

document.write("屏幕分辨率为:" + window.screen.width + "*" + window.screen.height);

效果图:

1280*720 分辨率的情况下:

image

1920*1080 分辨率的情况下:

image

9.5 三大系列的区别

图解三大系列区别:

image

width 和 height:

  • clientWidth/clientHeight

    • clientWidth = width + padding;
    • clientHeight = height + padding;
  • offsetWidth/offsetHeight

    • offsetWidth = width + padding + border;
    • offsetHeight = heigth + padding + border;
  • scrollWidth/scrollHeight

    • scrollWidth = 内容宽度(不包含border);
    • scrollHeight = 内容高度(不包含border);

top 和 left:

  • offsetTop/offsetLeft

    • 调用者:任意元素。(盒子为主)
    • 作用 :获取距离父系盒子中带有定位的距离。
  • scrollTop/scrollLeft:(盒子也可以调用,必须有滚动条)

    • 调用者:document.body.scrollTop/.....(window)
    • 作用:浏览器无法显示的部分(被卷去的部分)。
  • clientY/clientX:(clientTop/clientLeft 值是border)

    • 调用者:event.clientX(event)
    • 作用:鼠标距离浏览器可视区域的距离(左、上)。

10. 事件对象

10.1 事件对象的概述

在触发某个事件的时候,都会产生一个事件对象Event,这个对象中包含所有与事件相关的一些信息,包括触发事件的元素,事件的类型以及其他与事件相关的信息。

比如:

  • 鼠标事件触发时,事件对象中会包含鼠标的位置信息。
  • 键盘事件触发时,事件对象中会包含按下的键相关的信息。

10.2 获取事件对象

既然事件对象中存储了这么多的信息,我们首先需要做的就是获取到这个事件对象。获取事件对象的时候,存在浏览器的兼容问题。

现代浏览器:

获取事件对象非常的简单,只需要在注册事件的时候,指定一个形参即可。这个形参就是我们想要获取到的事件对象。

btn.onclick = function(event){
    // event就是事件对象,里面包含了事件触发时的一些信息。
    // 触发事件的时候,事件是由浏览器调用,生成一个事件对象,里面包含了一些信息,当成实参传递进来了。
    console.log(event);
}

IE678:

获取事件对象则是另一种方式,在事件里面,通过window.event来获取事件对象

btn.onclick = function(){
    // IE678通过window.event获取事件对象
    // IE678浏览器在触发的事件的时候,生成一个事件对象,但是呢,并没有当成实参传过来。会给window.event这个属性。
    var event = window.event;
    console.log(event);
}

兼容性封装: [ 41-事件对象Event兼容性.html ]

btn.onclick = function(event){
    //只要用到了事件对象,就要记得处理浏览器兼容性
    event = event || window.event;
}

10.3 事件对象的常用属性

事件对象中有很多很多的属性,但是很多属性并不常用。我们经常用到的是鼠标位置信息 和键盘码 相关的信息。

打印event对象我们可以看到如下信息:

image

我们可以看到一个鼠标按下的时候,它的事件对象里面有这么多属性,但是最常用的也就是鼠标位置信息和键盘码相关的信息。

记录了鼠标位置信息的相关属性:

  • screenXscreenY:光标相对于屏幕左上角的水平位置与垂直位置。
  • clientXclientY:光标相对于可视区左上角的水平位置和垂直位置。
  • pageXpageY:光标相对于网页(文档document)左上角的水平位置与垂直位置(推荐使用)

[ 42-事件对象-鼠标三种获取位置的属性.html ]

document.onclick = function(e) {
    var e = e || window.event;
    //获取鼠标的位置,相对的是可视区最左上角的点。(忽略滚动的距离)
    console.log("client(" + e.clientX + "," + e.clientY + ")");

    //获取鼠标的位置,相对的页面最左上角的位置 (计算滚动的距离)
    console.log("page(" + e.pageX + "," + e.pageY + ")");

    //获取鼠标的位置,相对的是屏幕最左上角的那个点
    console.log("screen(" + e.screenX + "," + e.screenY + ")");
}

图解:

image

记录了键盘码的属性:

  • event.keyCode:键盘按下的那个键的键盘码

10.4 pageX与pageY的兼容性

在鼠标事件中,记录鼠标位置信息的属性有很多,使用最多的还是pageXpageY这两个属性,但是pageXpageY存在浏览器兼容性问题。

在现代浏览器中: 直接通过事件对象就可以获得pageXpageY

document.onclick = function (event) {
    event = event || window.event;
    console.log(event.pageX+","+event.pageY);
}

在IE678中: 并没有pageXpageY,但是我们可以通过scrollTop + clientY的方式进行计算来获得pageY

document.onclick = function (event) {
    event = event || window.event;
    // 在IE678中使用document.documentElement.scrollTop就可以获取到scrollTop的值
    alert(event.clientY + document.documentElement.scrollTop);
}

pageX与pageY的兼容性封装:

function getPage(event) {
    return {
        //在IE678中使用document.documentElement.scrollLeft就可以获取到scrollLeft的值
        x:event.pageX || event.clientX + document.documentElement.scrollLeft,
        y:event.pageY || event.clientY + document.documentElement.scrollTop
    }
}

调用时:

getPage(event).x;
getPage(event).y;

示例代码:兼容性封装测试 [ 43-事件对象-pageX&PageY兼容性处理.html ]

<!-- 样式部分 -->
<style>
    body {
        height: 5000px;
    }
</style>

<!-- js 部分 -->
<script>
    document.onclick = function(event) {
        event = event || window.event;
        alert("当前坐标为(" + getPage(event).x + "," + getPage(event).y + ")");
    }

    function getPage(e) {
        return {
            x: e.pageX || e.clientX + document.documentElement.scrollLeft,
            y: e.pageY || e.clientY + document.documentElement.scrollTop
        }
    }
</script>

10.5 案例:鼠标跟随

  • 鼠标跟随,指的就是,鼠标后面有一张图片,会在页面中一直跟随鼠标
  • 通过事件对象的属性,我们知道了有三种方法获取鼠标的位置信息,我们只要把鼠标的位置,赋值给后面跟随图片的位置,就可以实现图片一直跟随鼠标移动了
  • 我们知道clientX/YscreenX/YpageX/Y,都可以获取鼠标的位置,但是各有优劣,我们先使用pageX/Y获取,上面我们已经处理pageX/Y的兼容性了,所以这里直接使用
  • 我们只需将获得的鼠标位置,赋值给定位后的图片,将图片绝对定位,再给页面注册鼠标移动事件,图片就会一直跟着鼠标移动。

[ 44-事件对象-跟随鼠标移动.html ]

<!-- 样式部分 -->
<style>
    * {
        margin: 0;
        padding: 0;
    }
    body {
        height: 5000px;
    }
    #follow {
        position: absolute;
        width: 160px;
    }
</style>

<!-- html 部分 -->
<img src="../image/鼠标跟随/2.gif" alt="" id="follow">

<!-- js 部分 -->
<script>
var follow = document.getElementById("follow");
//给document注册一个鼠标移动事件
document.onmousemove = function(e) {
    e = e || window.event;
    console.log(e.clientX + "   " + e.clientY);
    follow.style.left = getPage(e).x + "px";
    follow.style.top = getPage(e).y + "px";
}

function getPage(e) {
    return {
        x: e.pageX || e.clientX + document.documentElement.scrollLeft,
        y: e.pageY || e.clientY + document.documentElement.scrollTop,
    }
}

效果图:

image

通过效果图我们可以发现,绝对定位时,当鼠标移到最右边的时候,图片会撑大浏览器自动生成滚动条,那怎么办呢?

鼠标跟随优化版 [ 45-事件对象-跟随鼠标移动优化版.html ]
只要将图片固定定位,然后通过clientX/Y,获取可视区的位置,将它的值赋值给图片就行了

<!-- 样式部分 -->
<style>
    * {
        margin: 0;
        padding: 0;
    }
    body {
        height: 5000px;
    }
    #follow {
        position: fixed;
        width: 160px;
    }
</style>

<!-- html 部分 -->
<img src="../image/鼠标跟随/2.gif" alt="" id="follow">

<!-- js 部分 -->
<script>
    var follow = document.getElementById("follow");
    //给document注册一个鼠标移动事件
    document.onmousemove = function(e) {
        e = e || window.event;
        console.log(e.clientX + "   " + e.clientY);
        follow.style.left = e.clientX + "px";
        follow.style.top = e.clientY + "px";
    }
</script>

效果图:

image

10.6 案例:拖拽效果

1、获取鼠标在盒子中的位置

当在盒子里面点击鼠标的时候,怎么获得这个鼠标在盒子中的位置呢?

没有直接的方法能够获取,但是我们可以通过:

获取鼠标的位置 - 盒子距离顶部和左边的距离 = 鼠标在盒子里面的距离

[ 46-事件对象-获取鼠标在盒子中的位置.html ]

<!-- 样式部分 -->
<style>
    * {
        margin: 0;
        padding: 0;
    }
    #box {
        width: 150px;
        height: 150px;
        background: #b7f5de;
        margin: 200px;
    }
</style>

<!-- html 部分 -->
<div id="box"></div>

<!-- js 部分 -->
<script>
    var box = document.getElementById('box');

    box.onclick = function(e) {
        var e = e || window.event;
        // 没有直接的方法能够获取,但是我们可以通过:
        // 获取鼠标的位置 - 盒子距离顶部和左边的距离 = 鼠标在盒子里面的距离
        var x = getPage(e).x - box.offsetLeft;
        var y = getPage(e).y - box.offsetTop;
        console.log("当前位置坐标:(" + x + "," + y + ")");
    }

    function getPage(e) {
        return {
            x: e.pageX || e.clientX + document.documentElement.scrollLeft,
            y: e.pageY || e.clientY + document.documentElement.scrollTop
        }
    }
</script>

效果图:

image

2、拖拽效果

拖拽效果在网页很常见,比如一个注册框,弹出来的时候,你可以拖动它的位置。

新事件:

  • onmousedown :当鼠标按下的时候触发
  • onmouseup :当鼠标弹起的时候触发

实现思路:

  • 给盒子注册按下鼠标(onmousedown)事件,获取鼠标在盒子里的位置;
  • 然后在里面注册页面移动鼠标(onmousemove)事件,鼠标移动时,将此时的鼠标距浏览器的距离减去鼠标在盒子中的距离后,赋值给盒子的top/left
  • 给页面注册松开鼠标(onmouseup)事件,盒子应该停在那个位置,所以清除移动事件。

[ 47-事件对象-拖拽效果.html ]

<!-- 样式部分 -->
<style>
    * {
        margin: 0;
        padding: 0;
    }
    body {
        height: 4000px;
    }
    #box {
        width: 150px;
        height: 150px;
        background: #B7F5DE;
        position: absolute;
    }
</style>

<!-- html 部分 -->
<div id="box"></div>

<!-- js 部分 -->
<script>
    var box = document.getElementById('box');

    // 鼠标按下
    box.onmousedown = function(event) {
        event = event || window.event;
        // 记录按下的鼠标的位置
        var x = getPage(event).x - box.offsetLeft;
        var y = getPage(event).y - box.offsetTop;
        // 按下的时候才触发鼠标移动事件
        document.onmousemove = function(e) {
            // 鼠标点击的时候应该减去鼠标按下时在盒子中的位置
            box.style.left = getPage(e).x - x + "px";
            box.style.top = getPage(e).y - y + "px";
        }
    }

    // 鼠标松开
    // 这里为什么不给盒子注册鼠标松开事件呢? 因为一旦有延迟,鼠标不在盒子上松开的时候,move事件就清除不掉了
    // 所以直接给页面注册鼠标松开事件,只要鼠标松开,就清除move事件
    document.onmouseup = function() {
        // 上面注册的移动事件会一直触发,所以在鼠标松开的时候,我们应该将移动事件移除掉
        document.onmousemove = null;
    }

    // 获取事件对象里的pageX/Y属性
    function getPage(e) {
        return {
            x: e.pageX || e.clientX + document.documentElement.scrollLeft,
            y: e.pageY || e.clientY + document.documentElement.scrollTop
        }
    }
</script>

效果图:

image

注意:

  • 拖拽的时候,可能里面会有文字,当移动的时候,不小心获取文字焦点的时候,就不能清除鼠标移动事件了。

解决方法:
清除选中的文字

window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();

10.7 案例:放大镜

放大镜在开发中是一个很常见的特效,但是所有的放大镜的实现效果都是一样。

图解放大镜原理:

image

实现思路:

  • 当鼠标经过 smallBox 的时候,显示 maskbigBox
  • 当鼠标离开 smallBox 的时候,隐藏 maskbigBox
  • 获取鼠标在 smallBox 里面的位置;
  • 获得鼠标在 smallBox 里面的位置后,要减去 mask 一半的宽高,否则鼠标不在 mask 中间显示;
  • 判断x的值限定 mask 的位置 :

    • mask 在小盒子里面能够移动最大的宽度和高度 0
    • mask 在小盒子里面能够移动最大的宽度 = smallBox的宽度 - mask的宽度
  • 设定 mask 的位置;
  • 让大图片等比例的跟着动 :

    • bigImg 能够移动的距离 / mask 能移动的距离 = 大图片移动的距离 / mask移动的距离

思路图解:

image

示例代码: [ 48-事件对象-放大镜效果.html ]

<!-- 样式部分 -->
<style>
    * {
        margin: 0;
        padding: 0;
    }
    #box {
        width: 350px;
        height: 350px;
        margin: 100px;
        border: 1px solid #ccc;
        position: relative;
    }
    #bigBox {
        width: 400px;
        height: 400px;
        position: absolute;
        top: 0;
        left: 360px;
        border: 1px solid #ccc;
        overflow: hidden;
        display: none;
    }
    .mask {
        width: 175px;
        height: 175px;
        background-image: url(../image/放大镜/1.png);
        position: absolute;
        top: 1px;
        left: 1px;
        cursor: move;
        display: none;
    }
    #smallBox {
        position: relative;
    }
    #box img {
        vertical-align: top;
    }
    #bigBox img {
        position: absolute;
    }
</style>

<!-- html 部分 -->
<div id="box">
    <div id="smallBox">
        <img src="../image/放大镜/img.jpg" width="350" alt="">
        <div id="mask" class="mask"></div>
    </div>
    <div id="bigBox">
        <img src="../image/放大镜/img.jpg" width="800" id="bigImg" alt="" />
    </div>
</div>

<!-- js 部分 -->
<script>
    var box = document.getElementById('box');
    var smallBox = document.getElementById('smallBox');
    var bigBox = document.getElementById('bigBox');
    var mask = document.getElementById('mask');
    var bigImg = document.getElementById('bigImg');

    // 当鼠标 经过smallBox的时候,显示 mask 和 bigBox
    smallBox.onmouseover = function() {
        mask.style.display = "block";
        bigBox.style.display = "block";
    };
    // 当鼠标 离开smallBox的时候,隐藏 mask 和 bigBox
    smallBox.onmouseout = function() {
        mask.style.display = "none";
        bigBox.style.display = "none";
    };
    // 鼠标在smallBox里面移动,移动 mask 和 bigImg
    smallBox.onmousemove = function(e) {
        var e = e || window.event;
        // mask 跟着鼠标移动
        // 1- 获取鼠标在smallBox里面的位置
        var spaceX = getPage(e).x - box.offsetLeft;
        var spaceY = getPage(e).y - box.offsetTop;

        // 2- 获得鼠标在smallBox里面的位置后,要减去mask一半的宽高,否则鼠标不在mask中间显示
        var x = spaceX - mask.offsetWidth / 2;
        var y = spaceY - mask.offsetHeight / 2;

        // 3- 判断x的值 限定 mask的位置 
        // mask 在小盒子里面能够移动最大的宽度和高度 0 
        if (x <= 0) {
            x = 0;
        }
        if (y <= 0) {
            y = 0;
        }
        // mask 在小盒子里面能够移动最大的宽度 = smallBox的宽度 - mask的宽度
        var maskMaxX = smallBox.offsetWidth - mask.offsetWidth;
        var maskMaxY = smallBox.offsetHeight - mask.offsetHeight;
        if (x >= maskMaxX) {
            x = maskMaxX;
        }
        if (y >= maskMaxY) {
            y = maskMaxY;
        }

        // 4- 设定mask的位置
        mask.style.left = x + "px";
        mask.style.top = y + "px";

        // 5- 让大图片等比例的跟着动   
        // bigImg 能够移动的距离 / mask 能移动的距离 = 大图片移动的距离 / mask移动的距离
        // rate :比例
        var xRate = (bigImg.offsetWidth - bigBox.offsetWidth) / maskMaxX;
        bigImg.style.left = -xRate * x + "px";
        var yRate = (bigImg.offsetHeight - bigBox.offsetHeight) / maskMaxY;
        bigImg.style.top = -yRate * y + "px";
    };

    // pageX/Y 兼容性处理
    function getPage(e) {
        return {
            x: e.pageX || e.clientX + document.documentElement.scrollLeft,
            y: e.pageY || e.clientY + document.documentElement.scrollTop
        }
    }
</script>

效果图:

image

11. 注册事件

前面我们已经知道了许多触发事件的名称,但是我们只知道了一种注册事件的方式,就是"on + 事件名称",下面会为大家再介绍一种注册事件的方式:addEventListener

11.1 on + 事件名称 方式

onclickonmouseover这种on+ 事件名称的方式注册事件几乎所有的浏览器都支持。

注册事件:

box.onclick = function(){
    //事件处理程序    
}

移除事件:

box.onclick = null;

on + 事件名称注册事件的缺点:

同一个元素同一类型的事件,只能注册一个,如果注册了多个,会出现覆盖问题。

document.onclick = function(){
    console.log("呵呵");
}

document.onclick = function(){
    console.log("哈哈");   // 最后打印的是 "哈哈",呵呵会被覆盖掉
}

11.2 addEventListener 方式

现代浏览器支持的注册事件的新方式,这种方式注册的事件不会出现覆盖问题。以后在手机端就用这种注册事件的方式。

1、addEventListener 注册事件的语法:

// add:添加   Event:事件   Listener:监听器
// 三个参数:
//      1. type:事件类型  "click","mouseover"... 不要再加 on了
//      2. 函数:事件触发的时候要执行的程序
//      3. useCapture(是否使用事件捕获) :true & false 默认是false
document.addEventListener(type,function(){
    // 事件处理程序
},useCapture);

之前我们说过window.onload,只能注册一个就是这个原因,因为"on +"注册方式会覆盖。所以如果真的需要执行两个window.onload事件的时候,我们就可以使用addEventListener注册:

window.addEventListener("load",function(){
    // 预加载函数 1
});

window.addEventListener("load",function(){
    // 预加载函数 2
});

示例代码: [ 49-注册事件-addEventListener.html ]

// 给页面注册点击事件后,会同时打印 "呵呵呵","哈哈哈"
document.addEventListener("click", function() {
    console.log("呵呵呵");
});
document.addEventListener("click", function() {
    console.log("哈哈哈");
});

2、removeEventListener 移除事件的语法:

// remove:移除   Event:事件   Listener:监听器
// 三个参数:
//      1. type:事件类型  "click","mouseover"
//      2. 函数名:要移除的那个函数
//      3. useCapture(是否使用事件捕获) :true & false 默认是false
document.addEventListener(type,fn,useCapture);

注意:

要想一个事件能够被移除,在它注册事件的时候,执行函数必须要有函数名,不能是匿名函数。因为移除事件的时候,就是移除的这个函数名。

示例代码: [ 50-移除事件-removeEventListener.html ]

// 第二个点击事件就被移除了
document.addEventListener("click", fn1);
document.addEventListener("click", fn2);

function fn1() {
    console.log("呵呵呵");
};

function fn2() {
    console.log("哈哈哈");
}

// 移除第二个点击事件
document.removeEventListener("click", fn2); 

3、IE678兼容性问题:

IE678不支持addEventListenerremoveEventListen两个方法,但是支持attachEventdetachEvnet

attachEvent注册事件的语法:

// attach :附上;系上;贴上 
// 参数:
//     1. type:事件类型   需要加上on "onclick","onmouseenter"...
//     2. 函数fn:需要执行的那个事件函数
attachEvent(type, function(){
    // 事件处理程序
});

attach注册时间的时候,事件类型要加上on,没有为什么,IE就这样

detachEvent的用法:

// detach :脱离
// 参数:
//     1. type:事件类型   需要加上on "onclick","onmouseenter"...
//     2. 函数名: 需要执行的那个事件函数名
detachEvent(type, fn);

示例代码: [ 51-注册事件-IE678方法.html ]

// IE678 下运行
document.attachEvent("onclick", fn1);
document.attachEvent("onclick", fn2);

function fn1() {
    alert("123");
};

function fn2() {
    alert("456");
};

// 移除第一个注册事件
document.detachEvent("onclick", fn1);  

4、兼容性处理:

注册事件的新方式的解决了事件覆盖的问题,但是存在浏览器兼容性问题,因此可以进行兼容性封装。
// 添加事件兼容性封装
function addEvent(element, type, fn) {
    // 能力检测
    if (element.addEventListener) {
        element.addEventListener(type, fn);
    } else if (element.attachEvent) {
        element.attachEvent("on" + type, fn);
    } else {
        //如果都不行,那就用on方式
        element["on" + type] = fn;
    }
};

//移除事件兼容性封装
function removeEvent(element, type, fn) {
    if (element.removeEventListener) {
        element.removeEventListener(type, fn);
    } else if (element.detachEvent) {
        element.detachEvent("on" + type, fn);
    } else {
        element["on" + type] = null;
    }
}

示例代码: [ 52-注册事件-封装兼容性.html ]

// 添加事件兼容性封装
function addEvent(element, type, fn) {
    // 能力检测
    if (element.addEventListener) {
        element.addEventListener(type, fn);
    } else if (element.attachEvent) {
        element.attachEvent("on" + type, fn);
    } else {
        //如果都不行,那就用on方式
        element["on" + type] = fn;
    }
};

//移除事件兼容性封装
function removeEvent(element, type, fn) {
    if (element.removeEventListener) {
        element.removeEventListener(type, fn);
    } else if (element.detachEvent) {
        element.detachEvent("on" + type, fn);
    } else {
        element["on" + type] = null;
    }
}

function fn1() {
    alert("呵呵");
}

function fn2() {
    alert("哈哈");
}

addEvent(document, "click", fn1);
addEvent(document, "click", fn2);
removeEvent(document, "click", fn1);

12. 事件冒泡和事件捕获

事件冒泡和事件捕获其实可以理解成一样东西,就是当父级元素和子元素都具有点击事件的时候,点击触发子级元素的时候父级元素也会被触发。事件冒泡是IE678在处理事件间机制的一种说法,它执行的顺序是由内向外的,就是从子元素一直到window。 事件捕获是火狐在处理机制时的一种说法,它执行的顺序是由外向内的,就是window一直到子元素。

图解事件冒泡和事件捕获:

image

12.1 事件冒泡

当一个元素的事件被触发时,同样的事件将会在该元素的所有祖先元素中依次被触发。这一过程被称为事件冒泡。

说白了就是:当父元素和子元素都设置了点击事件的时候,触发子盒子点击事件的时候,父盒子的点击事件也会被执行。

image

示例代码: [ 53-事件冒泡.html ]

<!-- 样式部分 -->
<style>
        #big-box {
            width: 500px;
            height: 500px;
            margin: 100px auto;
            vertical-align: middle;
            text-align: center;
            border: 1px solid transparent;
            background-color: aquamarine;
        }
        
        #box {
            width: 300px;
            height: 300px;
            margin-top: 100px;
            display: inline-block;
            background-color: darkorange;
        }
</style>

<!-- html 部分 -->
<div id="big-box">
    <div id="box"></div>
</div>

<!-- js 部分 -->
<script>
    var bigBox = document.getElementById('big-box');
    var box = document.getElementById('box'); 
    document.onclick = function() {
        document.body.style.background = "#000";
    }
    bigBox.onclick = function() {
        bigBox.style.background = "fuchsia";
    }
    box.onclick = function() {
        // 当我们点击box的时候,bigBox、body的点击事件 也会被触发
        box.style.background = "lightgreen";
    }
    </script>

效果图:

image

我们会发现,当点击中间小盒子的时候,他的父级元素,只要有点击事件的,都被触发了,这就是事件冒泡。

12.2 阻止事件冒泡

正常情况下,我们肯定不想,点击子元素触发事件的时候,父元素事件也跟着触发,所以我们就要知道一个知识点:阻止事件冒泡。在阻止事件冒泡中是存在兼容性的:

正常浏览器:

前面我们知道了事件触发的时候,会有一个事件对象,我们只要给事件对象加上:stopPropagation方法即可。stopPropagation方法不仅可以阻止事件冒泡,还可以阻止事件委托

element.onclick = function (e) {
    e = event || window.event;
    //stop :停止  propagation:传播
    e.stopPropagation();
}

IE678浏览器:

ie是给事件对象的属性cancelBubble赋值

element.onclick = function (e) {
    e = event || window.event;   
    e.cancelBubble = true;
}

兼容性处理:

// 能力检测
element.onclick = function (e) {
    e = event || window.event;
    if(e.stopPropagation){
          e.stopPropagation();
    }else {
          e.cancelBubble = true;
    }
}

12.3 事件捕获

事件捕获(capture)是火狐浏览器提出来的,IE678不支持事件捕获(基本上,我们都是用事件冒泡)。事件的处理将从DOM层次的根开始,而不是从触发事件的目标元素开始,事件被从目标元素的所有祖先元素依次往下传递。

image

addEventListener第三个参数为true时,表示事件捕获:

element.addEventListener("click", function () {
    console.log("哈哈哈");
},true);  

12.4 事件流

事件流就是事件的三个阶段,首先发生的是捕获阶段,然后是目标阶段,最后才是冒泡阶段,对于捕获和冒泡,我们只能干预其中的一个,通常来说,我们可能会干预事件冒泡阶段,而不去干预事件捕获阶段。
  • 事件的捕获阶段
  • 事件的目标阶段
  • 事件的冒泡阶段

image

注意:

其实这三个阶段在执行是都会发生,但是冒泡和捕获只能执行一个,所以通过usecaptrue = false可以让捕获阶段执行但是不触发。

12.5 键盘事件

对于鼠标事件,事件对象中有一系列的XY记录了鼠标的位置信息。而键盘事件中,事件对象有一个event.keyCode属性,记录了按下去的键的键盘码。[ 54-键盘事件-键盘码.html ]
document.onkeydown = function (e) {
    // 键盘按下的时候触发的事件对象 
    console.log(e);
    // keyCode: 键盘码
    console.log(e.keyCode);
}

image

键盘码对应值:

image

常见的键盘事件:

  • onkeydown:键盘按下时触发
  • onkeyup:键盘弹起时触发

示例代码: [ 55-键盘事件-ESC键关闭遮罩层弹出框.html ]

// 点击登陆按钮
var btn = document.getElementById('btn');
// 登陆框
var login = document.getElementById('login');
// 遮罩层
var bg = document.getElementById('bg');
btn.addEventListener("click", function() {
    login.style.display = "block";
    bg.style.display = "block";
});
document.addEventListener("keyup", function(e) {
    e = e || window.event;
    // ESC 键的键盘码是27
    if (e.keyCode == 27) {
        login.style.display = "none";
        bg.style.display = "none";
    }
});

效果图:

image

12.6 案例:弹幕效果

我们都看过直播,都知道弹幕的效果,下面我们就模拟直播中的弹幕做个小案例。

实现步骤:

  • 获取输入框的的 value 值;并生成 span 标签
  • span 标签添加到 页面中,随机颜色 随机高度 span动画从右向左
  • 到达最左边的时候删除 span 标签(不删除会随着输入的内容越来越多影响性能)

示例代码:

<style>
    html,
    body {
        margin: 0px;
        padding: 0px;
        width: 100%;
        height: 100%;
        font-family: "微软雅黑";
        font-size: 62.5%;
        background: #ccc;
    }
    #page {
        width: 100%;
        height: 100%;
        position: relative;
        overflow: hidden;
    }
    #import {
        width: 100%;
        height: 60px;
        background: #666;
        position: fixed;
        bottom: 0px;
    }
    #content {
        display: inline-block;
        width: 430px;
        height: 40px;
        position: absolute;
        left: 0px;
        right: 0px;
        top: 0px;
        bottom: 0px;
        margin: auto;
    }
    .title {
        display: inline;
        font-size: 25px;
        vertical-align: bottom;
        color: #fff;
    }
    #text {
        border: none;
        width: 300px;
        height: 30px;
        border-radius: 5px;
        font-size: 15px;
        padding-left: 10px;
    }
    #btn {
        width: 60px;
        height: 30px;
        background: #f90000;
        border: none;
        color: #fff;
        font-size: 15px;
    }
    span {
        width: 300px;
        height: 40px;
        position: absolute;
        overflow: hidden;
        color: #000;
        font-size: 25px;
        line-height: 37.5px;
        cursor: pointer;
        white-space: nowrap;
    }
</style>

<!-- html 部分-->
<div id="page">
    <div id="import">
        <div id="content">
            <p class="title">吐槽</p>
            <input type="text" name="" id="text" placeholder="发送弹幕,与小伙伴一起互动!">
            <button id="btn">发射</button>
        </div>
    </div>
</div>

<!-- js 部分 -->
<script src="../js/animate-callback.js"></script>
<script>
    var page = document.getElementById('page');
    var text = document.getElementById('text');
    var btn = document.getElementById('btn');

    // 定义一个颜色数组
    var colorArr = ['#FF895D', '#78BBE6', '#FF4273', '#00BBF0', '#7C73E6', '#EE2B47', '#F60C86', '#9870FC', '#F96D00', '#303481'];

    btn.onclick = function() {
        // 点击发射按钮的时候,要做的事情:
        // 1- 获取 input 的 value 值;并生成 span 标签
        // 2- 将 span 标签添加到 page中,随机颜色 随机高度 span动画从右向左
        // 3- 到达最左边的时候删除 span 标签(不删除会随着输入的内容越来越多影响性能)
        // a. 获取input的值,并清空
        var content = text.value;
        // b. 生成span标签 添加到 page中
        if (content != "" && content.trim()) {
            text.value = '';
            var span = document.createElement('span');
            page.appendChild(span);
            span.innerText = content;
            // c. 随机颜色
            var randomColor = parseInt(Math.random() * colorArr.length);
            span.style.color = colorArr[randomColor];
            // d. 随机高度  随机位置
            var randomHeight = parseInt(Math.random() * 201);
            span.style.top = randomHeight + "px";
            span.style.left = getClient().width + "px";
            // e. 动画效果
            animate(span, -300, function() {
                // f. 动画执行完成之后,回调函数中移除执行完的 span
                page.removeChild(span);
            });
        }
    };

    // text 注册键盘按下事件 当为回车按键的时候,执行发射操作
    text.onkeydown = function(e) {
        e = e || window.event;
        if (e.keyCode == 13) {
            btn.click();
        }
    }

    // 获取可视区域宽高
    function getClient() {
        return {
            width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth || 0,
            height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight || 0
        }
    }
</script>

效果图:

image

13. 瀑布流

瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。

image

1、首先瀑布流所有的图片应该保持宽度一致,高度是由内容决定。

image

左浮动的话,我们可以看到第6个盒子直接就在第4个盒子旁边停下了,因为第4个高度最高,挡住了它左浮动的去路。第6个盒子是第2行的最后一个,所以第7个盒子只能在第3行排列了。当排到第12个盒子的时候,盒子会以第11个盒子的位置为基础左浮动(这就是第12个盒子为什么没有‘跳到’第9个盒子下面的原因),碰到第8个盒子后又被挡住了。

image

通过定位的方式是我们实现瀑布流的最基本的原理,只要我们动态的设置它的top值、left值,就能让它排列。

2、定位后确定浏览器显示区域内,一行能放多少列图片盒子。

  • 获取页面的宽度
  • 获取图片盒子的宽度
  • 显示的列数 = 页面宽度/图片盒子宽度
  • column = pageWidth / itemWidth

image

3、为了美观我们可以加上一个空隙

  • 显示的列数 = 页面宽度/(图片盒子宽度+间隙);
  • column = pageWidth / (itemWidth + gap);

image

4、 确定列数之后,排列第一行

  • 下面还有很多图片盒子,我们先要排列第1行,所以在for循环里就要判断一下,当i(所有图片盒子的索引) < column(显示列数)的时候,说明在第1行;
  • 知道在第1行之后,动态设置每个图片盒子的left值就能排好第1行。
  • left = i * ( itemWidth + gap );

image

5、第1行排列好之后,获取第1行所有图片盒子的高度

  • 需要定义一个数组arr,将获取到的高度存在数组中,因为第2行排列的时候需要考虑top值,此时只能根据第1行图片盒子的高度来设置;
  • 获取图片高度的时候要注意,程序必须写在入口函数onload里面,因为图片的加载特性是:等页面都加载完之后才去请求加载,所以不写在入口函数里可能会出现高度获取不到的情况。

image

6、排列第2行

  • 获取到刚刚数组中,高度最小的那一列,将第2行的第1个图片盒子放置在它的下方;
  • 此时的left值就是高度最小列的offsetLefttop值就是:第1行高度最小列的高度(为了布局美观可以加上上下间隙gap)。
  • 记录下高度最小列的索引index,后面计算会用到;
  • 设置完成之后,会发现后面所有的图片都叠在这个高度最小列的下面,原因就是此时的最小列高度没有改变,应该加上下面图片的高度,得出一个新高度。

image

7、改变最小列当前高度

  • 此时的这一列高度其实已经发生改变了,所以需要将新高度赋值给数组
  • 当前高度最小列的高度 = 当前高度最小列的高度 + 间隙 + 下面图片盒子的高度

image

8、触发resize事件

  • 将整个设置样式的部分封装成一个函数,在onload里面注册一个resize事件,只要页面一发生改变,就触发样式部分的代码。
  • 实时改变pageWidth的宽度,这样瀑布流就会是一个响应式的效果了

9、懒加载效果

  • 目前我们用的是30张图片,假如一个页面中有几百张图片的时候,我们不可能等到它都加载完再显示,所有这里引入一个懒加载的概念,我们规定第30张为显示的最后一张图片,当滚动条滚动到30张的时候,应该加载下一批图片。
  • 即页面可视区高度+滚动条卷去的高度 = 第30图片的offsetTop;的时候加载下面的图片。

image

完整代码:

<!-- 样式部分 -->
<style>
    * {
        margin: 0;
        padding: 0;
        position: relative;
    }
    
    img {
        width: 220px;
        display: block;
    }
    
    .item {
        box-shadow: 2px 2px 2px #999;
        position: absolute;
    }
</style>

<!-- html 部分 -->
<div id="box">
    <div class="item"><img src="../image/瀑布流/001.jpg" alt=""></div>
                                .
                                .
                                .
    <div class="item"><img src="../image/瀑布流/030.jpg" alt=""></div>
</div>

<!-- js 部分 -->
<script>
    var box = document.getElementById('box');
    var items = box.children;
    // 定义每一列之间的间隙 为10像素
    var gap = 10;

    window.onload = function() {
        // 一进来就调用一次
        waterFall();
        // 封装成一个函数
        function waterFall() {
            // 1- 确定列数  = 页面的宽度 / 图片的宽度
            var pageWidth = getClient().width;
            var itemWidth = items[0].offsetWidth;
            var columns = parseInt(pageWidth / (itemWidth + gap));
            var arr = [];
            for (var i = 0; i < items.length; i++) {
                if (i < columns) {
                    // 2- 确定第一行
                    items[i].style.top = 0;
                    items[i].style.left = (itemWidth + gap) * i + 'px';
                    arr.push(items[i].offsetHeight);

                } else {
                    // 其他行
                    // 3- 找到数组中最小高度  和 它的索引
                    var minHeight = arr[0];
                    var index = 0;
                    for (var j = 0; j < arr.length; j++) {
                        if (minHeight > arr[j]) {
                            minHeight = arr[j];
                            index = j;
                        }
                    }
                    // 4- 设置下一行的第一个盒子位置
                    // top值就是最小列的高度 + gap
                    items[i].style.top = arr[index] + gap + 'px';
                    // left值就是最小列距离左边的距离
                    items[i].style.left = items[index].offsetLeft + 'px';

                    // 5- 修改最小列的高度 
                    // 最小列的高度 = 当前自己的高度 + 拼接过来的高度 + 间隙的高度
                    arr[index] = arr[index] + items[i].offsetHeight + gap;
                }
            }
        }
        // 页面尺寸改变时实时触发
        window.onresize = function() {
            waterFall();
        };
        // 当加载到第30张的时候
        window.onscroll = function() {
            if (getClient().height + getScrollTop() >= items[items.length - 1].offsetTop) {
                // 模拟 ajax 获取数据    
                var datas = [
                    "../image/瀑布流/001.jpg",
                            ...
                    "../image/瀑布流/030.jpg"
                ];
                for (var i = 0; i < datas.length; i++) {
                    var div = document.createElement("div");
                    div.className = "item";
                    div.innerHTML = '<img src="' + datas[i] + '" alt="">';
                    box.appendChild(div);
                }
                waterFall();
            }

        };
    };

    // clientWidth 处理兼容性
    function getClient() {
        return {
            width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
            height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
        }
    }
    // scrollTop兼容性处理
    function getScrollTop() {
        return window.pageYOffset || document.documentElement.scrollTop;
    }
</script>

效果图:

image

14. 正则表达式

正则表达式:用于匹配规律规则的表达式,正则表达式最初是科学家对人类神经系统的工作原理的早起研究,现在在编程语言中有广泛的应用,经常用于表单校验,高级搜索等。

14.1 创建正则表达式

js中的正则表达式用RegExp对象表示,可以通过RegExp()构造函数来创建RegExp对象,不过更多的是通过字面量语法来创建。

/.../正则表达式必须要有斜杠,它表示的是正则构成

构造函数的方式:

var regExp = new RegExp(/abc/); // 判断是否包含字符abc

正则字面量:/.../

var regExp = /abc/;

正则的使用:

正则表达式有一个方法:test(); 有一个返回值,是布尔类型。决定参数是否符合正则表达式

console.log(/abc/.test("abc")); // true

示例代码: [ 57-正则表达式-创建正则表达式.html ]

var reg = new RegExp(/abc/);
console.log(reg.test("abc"));   // ture
console.log(reg.test("efg"));   // false
console.log(reg.test("abcd"));  // true   只要包含abc就正确 后面会细讲
console.log(/123/.test(123));   // true

14.2 元字符

正则表达式由一些普通字符元字符组成,普通字符包括大小写字母、数字等,而元字符则具有特殊的意义。元字符:^...

14.3 正则内部类

1、预定义类

正则表达式中具有特殊意义的字符。
预定义类 正则形式 释义
. [^\n\r] 除了换行和回车之外的任意字符
\d [0-9] 数字字符
\D [^0-9] 非数字字符
\w [a-zA-Z0-9_] 单词字符(所有的字母数字和'_')
\W [^a-zA-Z0-9_] 非单词字符
\s [\f\r\n\t\v] 不可见字符,包含空格
\S [^\f\r\n\t\v] 可见字符

示例代码: [ 59-正则表达式-预定义类.html ]

console.log("----------------'.'---------------");
console.log(/./.test('\n'));        // false
console.log(/./.test('2s#2'))       // true

console.log("----------------'\\d'---------------");
console.log(/\d/.test(123));        // true
console.log(/\d/.test('123abc'));   // true
console.log(/\d/.test('abc'));      // false

console.log("----------------'\\D'---------------");
console.log(/\D/.test(123));        // false
console.log(/\D/.test('123abc'));   // true
console.log(/\D/.test('abc'));      // true

console.log("----------------'\\w'---------------");
console.log(/\w/.test(123));        // true
console.log(/\w/.test('123abc_'));  // true
console.log(/\w/.test(' '));        // false

console.log("----------------'\\W'---------------");
console.log(/\W/.test(123));        // false
console.log(/\W/.test('123abc_'));  // false
console.log(/\W/.test(' '));        // true

console.log("----------------'\\s'---------------");
console.log(/\s/.test(123));        // false
console.log(/\s/.test('123abc_'));  // false
console.log(/\s/.test(' '));        // true

console.log("----------------'\\S'---------------");
console.log(/\S/.test(123));        // true
console.log(/\S/.test('123abc_'));  // true
console.log(/\S/.test(' '));        // false

2、简单类 [ 60-正则表达式-简单类.html ]

/ /中什么特殊符号也不写,就是简单类

直接字符: 必须是完整的包含正则选项,只能多不能少

console.log(/levi/.test('levi'));       // true
console.log(/levi/.test('le'));         // false
console.log(/levi/.test('levi_lxh'));   // true

只要完整的包含了“levi”即可(有他就行)

加上[] 只要包含里面任何一个即可 比如/[abcd]/ => a,b,c,d只要匹配项的里面有任意一项符合就返回true

console.log(/[levi]/.test("le"));       // true
console.log(/[levi]/.test("less"));     // true
console.log(/[levi]/.test("kill"));     // true
console.log(/[levi]/.test("ss"));       // false

3、负向类

元字符^必须出现在中括号内,表示非、取反的意思[^]

示例代码: [ 61-正则表达式-负向类.html ]

console.log(/[^levi]/.test("l"));           // false
console.log(/[^levi]/.test("le"));          // false
console.log(/[^levi]/.test("ec"));          // true
console.log(/[^levi]/.test("levi"));        // false
console.log(/[^levi]/.test("levi-lxh"));    // true
console.log(/[^levi]/.test("lxh"));         // true

条件项[^levi],表示不能有l,e,v, i任意组合,当匹配项小于等于条件项并且包含条件项的时候,返回false,当返回项不完全包含条件项的时候,返回true

4、范围类:

有时候匹配的东西过多,而且类型又相同,全部输入太麻烦,我们可以在中间加个横线-

示例代码: [ 62-正则表达式-范围类.html ]

console.log(/[a-d]/.test("a"));     // true
console.log(/[a-d]/.test("ac123")); // true
console.log(/[a-d]/.test("efg"))    // false
console.log(/[a-d]/.test("123"))    // false

5、组合类

用中括号匹配不同类型的单个字符串

示例代码: [ 63-正则表达式-组合类.html ]

console.log(/[a-f1-6]/.test('abs'));    // true
console.log(/[a-f1-6]/.test('12'));     // true
console.log(/[a-f1-6]/.test('sg8'));    // false

14.4 正则边界

我们前面学习的正则只要有满足的条件的就会返回true,并不能做到精确的匹配。正则边界就是以什么开始,以什么结束,进行精确匹配。

1、以什么开始:

^元字符在//里面的时候,表示的是必须以...开始^在中括号[]内才表示取反、非的意思。
console.log(/^levi/.test('lxhlevi'));       // false
console.log(/^levi/.test('levilxh'));       // true
console.log(/^levi/.test('lxhlevilxh'));    // false
console.log(/^levi/.test('levilevi'));      // true

2、以什么结尾:

$ 元字符表示的是必须以...结尾
console.log(/levi$/.test('lxhlevi'));       // true
console.log(/levi$/.test('levilxh'));       // false
console.log(/levi$/.test('lxhlevilxh'));    // false
console.log(/levi$/.test('levilevi'));      // true

3、精确匹配:

^...$表示的是精确匹配,匹配项必须是^、$之间的内容
console.log(/^levi$/.test('lxhlevi'));      // false
console.log(/^levi$/.test('levilxh'));      // false
console.log(/^levi$/.test('lxhlevilxh'));   // false
console.log(/^levi$/.test('levilevi'));     // false
console.log(/^levi$/.test('levi'));         // true
console.log(/^\d$/.test('111'));            // false  \d表示的是0-9当中的一位数
console.log(/^\d$/.test('1'));              // true

[ 64-正则表达式-正则边界.html ]

14.5 量词

量词用来控制出现的次数,一般来说量词和边界会一起使用

1、量词 *

表示能够出现0次,或者跟多的次数,x >= 0
// 可以出现0次或者多次  要么不出现 要么只能出现 a
console.log(/^a*$/.test('abc'));    // false
console.log(/^a*$/.test('bbb'));    // false
console.log(/^a*$/.test('aab'));    // false
console.log(/^a*$/.test('aaa'));    // true
console.log(/^a*$/.test('a'));      // true
console.log(/^a*$/.test(''));       // true

2、量词 +

表示能够出现1次或者多次,x >= 1
// +表示 可以出现1次或者1次以上
console.log(/^a+$/.test("a"));      //true
console.log(/^a+$/.test(""));       //false
console.log(/^a+$/.test("b"));      //false
console.log(/^a+$/.test("aa"));     //true
console.log(/^a+$/.test("aab"));    //false

3、量词 ?

表示能够出现0次或者1次,x=0或者x=1
// ? 表示可以出现0次或者1次
console.log(/^a?$/.test("a"));      //true
console.log(/^a?$/.test(""));       //true
console.log(/^a?$/.test("b"));      //false
console.log(/^a?$/.test("aa"));     //false
console.log(/^a?$/.test("aab"));    //false

4、量词 {n}

表示能够出现n
// * ==> {0,}
console.log(/^a{0,}$/.test('a'));    // true
console.log(/^a{0,}$/.test('aa'));   // true
console.log(/^a{0,}$/.test(''));     // true
console.log(/^a{0,}$/.test('abc'));  // fasle
console.log(/^a{0,}$/.test('aaab')); // fasle

5、量词 {n,}

表示能够出现n次或者n次以上
// + ==> {1,}
console.log(/^a{1,}$/.test('a'));    // true
console.log(/^a{1,}$/.test('aa'));   // true
console.log(/^a{1,}$/.test(''));     // fasle
console.log(/^a{1,}$/.test('abc'));  // fasle
console.log(/^a{1,}$/.test('aaab')); // fasle

6、量词 {n,m}

表示能够出现n-m次
// ? ==> {0,1}
console.log(/^a{0,1}$/.test("a"));   //true
console.log(/^a{0,1}$/.test(""));    //true
console.log(/^a{0,1}$/.test("b"));   //false
console.log(/^a{0,1}$/.test("aa"));  //false
console.log(/^a{0,1}$/.test("aab")); //false

[ 65-正则表达式-量词.html ]

14.6 括号总结

1、{} 大括号限定出现的次数

// 表示的是 n 重复两次
console.log(/chuan{2}/.test("chuanchuan"));     // false 
console.log(/chuan{2}/.test("chuann"));         // true
console.log(/chuan{2}/.test("chuann123123"));   // true

2、[] 表示一个字符出现的位置

console.log(/^[fb]oot$/.test("foot"));  // true
console.log(/^[fb]oot$/.test("boot"));  // true

3、() 用来提升优先级

console.log(/^(chuan){2}$/.test("chuanchuan")); // true

14.7 正则表达式综合案例

1、验证座机号码: [ 67-正则案例-验证座机号码.html ]

  • 直辖市座机号码:021-88888888
  • 普通地区做急哦号码:0515-12345678
  • 前区可以是三位也可以是四位,第一位必须是0,后区必须是8位;
var reg = /^0\d{2,3}-\d{8}$/;
console.log(reg.test('021-12345678'));   // true
console.log(reg.test('0515-88888888'));  // true
console.log(reg.test('0515-888880888')); // false

2、验证中文姓名 [ 68-正则案例-验证中文姓名.html ]

  • 只能是汉字
  • 长度2-6位之间
  • 汉字范围[\u4e00-\u9fa5]
  • unicode编码:万国码,其中\u4e00-\u9fa5表示的就是包含所有汉字的unicode编码
var nameReg = /^[\u4e00-\u9fa5]{2,6}$/;
console.log(nameReg.test('莫'));         // false
console.log(nameReg.test('小泽玛利亚')); // true
console.log(nameReg.test('柯南'));       // true

3、验证邮箱 [ 69-正则案例-验证邮箱.html ]

  • 前面是字母或者数字
  • 必须有@
  • @后面是字母或者数字
  • 必须有.
  • .后面是字母或者数字
var mailBoxReg = /^\w+@\w+(\.\w+)+$/;
console.log(mailBoxReg.test('18888888@qq.com')); // true

4、验证手机号码 [ 70-正则案例-验证手机号码.html ]

  • 11位数字组成
  • 号段13[0-9] 147 15[0-9] 177[0178] 18[0-9]
var mobileReg = /^(13[0-9]|147|15[0-9]|17[0178]|18[0-9])\d{8}$/;
console.log(mobileReg.test(15812345678));  // true

5、验证QQ [ 71-正则案例-验证qq.html ]

  • 只能是数字
  • 开头不能是0
  • 长度为5-11
var qqReg = /^[1-9]\d{4,10}$/;
console.log(qqReg.test(18888888)); // true

6、完整版表单验证 [ 72-正则案例-表单验证综合案例.html ]

<!-- 样式部分 -->
<style>
    body {
        background: #ccc;
    }
    .container {
        margin: 100px auto;
        width: 400px;
        padding: 50px;
        line-height: 40px;
        border: 1px solid #999;
        background: #efefef;
    }
    label {
        width: 40px;
        display: inline-block;
    }
    span {
        color: red;
    }
    span {
        margin-left: 30px;
        font-size: 12px;
    }
</style>

<!-- html 部分 -->
<div class="container">
    <label>Q Q</label><input type="text" id="inp1"><span></span><br/>
    <label>手机</label><input type="text" id="inp2"><span></span><br/>
    <label>邮箱</label><input type="text" id="inp3"><span></span><br/>
    <label>座机</label><input type="text" id="inp4"><span></span><br/>
    <label>姓名</label><input type="text" id="inp5"><span></span><br/>
</div>

<!-- js 部分 -->
<script>
    function checkReg(element, reg) {
        element.onblur = function() {
            var content = this.value;
            if (reg.test(content)) {
                this.nextElementSibling.innerHTML = "合法";
                this.nextElementSibling.style.color = "green";
            } else {
                this.nextElementSibling.innerHTML = "不合法";
                this.nextElementSibling.style.color = "red";
            }
        }
    }
    checkReg(document.getElementById("inp1"), /^[1-9]\d{4,11}$/);
    checkReg(document.getElementById("inp2"), /^(13[0-9]|14[57]|15[0-25-9]|17[0137]|18[0-9])\d{8}$/);
    checkReg(document.getElementById("inp3"), /^\w+@\w+(\.\w+)+$/);
    checkReg(document.getElementById("inp4"), /^0\d{2,3}-\d{7,8}$/);
    checkReg(document.getElementById("inp5"), /^[\u4e00-\u9fa5]{2,4}$/);
</script>

效果图:

image

14.8 正则补充知识点

这里要补充几个正则的小知识点,比如在正则里‘|’,表示的是或。‘g’,表示的是global:全局,全部。‘i’,表示的是ignore:忽视大小写。

1、或‘|’ :

var mobileReg = /^(13[0-9]|14[57]|15[0-25-9]|17[0137]|18[0-9])\d{8}$/;

我们可以看到在判断手机号码前三位的时候,我们就用到了或:“|”

2、全部‘g’:

var str = '123123123123';
// 找到所有的1  替换成3
var newStr = str.replace(/1/g, 3);
console.log(newStr);  // 323323323

3、忽略大小写‘i’:

var str2 = 'abcdAAAA';
var newStr2 = str2.replace(/a/gi, 'e');
console.log(newStr2);  // ebcdeeee

上一篇:JavaScript 进阶知识 - 特效篇(一)
下一篇:JavaScript 进阶知识 - Ajax篇


深海丶Deepsea
3.9k 声望1.4k 粉丝

Trust yourself,You know more than you think you do.