# JavaScript BOM 核心 Window 对象属性、方法、事件

TIP

从本章开始,我们来学习 BOM 相关内容,在之前的 DOM 相关内容中,提到的和 window 对象有关的属性,部分也属于 BOM 相关内容,比如定时器。只是因为在 DOM 中要经常用到,所以就提前学习了。

什么是 BOM

  • BOM(Browser Object Model)浏览器对象模型,是 JS 与浏览器窗口交互的接口
  • BOM 的核心是 window 对象,表示浏览器的实例

接下来我们就来深入学习下 window 对象

# 一、window 对象

TIP

  • window作为全局变量,代表了脚本正在运行的窗口,暴露给 Javascript 代码
  • 在有标签页功能的浏览器中,每个标签都拥有自己的 window 对象
  • 也就是说,同一个窗口的标签页之间不会共享一个 window 对象

window 对象有两重身份

  • 在 ECMScript 中为 Global 对象,也就是后面常说的 Node 中的全局对象。
  • 在浏览器窗口中为 window 对象,所有全局作用域下的 var 声明的全局变量和函数都是 window 对象的属性

# 1、全局变量是 window 属性

TIP

所有全局作用域下 var 声明的全局变量和函数都是 window 对象的属性

var a = 2;
// hasOwnProperty 检测当前对象是否拥有该属性
console.log(window.hasOwnProperty("a")); // true
console.log(window.a); // 2
console.log(window.a == a); // true
  • 多个 JS 文件之间是共享全局作用域的,即 JS 文件没有作用域隔离功能
  • 创建a.jsb.jsc.html文件,在c.html页面引入a.jsb.js
// a.js文件代码如下
var i=2;

// b.js文件代码如下
var i++;
<!-- c.html文件代码如下 -->
<script src="js/a.js"></script>
<script src="js/b.js"></script>
<script>
  console.log(i); // 3
</script>

# 2、内置函数普遍是 window 对象的方法

TIP

setInterval()alert()Array 等普遍是 window 的方法

console.log(window.alert == alert); // true
console.log(window.setInterval == setInterval); // true

console.log(window.hasOwnProperty("setInterval"));

window.setInterval(function () {
  window.console.log("aaa");
}, 2000);

# 3、浏览器窗口大小

TIP

window 对象身上的以下 4 个属性用来确定浏览器窗品大小

属性名 说明
innerWidth 返回浏览器窗口中页面可视区宽(不包含 浏览器边框和工具栏,包括滚动条)
innerHeight 返回浏览器窗口中页面可视区高(不包含 浏览器边框和工具栏,包括滚动条)
outerWidth 返回浏览器窗口自身的实际宽
outerHeight 返回浏览器窗口自身的实际高

注:

因为 innerWidth 和 innerHeight 包括了滚动条的宽和高,所以很多时候我们获取页面可视宽和高是通过。

document.body.clientWidth || document.documentElement.clientWidth 来实现

// 浏览器窗口中页面视口大小 包括滚动条
console.log("innerWidth:" + window.innerWidth);
console.log("innerHeight:" + window.innerHeight);
// 浏览器窗口大小
console.log("outerWidth:" + window.outerWidth);
console.log("outerHeight:" + window.outerHeight);

// 页面可视区大小(不包括滚动条)
console.log("clientWidth:" + document.documentElement.clientWidth);
console.log("clientHeight:" + document.documentElement.clientHeight);

image-20221201145847880

# 4、滚动距离

属性 说明
window.scrollX 返回文档/页面水平方向滚动的像素值
window.scrollY 返回文档在垂直方向已滚动的像素值
window.pageXoffset 相当于 scrollX 的别名
window.pageYoffset 相当于 scrollY 的别名

注:

通常获取页面的水平和垂滚动距离还会通过如下代码获取

  • 文档水平滚动距离: document.documentElement.scrollLeft || document.body.scrollLeft
  • 文档垂直滚动距离: document.documentElement.scrollTop || document.body.scrollTop
console.log("scrollX:" + window.scrollX);
console.log("scrollY:" + window.scrollY);
console.log("pageXoffset:" + window.pageXOffset);
console.log("pageYoffset:" + window.pageYOffset);
console.log("scrollTop:" + document.documentElement.scrollTop);
console.log("scrollLeft:" + document.documentElement.scrollLeft);
方法 说明
scroll(x,y) x,y表示相对视口距离的xy坐标,scroll(x,y)表示文档滚动到指定坐标位置
scrollBy(x,y) x表示水平方向上要滚动的偏移量,y表示垂直方向上要滚动的偏移量
<style>
  html,
  body {
    margin: 0;
    padding: 0;
    height: 3000px;
    width: 3000px;
  }
  button {
    width: 150px;
    height: 50px;
    position: fixed;
    right: 50px;
    top: 200px;
  }
  .by {
    top: 280px;
  }
</style>

<button class="to">滚动到指定位置</button>
<button class="by">滚动一定距离</button>

<script>
  var button = document.getElementsByTagName("button");
  button[0].onclick = function () {
    window.scroll(100, 200); // 点击后,跳转到与视口x=100,y=200的坐标位置
  };

  button[1].onclick = function () {
    // window.scrollBy(0, -100); // 每次点击,滚动条向上滚动 100px
    window.scrollBy(0, 100); // 每次点击,滚动条向上滚动100px
  };
</script>

# 5、window 其它属性

TIP

重点提示

  • 因为 window 对象的属性在全局作用域中有效,所以很多浏览器 API及相关构造函数等都以 window 对象属性的形式暴露出来
  • 总结一句话:window 对象身上的属性并非全是与 BOM 操作相关的方法和属性

接下来我们学习window对象身上与BOM相关的属性

属性 说明
location 获取当前面面的 URL 信息
history 对象提供了操作浏览器会话历史的接口
navigator 对象包含用户此次活动的浏览器的相关属性和标识

# 二、window.location 对象

TIP

window.location 只读属性,返回一个 Location对象,其中包含有关文档当前位置的信息。

Location 接口表示其链接到的对象的位置(URL)

location.__proto__ === Location.prototype; // true
window.location;

image-20221108141335356

强调:location 对象可以赋值,重新赋值 URL,相当于跳转到当 URL

// 跳转到  http://www.icodingedu.com 这个页面
window.location = "http://www.icodingedu.com";

# 1、URL 组成部分

TIP

  • URL(Uniform Resource Locator)统一资源定位符
  • URL 无非就是一个给定的独特资源在 Web 上的地址
  • 理论上说,每个有效的 URL 都指向一个唯一的资源
  • 这个资源可以是一个 HTML 页面,一个 CSS 文档,一幅图像,等等

URL 的组成部分

protocol://host[:port]/path/[?query]#fragment  /* [] 方括号表示可选 */
http://www.arryblog.com/guide/html5/html5.html?targetId=12&preview=0#top
组成 说明
protocol URL 对应的协议名,常用的协议有:httphttpsftpmaitofile
host 主机(域名),如 www.arryblog.com
port 网络端口号,可选。如果省略,表示使用默认的端口。如 http 的默认端口是 80
path 路径,用来表示服务器上的一个目录或文件路径 如:guide/html5/html5.html
query 查询字符串(参数),以键值对的形式表示,多个用&符号分隔,如:targetId=12&preview=0
fragment 片段标识符,#后内容,用来标记已获取资源的文档内的某个位置。如:常见锚点 #top

