1. 固定步长的动画
// JS动画的原理:通过定时器去不断修改一个元素的某个属性,因为定时器每隔一段时间就会执行一次,元素的属性每隔一段时间就会被修改一次,元素就去了新的位置,人的眼睛看到的就是动画效果;// 固定步长的动画:每次定时器执行让这个元素改变固定的步长。const { win, css } = window.utils;let box = document.getElementById('box');let step = 10; // 设置步长let maxL = win('clientWidth') - css(box, 'width'); let timer = setInterval(() => { // 固定步长的动画,在原有的基础上累加上步长,然后再设置回去 let preLeft = css(box, 'left'); // 获取原有的 left 值 let curLeft = preLeft + step; // 动画过界判断: 判断元素是否运动到边界了,如果到达超过边界,先把定时器清除了,停止动画,并且把 curLeft 改成最大值 if (curLeft >= maxL) { clearInterval(timer); curLeft = maxL; } css(box, 'left', curLeft); // curLeft 是当前元素因下一帧的位置,要把 curLeft 设置给 box 的 // left 属性,box 才会去 curLeft 的位置}, 16);
2. 指定时长的动画
/** 目标:* 1. 理解指定时长的动画原理* 2. 比较指定时长和指定步长动画的区别** */// 固定时长的动画:在指定的时间内,从某个位置运动到指定的位置;// 在 3s 内运动到 left 的值是800;const {css, win} = window.utils;let time = 3000; // 3s 指定时间 3slet target = 800; // 目标位置// 路程 = 目标位置 - 起点位置;let begin = css(box, 'left');let change = target - begin; // 路程// 速度 = 路程 / 时间let speed = change / time; // 速度// console.log(speed);let curT = 0; // 记录从动画开始后经过的时间// 使用定时器开启动画let timer = setInterval(() => { curT += 16; // curT经过的时间 console.log(curT); if (curT >= time) { // 动画过界判断:如果 curT 大于3000,就应该修正成3000 clearInterval(timer); curT = time; } let curLeft = curT * speed; // 在 curT 时间内走过的路程 let total = begin + curLeft; // 经过 curT 时间后,元素应该所处的位置 css(box, 'left', total);}, 16);
3. utils库
// 封装一个工具集:增强代码的可复用性,提升开发效率;// utils 工具包,这里面提供了常用的方法;window.utils = (function () { /** * @desc 类数组转数组 * @param arrLike 类数组对象 * @returns {Array} 类数组对象转成的数组 */ function arrLikeToAry(arrLike) { try { return Array.from(arrLike); } catch (e) { var ary = []; // for (var i = 0; i < arrLike.length; i++) { ary.push(arrLike[i]); } return ary; } } /** * @desc JSON格式字符串转对象 * @param jsonstr JSON格式字符串 * @returns {Object} 对象 */ function toJSON(jsonstr) { if ('JSON' in window) { // 'JSON' in window 返回 false 表示 JSON 的方法不可以用 return JSON.parse(jsonstr); } else { return eval('(' + jsonstr + ')'); } } /** * @desc 获取 documentElement、document.body 的盒子模型属性 * @param attr 盒子模型属性名 * @param val 设置的值 * @returns {*} 获取的盒子模型属性值 */ function win(attr, val) { if (typeof val === 'undefined') { // 如果 val 是 undefined,证明第二个参数没传,没传就是获取 return document.documentElement[attr] || document.body[attr] // 如果函数法返回值是表达 式,它会等着表达式求值,把求出来的值作为返回值返回 } document.documentElement[attr] = document.body[attr] = val; } /** * @desc 获取当前元素相对于 body 的左上角点坐标() * @param ele 当前元素 * @returns {{left: number, top: number}} left:元素左外边到 body 左内边的距离; top: 元素的上 外边距离 body 上内边的距离 */ function offset(ele) { let left = ele.offsetLeft; // 当前元素的 offsetLeft let top = ele.offsetTop; // 当前元素的 offsetTop let parent = ele.offsetParent; // 获取当前元素的 offsetParent while (parent && parent.nodeName !== 'BODY') { left += parent.clientLeft + parent.offsetLeft; top += parent.clientTop + parent.offsetTop; parent = parent.offsetParent; } return { left, top } } /** * @desc 获取元素的计算生效的样式值 * @param ele 元素对象 * @param attr css属性 * @returns {*} css样式计算生效后的值 */ function getCss(ele, attr) { var value; // 1. 判断是否是 IE 浏览器 if ('getComputedStyle' in window) { // 判断 window 对象上有 getComputedStyle 吗 value = window.getComputedStyle(ele, null)[attr]; } else { // 执行 else 的时候说明是 IE 低版本,使用 currentStyle 属性 value = ele.currentStyle[attr]; } // 把单位去掉:把数字且带单位的,把单位去掉 var reg = /^-?\d+(\.\d+)?px|rem|em|pt$/g; if (reg.test(value)) { value = parseFloat(value); } return value } /** * @desc 设置元素对象的样式 * @param ele 元素对象 * @param attr CSS属性 * @param val 样式的值 */ function setCss(ele, attr, val) { let reg = /^(fontSize|width|height|(margin|padding)?(top|right|bottom|left)?)$/i; if (reg.test(attr)) { if (!isNaN(val)) val += 'px'; } ele.style[attr] = val; } /** * @desc 批量设置CSS样式 * @param ele * @param cssBatch */ function setBatchCss(ele, cssBatch) { // 批量设置 css 样式就是遍历传入的 CSS 对象,把样式和值依次设置给元素对象即可 // 检验 cssBatch 是不是一个对象 if (typeof cssBatch !== 'object') { throw TypeError('cssBatch is not a object') } for (let key in cssBatch) { // hasOwnProperty() 检测某个属性是不是对象私有的,是则 true,不是返回 false if (cssBatch.hasOwnProperty(key)) { // 去复习for in循环、面向对象 setCss(ele, key, cssBatch[key]); } } } /** * @desc 封装一个 css 的方法,根据参数不同有不同的功能 * @param ele 元素 * @param param CSS 样式或者 CSS 样式对象 * @param val CSS 样式值 * @returns {*} 获取时是 CSS 样式值 */ function css(ele, param, val) { // 根据传参的不同调用不同的方法 // 第二个参数是字符串类型,不传 val 时,就是获取 // 第二个参数是字符串时,并且第三个参数传了,就是设置单个样式 // 第二个参数是对象时,就是批量设置 CSS 样式 if (typeof param === 'string' && typeof val === 'undefined') { return getCss(ele, param); } if (typeof param === 'string' && typeof val !== 'undefined') { setCss(ele, param, val); return; } if (typeof param === 'object') { setBatchCss(ele, param); } } /** * @desc 判断当前元素有没有某个类名 * @param ele 元素对象 * @param className 类名 * @returns {boolean} 是否有类名 */ function hasClass(ele, className) { let cN = className.trim(); // return ele.className.includes(cN); return ele.className.indexOf(cN) !== -1; } /** * @desc 为元素添加类名 * @param ele 元素对象 * @param className 类名 */ function addClass(ele, className) { let cN = className.trim(); if (hasClass(ele, cN)) return; // 优化:如果原来的类名末尾有空格,就可以不拼接空格,如果没有时再拼接 let reg = / $/g; // 'box ' if (reg.test(ele.className)) { ele.className += `${cN}`; } else { ele.className += ` ${cN}`; } } /** * @desc 移除类名 * @param ele 元素对象 * @param className 类名 */ function removeClass(ele, className) { let cN = className.trim(); let reg = new RegExp(cN, 'g'); ele.className = ele.className.replace(reg, ''); } return { arrLikeToAry, toJSON, win, offset, getCss, setCss, setBatchCss, css, hasClass, addClass, removeClass }})();
案例:回到顶部
css部分
<!doctype html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> * { margin: 0; padding: 0; } .box { height: 5000px; background: -webkit-linear-gradient(top left, red, blue, green, orange); } .btn { position: fixed; right: 50px; bottom: 50px; border-radius: 50%; width: 80px; height: 80px; background: #999; line-height: 80px; text-align: center; color: #fff; -webkit-user-select: none; user-select: none; cursor: pointer; text-decoration: none; } </style></head><body><div id="box" class="box"></div><div class="btn" id="btn">回到顶部</div><!--<a href="#box" class="btn">回到顶部</a>--><!--锚点定位:把 a 标签的 href 设置成某个元素的 id# 元素 id 点击 a 标签就可以回到顶部 --><script src="js/utils.js"></script><script src="js/4-回到顶部.js"></script></body></html>
js部分
const {win} = window.utils; // 从 utils 中解构 win 方法// 写一个动画,让页面缓慢的回到顶部// 让页面回到顶部:需要操作盒子模型的 scrollTop 属性(页面滚动条卷去的距离)let winScrollTop = win('scrollTop'); // win 方法获取页面滚动条卷去的高度;let btn = document.getElementById('btn');// 点击的时候才会触发滚动的行为// 1. 瞬间回去/*btn.onclick = function () { win('scrollTop', 0);};*/// 2. 动画着回去:用一个定时器不断的修改页面的 scrollTop 值/*let time = 2000; // 指定时间回去; 需要计算速度let isRun = false; // 标识符:标识当前的滚动条是否正在运动btn.onclick = function () { if (isRun) return; // 如果 isRun 是 true ,说明现在有正在执行的动画,为了避免动画累加,后面的代 码不能执行; // 1. 计算速度 let winScrollTop = win('scrollTop'); // 点击时页面滚动条卷去的高度 let speed = winScrollTop / time; // 计算速度 let curT = 0; // curT 记录经过的时间 isRun = true; // 下一行就要开启定时器了,所以在这里把 isRun 置为 true let timer = setInterval(() => { curT += 16; // 让时间累加 if (curT >= time) { // 当到大于等于 time 时,应该滚动到底了 clearInterval(timer); curT = time; isRun = false; // 动画结束后把 isRun 置为 false } let change = 16 * speed; // 在 curT 时间内走过的路程 winScrollTop -= change; // 经过 curT 时间后,页面滚动条的位置 win('scrollTop', winScrollTop); // 设置回去 }, 16);};*/// 避免动画累加的第一种方案:设置标识符,初始值是 false,表示当前没有正在执行的动画。如果开始动画,我们就把这个标识符置为 true;当我们再次开启动画之前,如果这个标识符是 true,表示当前有正在执行到的动画,就不能再次开启相同的动画;而且注意当动画结束后,把标识符修改为 false;// 第二种解决动画累加的解决方案:在开启新的动画之前把之前的动画清除掉(把之前的定时器清除了);let time = 2000;// let timerId = null; 一般我们不用全局变量记录定时器 id,一般使用自定义属性的方式来记录定时器的 ID;btn.onclick = function () { // 1. 计算速度 let winScrollTop = win('scrollTop'); // 点击时页面滚动条卷去的高度 if (winScrollTop <= 0) return; // 如果卷去的高度为小于等于0,不开启动画 let speed = winScrollTop / time; // 计算速度 let curT = 0; // curT 记录经过的时间 if (this.timerId) { // 如果 timerId 不是 null 就说明之前已经有动画了,在开启新的动画之前把原来的动画清除掉 clearInterval(this.timerId); } this.timerId = setInterval(() => { curT += 16; // 让时间累加 if (curT >= time) { // 当到大于等于 time 时,应该滚动到底了 clearInterval(this.timerId); curT = time; } let change = 16 * speed; // 在 curT 时间内走过的路程 winScrollTop -= change; // 经过 curT 时间后,页面滚动条的位置 win('scrollTop', winScrollTop); // 设置回去 }, 16);};