# 2、location 对象的属性

location 对象上有很多属性,以下就是其中的一部分

属性 说明
location.href 获取完整的 URL,也可以重新设置 URL
location.host 获取主机名(域名)
location.port 获取端口号,如果端口号省略,返回空字符串“”
location.pathname 返回 URL 中 path 路径部分
location.search 返回 URL 中的 query 查询字符串部分内容
location.hash 返回 URL 中的 fragment 部分内容。

image-20221108142440670

# 3、处理 URL 中 query 部分数据

TIP

写一个函数,把 URL 中的 query 部分参数转换成对象中的属性和值,然后将这个对象返回,同时要考虑参数的类型。

MDN 官方参考地址:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/location (opens new window)

// location.search得到 URL中的query数据
"?targetId=99&sort=hot&bool=true&name=张三"

// 最终转换成下面这样一个对象
{ targetId: 99, sort: 'hot', bool: true, name: '张三'}
// 获取地址中query查询字符串部分内容
var search = window.location.search;
// 返回处理后以对象形式呈现的查询字符
paramObj = GetVars(search);
console.log(paramObj);
/**
 * GetVars 将取得的查询参数以键值对形式存到对象中,同时考虑数据类型
 * @param search查询的字符串内容 如 ?targetId=99&sort=hot
 */
function GetVars(search) {
  var oGetVars = {}; // 用来保存返回的参数与参数对应的值
  if (search.length > 1) {
    // 将获取到的字符串,去掉第一个问号,同时将剩下的字符串以 & 来分隔
    var arr = search.slice(1).split("&"); //得到如下数组:
    arr.forEach(function (item) {
      var keyValue = item.split("=");
      var key = keyValue[0];
      var value = keyValue[1];
      // decodeURIComponent  将已编码 URI 中所有能识别的转义序列转换成原字符
      oGetVars[key] = buildValue(decodeURIComponent(value));
    });
  }
  return oGetVars;
}

// 写一个函数,专门来处理参数值的类型
// 1、考虑传过来的值是不是空的,如果为空就返回null
// 2、考虑参数是boole值 true和false,返回对应bool值
// 3、考虑是不是一个数字类型
// 4、考虑是不是一个日期类型
// 5、排除上面,最后只有字符串类型
function buildValue(value) {
  // 1、考虑传过来的值是不是空的,如果为空就返回null
  if (value === undefined || value.trim() === "") return null;
  // 2、如果是一个boole值,则返回对应的bool值
  if (value.toLowerCase() === "true") return true;
  if (value.toLowerCase() === "false") return false;
  // 3、全局isFinite() 用来判断被传入的数值是否为一个有限数值,判断时会做数据类型转换
  if (isFinite(value)) return parseFloat(value);
  // 4、判断是不是一个日期
  if (isFinite(Date.parse(value))) return new Date(value);
  // 5、上面都不满足,最后只能是字符串,并将其返回
  return value;
}

# 4、数据交互 - 根据参数显示页面内容

TIP

  • index.html表示首页,用来显示产品列表
  • goods.html表示产品详细页,根据 URL 中的参数来决定显示那个产品的详细信息
  • 当点击index.html页面的产品名,就会跳转到goods.html页面,显示对应产品的详细信息

数据交互,根据参数来显示页面内容

index.html 页面源代码

<ul id="list">
  <!--
<li>
<a href="./goods.html?targetId=93">Web前端高级工程师系统课-星辰班</a>
</li>
<li><a href="./goods.html?targetId=91">CSS实战小案例详解</a></li>
<li>
<a href="./goods.html?targetId=77">云原生Kubernetes与云上DevOps新版系统实战</a>
</li> 
--></ul>
<script>
  var data = [
    {
      targetId: 93,
      title: "Web前端高级工程师系统课-星辰班",
      mainImage:
        "https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/08-29/210311f40bcf290736.jpg",
      price: 8680,
    },
    {
      targetId: 91,
      title: "30个HTML+CSS实战小案例详解",
      mainImage:
        "https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/03-19/174949d70767470556.jpg",
      price: 0,
    },
    {
      targetId: 77,
      title: "云原生Kubernetes与云上DevOps新版系统实战课",
      mainImage:
        "https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2020/07-14/122139326c0d505110.png",
      price: 2680,
    },
  ];

  var ul = document.getElementById("list");
  // 遍历我们拿到的数据
  for (var i = 0; i < data.length; i++) {
    var li = document.createElement("li");
    var a = document.createElement("a");
    a.href = "./goods.html?targetId=" + data[i].targetId;
    a.target = "_blank";
    a.innerText = data[i].title;
    li.appendChild(a);
    ul.appendChild(li);
  }
</script>

goods.html 页面源代码

<div id="container">
  <!--
<img  src="https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/08-29/210311f40bcf290736.jpg"
width="300"
/>
<h3>Web前端高级工程师系统课-星辰班</h3>
<p>价格:8680</p> 
--></div>

<script src="./getVars.js"></script>
<script>
  var data = [
    {
      targetId: 93,
      title: "Web前端高级工程师系统课-星辰班",
      mainImage:
        "https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/08-29/210311f40bcf290736.jpg",
      price: 8680,
    },
    {
      targetId: 91,
      title: "30个HTML+CSS实战小案例详解",
      mainImage:
        "https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/03-19/174949d70767470556.jpg",
      price: 0,
    },
    {
      targetId: 77,
      title: "云原生Kubernetes与云上DevOps新版系统实战课",
      mainImage:
        "https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2020/07-14/122139326c0d505110.png",
      price: 2680,
    },
  ];

  //
  var container = document.getElementById("container");
  // 获取查询字符串中的targetId
  var search = location.search;
  var Vars = GetVars(search);

  data.forEach(function (item) {
    if (item.targetId === Vars.targetId) {
      // 就是我想要找到用来渲染的那一条数据
      // 创建一个DOM碎片
      var frag = document.createDocumentFragment();
      // 创建图片
      var img = document.createElement("img");
      img.src = item.mainImage;
      // 创建h3
      var h3 = document.createElement("h3");
      h3.innerText = item.title;
      // 创建p标签
      var p = document.createElement("p");
      var price = item.price > 0 ? item.price : "免费";
      p.innerText = "价格:" + price;

      // 先添加到DOM碎片
      frag.appendChild(img);
      frag.appendChild(h3);
      frag.appendChild(p);
      // 统一加到container中去
      container.appendChild(frag);
    }
  });
</script>

# 5、404 页面,定时跳转功能

20221108time404

<style>
  .time {
    font-size: 20px;
    color: red;
  }
</style>
<span class="time">5</span>秒后,页面跳转到首页
<script>
  var time = document.querySelector(".time");
  var second = 5;
  var timer = setInterval(function () {
    second--;
    time.innerText = second;
    if (!second) {
      clearInterval(timer);
      window.location.href = "http://www.icodingedu.com";
    }
  }, 1000);
</script>

# 6、location 对象的方法

以下是 location 对象身上的一些常见的方法

方法名 说明
assign location.assign()方法会触发窗口加载并显示指定的 URL 的内容,类似location.href
会有历史记录,可通过前进和后退按扭来追回历史页。
如果传入一个无效的 URL,则会抛出一个
replace 用给定的 URL 替换掉当前的资源,不会有历史记录,即不能用后退回到原页面
reload 重新加载当前页面,相当于刷新按扭或F5
如果参数 为true,表示强制刷新 ctrl + F5 ,即:要从服务器上加载数据
如果参数为false,表示浏览器可能从缓存当中加载页面
// assign 相当于href,跳转到一个新的页面
location.assign("http://www.icodingedu.com");

// 用给定URL替换当前面 不会产生历史记录
location.replace("http://www.icodingedu.com");

// 重新加载当页面
location.reload(true); // 强制制新,重新从服务器加载数据

# 三、window.history 对象

TIP

Window.history 是一个只读属性,用来获取History对象的引用,History 对象提供了操作浏览器会话历史的接口

window.history对象相关方法

方法 作用
back 后退功能,返回上一页,相当于用户点击了浏览的 🡰 等价 history.go(-1)
forward 前进功能,进入下一页,相当于用户点击了浏览的 等价history.go(1)
go 前进与后退,
当参数为1,表示进入下一页,
当参数为-1,表示退回上一页,
如果参数为0,则重新载入当前页面
history.back(); // 后退
history.forward(); // 前进
history.go(1); // 前进 进入下一页

# 四、navigator 对象

TIP

  • window.navigator对象包含用户此次活动的浏览器的相关属性和标识
  • 他有很多属性,但最常用的是 userAgent 这个属性
  • userAgent 属性返回当前浏览器的用户代理字符串
navigator.userAgent; // 返回用户代理(浏览器)相关信息

image-20221108201344417

我们通常利用这个userAgent属性来判断当前的打开页面的浏览器和设备

# 1、检测浏览器并返回浏览器名称

以下代码来自 MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/navigator (opens new window)

function BrowserName() {
  var sBrowser,
    sUsrAg = navigator.userAgent;
  if (sUsrAg.indexOf("Firefox") > -1) {
    sBrowser = "Mozilla Firefox"; // 火狐
  } else if (sUsrAg.indexOf("Opera") > -1 || sUsrAg.indexOf("OPR") > -1) {
    sBrowser = "Opera"; // 欧朋
  } else if (sUsrAg.indexOf("Trident") > -1) {
    sBrowser = "Microsoft Internet Explorer";
  } else if (sUsrAg.indexOf("Edge") > -1) {
    sBrowser = "Microsoft Edge"; //Edge浏览器
  } else if (sUsrAg.indexOf("Chrome") > -1) {
    sBrowser = "Google Chrome or Chromium"; // Chrome
  } else if (sUsrAg.indexOf("Safari") > -1) {
    sBrowser = "Apple Safari"; // Safari
  } else {
    sBrowser = "unknown"; // 不知道
  }
  return sBrowser;
}

# 2、判断是否为微信环境

TIP

micromessenger为微信内嵌的浏览器,有此标识的基本上可以判断是微信环境吗,但此标识也可以伪造

// 判断是否为微信
const isWx = function () {
  var ua = window.navigator.userAgent.toLowerCase();
  if (ua.indexOf("micromessenger") !== -1) return true;
  return false;
};

# 3、设备判断:android、ios、web

function isDevice() {
  if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
    return "iOS";
  } else if (/(Android)/i.test(navigator.userAgent)) {
    return "Android";
  }
  return "Web";
}

注:

  • i:代表不区分大小写匹配
  • ()是为了提取匹配字符串的,表达式中有几个(),就有几个相应的匹配字符串

# 五、window 相关事件

接下来我们学习几个非常用要的事件

事件名 说明
load 整个页面(包括所有外部资源,如:图片、JavaScript 文件和 CSS 文件)加载完成后触发
DOMContentLoaded 在 DOM 树构建完成后立即触发,不用等待图片、JavaScript 文件、CSS 文件或其它资源加载完成。
resize 当调整浏览器的窗口大小时,会触发 resize 事件。
resize 事件触发会很频繁,所以我们在处理时,可以设置节流操作
scroll 当浏览器滚动条发生滚动时触发window.onsrcoll事件

注:

DOMContentLoaded事件的实际目标是document,但会冒泡到window

所以我们可以在document上监听这个事件

# 1、load 和 DOMContentLoaded 事件

TIP

  • load :整个页面(包括所有外部资源,如:图片、JavaScript 文件和 CSS 文件)加载完成后触发
  • DOMContentLoaded :在 DOM 树构建完成后立即触发,不用等待图片、JavaScript 文件、CSS 文件或其它资源加载完成。

DOMContentLoaded 事件始终在 load 事件之前发生

console.log("我第一个出来");
// DOM树构建完成时触发
window.addEventListener("DOMContentLoaded", function () {
  alert("我第二个出来");
});
// 页面加载完成后触发
window.onload = function () {
  alert("我第三个出来");
};

除了页面加完会触发 load 事件外,其它元素也会触发与之对应的 load 的事件

元素 说明
<img>标签 图片会在图片加载完成后触发 load 事件
图片并不需要插入到页面。只要 img 对象添加 src 属性,就会发起 http 请求,下载成功就会触发 load 事件
<script>标签 在 JS 加载完后会触发 load 事件
不过<script>标签要指定 src 属性,同时要插入到文档中,才会发起 http 请求,下载成功才会触发 load 事件
<link>标签 需要指定<link>标签的 href 属性,同时要插入到文档中,才会发起 http 请求,在 CSS 文件全部加载完成后会触发 load 事件

# 1.1、动态加载图片,插入页面

TIP

load 加载成功,触发 load 事件

  • 图片加载完成后触发 load 事件,图片并不需要插入到页面。
  • 只要 img 对象添加 src 属性,就会发起 http 请求,下载成功就会触发 load 事件
  • error 加载失败,触发 error 事件
/**
 * loadImg 动态加载图片
 * @param url 图片地址
 */
function loadImg(url) {
  var img = new Image(); // 创建img对象
  // 图片加载成功,插入到页面
  img.onload = function () {
    // alert('加载成功')
    document.body.appendChild(img);
  };
  // 图片加载失败,在页面显示图片加载失败
  img.onerror = function () {
    document.body.innerText = "图片加载失败";
  };
  img.src = url;
}

var url =
  "https://sce7a2b9c9d95a-sb-qn.qiqiuyun.net/files/course/2022/08-29/210311f40bcf290736.jpg";
loadImg(url);

# 1.2、动态加载 JS

TIP

  • 在 JS 加载完后会触发 load 事件
  • 不过<script>标签要指定 src 属性,同时要插入到文档中,才会发起 http 请求,下载成功才会触发 load 事件。
/**
 * loadScript 动态加载JS文件
 * @param src js文件地址
 * @param callback回调函数,js加载完成后,要处理的事情
 */
function loadScript(src, callback) {
  var script = document.createElement("script");
  script.onload = function () {
    typeof callback === "function" ? callback() : callback;
  };
  script.onerror = function () {
    alert("加载失败");
  };
  // 指定src属性值,并插入到页面中
  script.src = src;
  document.body.appendChild(script); // 不插入到页面,load事件永远不会触发
}

loadScript("./a.js", function () {
  alert("加载成功");
});

# 1.3、动态加载 CSS

TIP

  • 在 CSS 文件全部加载完成后会触发 load 事件
  • <script>标签一样,需要指定 href 属性值,并且要把<link>标签插入到文档发中才会开始下载 CSS 样式,下载完成后才会触发 load 事件
/**
 * 动态插入外链css
 * href css外链地址
 */
function loadCSS(href) {
  var link = document.createElement("link");
  link.rel = "stylesheet";
  link.addEventListener(
    "load",
    function () {
      alert("css加载成功");
    },
    false
  );
  link.href = href;
  // 把link标签插入到head标签中
  var head = document.getElementsByTagName("head")[0];
  head.appendChild(link);
}
loadCSS("./index.css");

# 2、resize 事件

TIP

  • 当调整浏览器的窗口大小时,会触发resize事件
  • resize事件触发会很频繁,所以我们在处理时,可以设置节流操作
var a = 0;
window.onresize = function () {
  console.log(a++);
};
var a = 0;
window.onresize = throttle(fn, 50);
function fn() {
  console.log(a++);
}

/**
 * 节流函数
 * fn 事件处理函数
 * delay 函数执行间隔时间
 */
function throttle(fn, delay) {
  var timer = null;

  return function () {
    var self = this;
    var args = arguments;
    if (timer) return;
    timer = setTimeout(function () {
      // 函数体执行代码
      fn.apply(self, args);
      // 开锁
      timer = null;
    }, delay);
  };
}

# 3、srcoll 事件

TIP

  • 当浏览器滚动条发生滚动时触发window.onsrcoll事件
  • 如果某个元素内的内容溢出,显示了滚动条,滚动对应的滚动条,也会触发对应scroll事件
  • scroll事件的触发频率也非常的快,所以了要根据实际业务场景来添加节流操作
var a = 0;
window.onscroll = function () {
  console.log(a++);
};

GIF2022-11-822-45-11

<style>
  .box {
    width: 200px;
    height: 200px;
    background-color: skyblue;
    overflow: scroll;
  }
  .box1 {
    height: 400px;
  }
</style>

<div class="box">
  <div class="box1"></div>
</div>

<script>
  var box = document.querySelector(".box");
  var a = 0;
  box.onscroll = function () {
    console.log(a++);
  };
</script>

GIF2022-11-822-42-50

# 4、返回顶部

实现效果

GIF 2022-11-9 0-18-25

点击返回顶部按扭,页面滚动到最顶部,即滚动条与浏览器顶部的滚动距离不断的减小,直到减少到 0

其实现方式有三种

TIP

  • 1、不断减小window.scroll(x,y)中 y 的值,直到y <= 0
  • 2、通过window.scrollBy(x,y) 方法,每次向上移动一点距离,直接window.scrollY <= 0
  • 3、通过不断减小document.documentElement.scrollTop的值,直到 <= 0

三种实现方式的通用布局

<style>
  #back-top {
    width: 80px;
    height: 80px;
    background-color: #ddd;
    text-align: center;
    line-height: 80px;
    cursor: pointer;
    position: fixed;
    right: 10px;
    bottom: 100px;
  }
  body {
    height: 3000px;
  }
</style>

<div id="back-top">返回顶部</div>

# 4.1、实现方式一

TIP

使用 window.scroll(x,y)window.scrollY

  • 点击后,首先获取当前滚动条滚动的距离scrollY = window.scrollY
  • 然后开启一个定时器,隔一定时间让 window.scroll(0,y)y 的值减小 100 ,直到window.scrollY <= 0,说明滚动条滚动到页面的顶部,然后暂停定时器
  • 添加 lock 变量,用来防止当次滚动没有结束前,再次执行新的动画
<script>
  var backTop = document.getElementById("back-top");
  // 第一套方案 window.scroll();
  var timer = null;
  var lock = false; // 可以点击
  // 点击事件
  backTop.onclick = function () {
    // 获取滚动条与浏览器顶部的距
    if (lock) return;
    lock = true;
    var scrollY = window.scrollY; // 把他放在外面,不用频繁的读他
    timer = setInterval(function () {
      scrollY -= 100;
      if (scrollY <= 0) {
        clearInterval(timer);
        lock = false;
      }
      window.scroll(0, scrollY);
    }, 20);
  };
</script>

# 4.2、实现方式二

TIP

使用 window.scrollBywindow.scrollY

  • 通过window.scrollBy(x,y) 方法,每次向上移动一点距离,直接window.scrollY <= 0
  • 添加 lock 变量,用来防止当次滚动没有结束前,再次执行新的动画
<script>
  var backTop = document.getElementById("back-top");
  // 第一套方案 window.scroll();
  var timer = null;
  var lock = false; // 可以点击

  // 点击事件
  backTop.onclick = function () {
    // 获取滚动条与浏览器顶部的距
    if (lock) return;
    lock = true;

    timer = setInterval(function () {
      var scrollY = window.scrollY; // 把他放在外面,不用频繁的读他
      if (scrollY <= 0) {
        clearInterval(timer);
        lock = false;
      }
      window.scrollBy(0, -20);
    }, 20);
  };
</script>

# 4.3、实现方式三

TIP

使用 document.documentElement.scrollTop 来实现

  • 首先获取当前滚动条滚动过的距离 document.documentElement.scrollTop
  • 开启定时器,不断的减小scrollTop 的值,直到 <= 0
  • 添加 lock 变量,用来防止当次滚动没有结束前,再次执行新的动画
<script>
  var backTop = document.getElementById("back-top");
  backTop.addEventListener("click", backToTop, false);
  var lock = false;
  function backToTop() {
    if (lock) return;
    lock = true;
    // 获取当前滚动条滚动的距离
    var doc = document.documentElement || document.body;
    var top = doc.scrollTop;
    clearInterval(this.timer); // 多次点击无效,以最后一次为主
    var that = this; // 保存this,在定时器内需要用到

    this.timer = setInterval(function () {
      if (top <= 0) {
        clearInterval(that.timer); // 如果top小于0清除定时器
        lock = false;
      } else {
        top = top - 100; // 每次减 100
        doc.scrollTop = top; // 动态更改滚动条件与浏览器的滚动的距离
      }
    }, 20);
  }
</script>

以上三种方式,都是限定步长的运动,如果想要实现限定时间的匀速动动,则参考如下代码:

# 4.4、优化版 - 最佳实践 - 限定时间匀速运动

TIP

  • T:time 已经运动的时间
  • B:begin 开始位置
  • C: change 需要移动的总距离 = Target - Begin
  • D: duration 动画运动的总时间 CurrentDistance = T / D * C + B 即:当前位置 = T / D * C + B ,每次运动后,元素的当前位置

使用 window.scroll(x,y)window.scrollY 实现

<script>
  // 限定时运动到顶部
  var backTop = document.getElementById("back-top");
  var timer = null;
  var lock = false;
  // 限定时间500ms
  backTop.onclick = function () {
    if (lock) return;
    lock = false;
    // 已经运动的时间
    var T = 0;
    // 开始运动的位置
    var B = window.scrollY;
    // 目标位置是 0  var targe=0;
    // 总共需要改变的距离
    var C = 0 - B;
    // 总共需要运动的时间
    var D = 200;

    // 开始执行动画
    timer = setInterval(function () {
      T += 20; // 记录运动的时间
      // 当前滚动条运动的距离
      var currentDistance = (T / D) * C + B;
      // currentDistance <= 0
      if (T >= D) {
        clearInterval(timer);
        currentDistance = 0;
        lock = false;
      }
      window.scroll(0, currentDistance);
    }, 20);
  };
</script>

使用 document.documentElement.scrollTop 实现

<script>
  var backTop = document.getElementById("back-top");
  var doc = document.documentElement || document.body;
  var timer = null;
  var lock = false;
  //点击后要处理的事情
  // 限定时间动画
  backTop.onclick = function () {
    if (lock) return;
    lock = true;
    // 运动的时间
    var T = 0;
    var B = doc.scrollTop; // 开始位置
    // target=0 目标位置
    var C = 0 - B; // 总共需要改变的距离
    var D = 500; // 总共运动的时间

    timer = setInterval(function () {
      // 记录运的时间
      T += 20;
      // 当前运动到的位置
      var currentDistance = (T / D) * C + B;
      // currentDistance < 0
      if (T >= D) {
        clearInterval(timer);
        currentDistance = 0;
        lock = false;
      }

      doc.scrollTop = currentDistance;
    }, 20);
  };
</script>

# 5、图片延时加载

GIF2022-12-121-39-22

实现原理:

  • 页面中所有需要做延时加载的图片上加上class = 'lazy'data-src = ' 图片真实地址'
  • 获取页面中所有class中包含lazy的元素,同时要判断元素是否为img标签且data-src是否有值。确保操作的元素是 img 元素,同时data-src中有图片地址。
  • 当滚动浏览器的滚动条时,要判断对应的图片是否进入可视区,如果进入,则给img.src动态赋值
  • 如果图片一但赋值,则后面就不需要再对此 img 标签做监听。则可以从数组对象中将其删除,同时把 lazy 样式删除
  • 最后考虑浏览器窗口大小改变时的情况,还有要考虑对 scroll 和 resize 事件做节流操作

image-20221201200514490

# 5.1、HTML 结构

<style>
  html,
  body,
  ul,
  li {
    margin: 0;
    padding: 0;
  }
  ul {
    /* width: 968px; */
    margin: 0 auto;
  }
  li {
    list-style: none;
    width: 200px;
    height: 200px;
    padding: 10px;
    margin: 10px;
    border: 1px solid #ddd;
    float: left;
  }
  li img {
    width: 200px;
    height: 200px;
  }
</style>
<body>
  <ul id="J_container">
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/01.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/01.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/02.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/03.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/04.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/05.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/06.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/07.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/08.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/09.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/10.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/11.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/12.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/13.png"
        class="lazy"
      />
    </li>
    <li>
      <img
        src="./images/loading-svg/loading-bars.svg"
        data-src="./images/14.png"
        class="lazy"
      />
    </li>
  </ul>
</body>

# 5.2、JS 实现页面响应式布局

TIP

第一次打开页面或当浏览器窗品大小发生改变时,获取浏览器窗口的可视区宽 clientWidth

var clientWidth =
  document.documentElement.clientWidth || document.body.clientWidth;

然后用clienwWidth / 每个li元素的占位宽(包括 margin,border,padding),对结果向上取整,计算得出页面一行最多能放几个li

var count = Math.floor(clientWidth / 242);

利用 count * 242 动态计算得到其父容器ul的宽

container.style.width = count * 242 + "px";

完整实现代码

var container = document.getElementById("J_container"); // 获取ul元素
initUlWidth(); // 初始化ul宽
// throttle 为节流函数
window.addEventListener("resize", throttle(initUlWidth, 90));
function initUlWidth() {
  // 获取当前浏览器可视区的宽
  var clientWidth =
    document.documentElement.clientWidth || document.body.clientWidth;
  var count = Math.floor(clientWidth / 242);
  // 动态计算container的宽
  container.style.width = count * 242 + "px";
}

# 5.3、JS 实现懒加载(延时加载)效果

获取页面中所有class = 'lazy' 的元素,同时过滤掉不是图标的元素

// 1、需要获取到所有需要延时加载的图片
var imgs = document.querySelectorAll(".lazy");
// 2、过滤掉不是图片的标签
imgs = Array.prototype.filter.call(imgs, function (item) {
  return item instanceof Image;
});

获取页面可视区的高

// 获取浏览器可视区的高
var clientHeight =
  document.documentElement.clientHeight || document.body.clientHeight;
  • 对获取的所有图片进行遍历,遍历时判断元素是否进入到可视区
  • 元素进入可视区原理:元素底部与浏览器可视区的高 > 0 同时元素顶部与浏览器可视区的高要小于浏览器可视区高。 即bottom > 0 && _top < clientHeight
  • 如果元素进入可视区,将图片保存在data-src属性上的正确地址赋值给src属性
  • 同时将此图片从imgs数组中删除,以后不需要再监听了
// 遍历所有过滤后得图片,然后查看图片是否进入到可视区,如果进入到可视区,就动态给图片添加真实的图片地址
for (var i = 0; i < imgs.length; i++) {
  // 判断图片是否进入可视区  bottom>0  && top<浏览器可视区高
  var rect = imgs[i].getBoundingClientRect();

  var bottom = rect.bottom;
  var _top = rect.top;
  // 以下条件成立,代表元素进入到可视区
  if (bottom > 0 && _top < clientHeight) {
    imgs[i].src = imgs[i].dataset.src; // 把自定义属性上的真实地址赋值给图片
    // 如果图片进入过一次可视区,动态赋过值,就不用再管他了
    var index = imgs.indexOf(imgs[i]);
    imgs.splice(index, 1);
    i--; // 一定要注意,i--
  }
}
  • 当滚动条滚动时,或浏览器窗口大小发生改变时,会有其它图片进入到当前浏览器可视区,则需要添加对应的事件来监听。但整个过程不需要再重新获取页面的中的class = 'lazy'的元素,同时还要判断,如果 imgs 的长度为 0,则表示所有图片都加载完成,不需要再做任何处理
  • 最后还要做相关节流操作

完整的代码如下

// 以下内容,单独放在名lazyload.js 文件中,其它页面需要此效果,
// 只需要将图片上添加data-src属性,存放真实图片地址,同时添加class='lazy",最后调用lazyLoad()即可
(function () {
  // 实现延时加载
  function lazyLoad() {
    // 1、需要获取到所有需要延时加载的图片
    var imgs = document.querySelectorAll(".lazy");
    // 2、过滤掉不是图片的标签
    imgs = Array.prototype.filter.call(imgs, function (item) {
      return item instanceof Image;
    });
    lazy(); // 调用
    window.addEventListener("scroll", throttle(lazy, 100)); //  给window绑定滚动事件
    window.addEventListener("resize", throttle(lazy, 100));
    function lazy() {
      if (imgs.length === 0) return; // 当前数组中没有需要监听的图片了
      // 获取浏览器可视区的高
      var clientHeight =
        document.documentElement.clientHeight || document.body.clientHeight;

      // 遍历所有过滤后得图片,然后查看图片是否进入到可视区,
      // 如果进入到可视区,就动态给图片添加真实的图片地址
      for (var i = 0; i < imgs.length; i++) {
        // 判断图片是否进入可视区  bottom>0  && top<浏览器可视区高
        var rect = imgs[i].getBoundingClientRect();

        var bottom = rect.bottom;
        var _top = rect.top;
        // 以下条件成立,代表元素进入到可视区
        if (bottom > 0 && _top < clientHeight) {
          // 把自定义属性上的真实地址赋值给图片
          imgs[i].src = imgs[i].dataset.src;
          // 如果图片进入过一次可视区,动态赋过值,就不用再管他了
          var index = imgs.indexOf(imgs[i]);
          imgs.splice(index, 1);
          i--; // 一定要注意,i--
        }
      }
    }
  }

  /**
   * 节流函数
   * fn 事件处理函数
   * delay 函数执行间隔时间
   */
  function throttle(fn, delay) {
    var timer = null;
    return function () {
      var self = this;
      var args = arguments;
      if (timer) return;
      timer = setTimeout(function () {
        // 函数体执行代码
        fn.apply(self, args);
        // 开锁
        timer = null;
      }, delay);
    };
  }

  window.lazyLoad = lazyLoad;
})();

# 6、吸顶盒导航

TIP

涉及知识点

  • 过渡动画
  • window.onsrcoll事件
  • offsetTop、offsetHeight、scrollTop

GIF2022-12-615-40-31

<style>
  html,
  body {
    margin: 0;
    height: 100%;
  }
  .top {
    height: 50px;
    background-color: #000;
  }
  .header {
    height: 120px;
    background-color: skyblue;
  }
  .nav {
    width: 100%;
    height: 100px;
    background-color: red;
  }
  .nav2 {
    background-color: red;
    position: fixed;
    top: -100px;
  }
  .transition {
    transition: top 1s ease;
  }

  .main {
    height: 3000px;
  }
</style>

<!-- 模拟的头部内部 -->
<div class="top"></div>
<div class="header"></div>
<!-- 吸顶盒 -->
<!-- nav2相当于nav1的副本,不过nav2是固定定位在浏览器的外部 -->
<div class="nav nav1"></div>
<div class="nav nav2"></div>
<div class="main"></div>

<script>
  // 获取nav1与nav2
  var nav1 = document.querySelector(".nav1");
  var nav2 = document.querySelector(".nav2");
  var nav1Top = nav1.offsetTop; // nav1的顶部与浏览器顶部距离
  var bottom = nav1Top + nav1.offsetHeight; // nav1的底部与浏览器顶部距离

  //添加 scroll事件
  window.addEventListener("scroll", fn);
  function fn() {
    var scrollTop =
      document.documentElement.scrollTop || document.body.scrollTop;
    // 滚动条滚动距离 >= 元素底部与浏览器顶部距离
    if (scrollTop >= bottom) {
      nav2.classList.add("transition");
      //nav2 区块显示出
      nav2.style.top = "0px";
    }
    // 滚动条滚动距离 <= 元素顶部与浏览器项部距离
    if (scrollTop <= nav1Top) {
      nav2.classList.remove("transition");
      nav2.style.top = "-100px";
    }
  }
</script>

# 7、滚动加载更多

TIP

  • 当滚动条滚动到最底部时,加载一批数据,填充到页面上
  • 判断滚动条滚动到底部公式:scrollHeight - scrollTop - clientHeight = 0
属性 说明
scrollTop 元素滚动出去的高
clientHeight 元素的可视高(height + padding)
scrollHeight 元素实际内容的高
var doc = document.documentElement || document.body;
if (doc.scrollHeight - doc.scrollTop - doc.clientHeight < 100) {
  // 快滚动到底部时,就开始请求加载数据
}
<style>
  .main {
    height: 3000px;
  }
  .box1 {
    background-color: khaki;
    height: 500px;
    margin: 50px;
  }
</style>

<div class="main"></div>
<!--  滚动到底部,动态加载的内容
<div class="box1"></div>
<div class="box1"></div>
<div class="box1"></div> 
-->

<script>
  // window滚动事件
  window.addEventListener("scroll", throttle(loadmore, 100));
  // 加载更多函数
  function loadmore() {
    var doc = document.documentElement || document.body;
    var scrollHeight = doc.scrollHeight;
    var scrollTop = doc.scrollTop;
    var clientHeight = doc.clientHeight;
    // 什么时开始加载下一批内容
    // 当浏览器的srollHeight - scrollTop -clientHeight=0时,表示滚动到页面的底部
    // 但我们更希望在快到达底部前,就开始加载下一批数据,所以可以设置100左右的差值
    if (scrollHeight - scrollTop - clientHeight < 100) {
      // 加载更多内容
      //   console.log("加载更多内容");
      // 每次滚动到底部,创建一批内容,加载到页面当中
      var div = document.createElement("div");
      div.className = "box1";
      document.body.appendChild(div);

      // ajax请求
      // 把数据渲染到页面当中来
    }
  }

  /**
   * 节流函数
   * fn 事件处理函数
   * delay 函数执行间隔时间
   */
  function throttle(fn, delay) {
    var timer = null;
    return function () {
      var self = this;
      var args = arguments;
      if (timer) return;
      timer = setTimeout(function () {
        // 函数体执行代码
        fn.apply(self, args);
        // 开锁
        timer = null;
      }, delay);
    };
  }
</script>

# 8、楼梯式导航

TIP

涉及知识

  • 元素添加自定义属性绑定下标序号
  • 事件委托
  • 查找数组中第一个大于 n 的数的下标
  • 限时匀速动画
  • window.onscroll事件
  • scrollTop、offsetTop
  • 函数节流

GIF2022-11-1019-07-19

布局思路

  • foor-nav 用来制作右侧楼梯导航,采用固定定位,定位到浏览右侧
  • header 和 footer 用来占位,模拟真实网站头部和尾部占用的空间
  • main 中的直接子元素floor-item区域为楼梯导航滚动相关内容区块

# 8.1、HTML 结构

<style>
  html {
    /* 滚动条平滑滚动效果 */
    scroll-behavior: smooth;
  }
  html,
  body,
  ul,
  li {
    padding: 0;
    margin: 0;
    list-style: none;
  }
  body {
    background-color: #ddd;
  }
  .floor-nav {
    position: fixed;
    right: 50px;
    top: 100px;
    width: 50px;
    background-color: #fff;
    padding: 10px 10px;
  }
  .floor-nav li {
    height: 35px;
    line-height: 35px;
    text-align: center;
    color: #000;
    font-size: 14px;
    border-bottom: 1px dashed #ddd;
    cursor: pointer;
  }
  .floor-nav li:hover,
  .floor-nav li.active:hover {
    background-color: red;
    color: #fff;
  }

  .floor-nav li.active {
    color: red;
  }
  .main {
    width: 600px;
    margin: 0px auto;
  }
  .main .floor-item {
    width: 100%;
    background-color: skyblue;
    font-size: 50px;
    text-align: center;
    line-height: 200px;
    margin-top: 50px;
  }
</style>

<!-- 楼梯导航按扭开始 -->
<ul class="floor-nav" id="J_floor">
  <li>国创</li>
  <li>综艺</li>
  <li>娱乐</li>
  <li>电影</li>
  <li>游戏</li>
  <li>纪录片</li>
  <li>电视剧</li>
</ul>
<!-- 楼梯导航按扭开始 -->
<div class="header" style="height: 500px"></div>
<!--楼梯内容开始-->
<div id="J_app" class="main">
  <div class="floor-item" style="height: 500px">国创</div>
  <div class="floor-item" style="height: 530px">综艺</div>
  <div class="floor-item" style="height: 400px">娱乐</div>
  <div class="floor-item" style="height: 600px">电影</div>
  <div class="floor-item" style="height: 560px">游戏</div>
  <div class="floor-item" style="height: 400px">纪录片</div>
  <div class="floor-item" style="height: 620px">电视剧</div>
</div>
<!--楼梯内容结束-->
<div class="footer" style="height: 1500px"></div>

# 8.2、JS 实现思路

第一步:实现右侧楼梯导航按扭点击滚动效果

  • 利用事件委托来实现,所有 li 的点击事件全委托给父元素#J_floor来实现
  • 点击对应按扭实现:当前点击元素文字变红,其它文字为灰色,同时滚动条滚动到对应楼层
  • 要实现当前文字变红,其它文字变灰,需要定义变量 prevIndex 来保存前一项被点击元素的下标
// 获取id为 J_floor元素
var floorNav = document.getElementById("J_floor");
var navs = document.querySelectorAll("#J_floor li");
var prevIndex = -1; // 前一个楼梯按扭序号
// 事件委托,所有子元素li的点击事件委托给父元素来处理
floorNav.onclick = function (e) {
  var target = e.target;
  var tagName = target.tagName.toLowerCase();
  // 如果不是点击对应的li啥也不做
  if (tagName !== "li") return;

  // 点击后要处理的事情
  // 1.前面变红的li复原
  prevIndex !== -1 && navs[prevIndex].classList.remove("active");
  // 2.当前项变红色
  target.classList.add("active");
  // 3、把前一个序号更新为当前序号,供后面使用
  prevIndex = Array.prototype.indexOf.call(navs, target);
  // 4、滚动条滚动到当前楼层所在位置
  // .........具体实现思路看下一步,
};

第二步:实现点击导航按扭后,滚动条滚动到对应楼层

TIP

  • 创建一个空数组floorItemsTopArr = [],用来保存每一个楼层的元素与浏览器顶部的距离

  • 点击按扭时,找到对应按扭元素的下标,再找到floorItemsTopArr数组中对应下标的元素,获得当前浏览器滚动条需要滚动到的距离。

  • 为了实现滚动条在滚动时能平滑滚动,可以在html 标签的样式中添加scroll-behavior: smooth;样式

  • 因为scroll-behavior: smooth;目前在一些底版本浏览器中不支持,可以封装 JS 动画来实现

html {
  /* 滚动条滚动效果 */
  scroll-behavior: smooth;
}
var floorItems = document.querySelectorAll("#J_app .floor-item");
var floorItemsTopArr = []; // 用来保存每一层的顶部与浏览器顶部的高

// 把所有 floor-item与body顶部的距离添加到一个数组floorItemsTopArr
for (var i = 0; i < floorItems.length; i++) {
  floorItemsTopArr.push(floorItems[i].offsetTop);
}

// 以下代码,对接到上一步的 4的下面
// 4、滚动条滚动到当前楼层所在位置
// 方法一:
window.scroll(0, floorItemsTopArr[prevIndex]);
// 方法二:
// var doc = document.documentElement || document.body;
// doc.scrollTop = floorItemsTopArr[prevIndex];

// 也可以调用手动封装的 JS动画来实现滚动条平滑滚动
// 方法三:

JS 手动封装实现滚动条平滑滚动的 JS

/**
 * scrollByTop 滚动条从当前位置滚动到目标位置(只针对垂直方向)
 * target 滚动到的目标位置
 * delay 滚动的总时间
 */
function scrollByTop(target, delay = 200) {
  // 已经运动的时间
  var T = 0;
  // 开始运动的位置
  var B = window.scrollY;
  // 目标位置是 0  var targe=0;
  // 总共需要改变的距离
  var C = target - B;
  // 总共需要运动的时间
  var D = delay;

  // 开始执行动画
  timer = setInterval(function () {
    T += 20; // 记录运动的时间
    // 当前滚动条运动的距离
    var currentDistance = (T / D) * C + B;
    if (T >= D) {
      currentDistance = target;
      clearInterval(timer);
    }
    window.scroll(0, currentDistance);
  }, 20);
}

第三步:实现滚动浏览器窗口,对应楼层按扭显示对应样式

TIP

  • 如何知道当前滚动条滚动所在的对应楼层呢 ?可以通过当前滚动条滚动的距离与楼层顶部与浏览器的距离来判断
  • 找到数组中从前往后满足: 滚动条滚动距离 > 楼层顶部与浏览器顶部距离的最后一个元素 。这个元素所在的下标,就是当前滚动条滚动到的楼层所对应的下标。把这个下标保存在变量currentIndex中,供后面使用。
  • 找到对应楼层序号,就可以修改楼层的按扭样式。
var currentIndex = -1; // 当前楼梯所在按扭序号
// 当滚动浏览器窗口时,对应导航的样式要显示到对应楼层
window.onscroll = function () {
  // 不断获取浏览器滚动的距离
  var scrollY = window.scrollY;
  // 找到当前滚动所在的楼层序号
  for (var i = 0; i < floorItemsTopArr.length; i++) {
    if (scrollY >= floorItemsTopArr[i]) {
      currentIndex = i; // 最后一次找到的i就是当前滚动所在的楼层
    } else {
      break;
    }
  }

  // 相关优化代码,会添加到此处
  // ........

  // 找到对应楼层后,需要处理的事情
  prevIndex !== -1 && navs[prevIndex].classList.remove("active");
  currentIndex !== -1 && navs[currentIndex].classList.add("active");
  prevIndex = currentIndex;
};

第四步:性能优化

TIP

  • 如果滚动条一直在当前楼层中滚动,则不需要频繁执行更改楼层按扭样式的代码
  • 如果prevIndex === currentIndex 说明当前滚动条一直在当前楼层滚动
// 以下代码,添加到上一步,相关优化代码.....位置

// 优化处理,如果一直在当前楼层内滚动,不执行以下代码
if (prevIndex === currentIndex) return;

第五步:第一个按扭和最后一个按扭样式的特殊情况

TIP

  • 当滚动条滚动在最上面,不在第一个楼层区,即 滚动距离 < 第一个楼层与浏览器顶部距离,则楼层按扭文字不变红
  • 当滚动条滚动到最后面,不在最后一个楼层区,即 滚动距离 > 最后一个楼层与浏览器顶部距离 + 最后一个楼层的高度 ,则最后一个楼层按扭文字也不变红
  • 要使楼层按扭文字不变红,只需要把currentIndex = -1即可
var len = floorItems.length;
// 最后一个元素底部与浏览器顶部距离
var bottom = floorItemsTopArr[len - 1] + floorItems[len - 1].offsetHeight;
if (scrollY < floorItemsTopArr[0] || scrollY > bottom) {
  currentIndex = -1;
}

# 8.3、完整源代码

<style>
  html {
    /* 滚动条滚动效果 */
    scroll-behavior: smooth;
  }
  body,
  ul,
  li {
    padding: 0;
    margin: 0;
    list-style: none;
  }
  body {
    background-color: #ddd;
  }
  .floor-nav {
    position: fixed;
    right: 50px;
    top: 100px;
    width: 50px;
    background-color: #fff;
    padding: 10px 10px;
  }
  .floor-nav li {
    height: 35px;
    line-height: 35px;
    text-align: center;
    color: #000;
    font-size: 14px;
    border-bottom: 1px dashed #ddd;
    cursor: pointer;
  }
  .floor-nav li:hover,
  .floor-nav li.active:hover {
    background-color: red;
    color: #fff;
  }

  .floor-nav li.active {
    color: red;
  }

  .main {
    width: 600px;
    margin: 0px auto;
  }
  .main .floor-item {
    width: 100%;
    background-color: skyblue;
    font-size: 50px;
    text-align: center;
    line-height: 200px;
    margin-top: 50px;
  }
</style>

<body>
  <!-- 楼梯导航按扭开始 -->
  <div class="header" style="height: 1500px"></div>

  <!-- 楼梯导航按扭开始 -->
  <ul class="floor-nav" id="J_floor">
    <li>国创</li>
    <li>综艺</li>
    <li>娱乐</li>
    <li>电影</li>
    <li>游戏</li>
    <li>纪录片</li>
    <li>电视剧</li>
  </ul>

  <!--楼梯内容开始-->
  <div id="J_app" class="main">
    <div class="floor-item" style="height: 500px">国创</div>
    <div class="floor-item" style="height: 530px">综艺</div>
    <div class="floor-item" style="height: 400px">娱乐</div>
    <div class="floor-item" style="height: 600px">电影</div>
    <div class="floor-item" style="height: 560px">游戏</div>
    <div class="floor-item" style="height: 400px">纪录片</div>
    <div class="floor-item" style="height: 620px">电视剧</div>
  </div>

  <!--楼梯内容结束-->
  <div class="footer" style="height: 3000px"></div>

  <script>
    // 获取id为 J_floor元素
    var floorNav = document.getElementById("J_floor");
    var navs = document.querySelectorAll("#J_floor li");
    var floorItems = document.querySelectorAll("#J_app .floor-item");
    var len = floorItems.length;
    var prevIndex = -1; // 前一个楼梯按扭序号
    var currentIndex = -1; // 当前楼梯所在按扭序号
    var floorItemsTopArr = []; // 用来保存每一层的顶部与浏览器顶部的高

    // 把所有 floor-item与body顶部的距离添加到一个数组floorItemsTopArr
    for (var i = 0; i < floorItems.length; i++) {
      floorItemsTopArr.push(floorItems[i].offsetTop);
    }

    // 事件委托,所有子元素li的点击事件委托给父元素来处理
    floorNav.onclick = function (e) {
      var target = e.target;
      var tagName = target.tagName.toLowerCase();
      // 如果不是点击对应的li啥也不做
      if (tagName !== "li") return;

      // 点击后要处理的事情
      // 1.前面变红的li复原
      prevIndex !== -1 && navs[prevIndex].classList.remove("active");
      // 2.当前项变红色
      target.classList.add("active");
      // 3、把前一个序号更新为当前序号,供后面使用
      prevIndex = Array.prototype.indexOf.call(navs, target);
      // 4、滚动条滚动到当前楼层所在位置
      // 方法一:
      window.scroll(0, floorItemsTopArr[prevIndex]);
      // 方法二:
      // var doc = document.documentElement || document.body;
      // doc.scrollTop = floorItemsTopArr[prevIndex];
      // 方法三
      //   scrollByTop(floorItemsTopArr[prevIndex]);
    };

    // 当滚动浏览器窗口时,对应导航的样式要显示到对应楼层
    window.onscroll = function () {
      // 不断获取浏览器滚动的距离
      var scrollY = window.scrollY;
      // 找到当前滚动所在的楼层序号
      // 楼层数据中最后一个满足: 条件的的元素所在序号
      for (var i = 0; i < floorItemsTopArr.length; i++) {
        if (scrollY >= floorItemsTopArr[i]) {
          currentIndex = i; // 最后一次找到的i就是当前滚动所在的楼层
        } else {
          break;
        }
      }
      // 考虑第一个和最后一个的特殊情况
      // 最后一个元素底部与浏览器顶部距离
      var bottom = floorItemsTopArr[len - 1] + floorItems[len - 1].offsetHeight;
      if (scrollY < floorItemsTopArr[0] || scrollY > bottom) {
        currentIndex = -1;
      }

      // 优化处理,如果一直在当前楼层内滚动,不执行以下代码
      if (prevIndex === currentIndex) return;

      // 找到对应楼层后,需要处理的事情
      prevIndex !== -1 && navs[prevIndex].classList.remove("active");
      currentIndex !== -1 && navs[currentIndex].classList.add("active");
      prevIndex = currentIndex;
    };
  </script>
</body>

# 六、重难点总结

TIP

总结本章重难点知识,理清思路,把握重难点。并能轻松回答以下问题,说明自己就真正的掌握了。

用于故而知新,快速复习。

# 1、重点

TIP

  • 理解什么是 BOM
  • 掌握 window 对象上的以下属性和方法:window.scrollY属性,window.scroll()window.scrollBy()方法
  • 掌握 URL 的组成部分
  • 了解 navigator 对象的三大应用:检测浏览器、判断是否在微信中打开页面、设备判断 ios、android、web
  • 区分 load 与 DOMContentLoaded 事件的执行顺序
  • 如何动态加载 JS、CSS、图片

# 2、难点

WARNING

  • 对查询字符串作处理,转换为对象形式并考虑数据类型
  • 手写以下案例
    • 返回顶部
    • 图片延时加载
    • 吸顶盒导航
    • 滚动加载更多
    • 楼梯式导航
上次更新时间: 6/8/2023, 9:23:17 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X