JS奇技之利用scroll来监听resize详解


Posted in Javascript onJune 15, 2017

前言

大家都知道知道原生的 resize 事件只能作用于 defaultView 即 window 上,那么我们应该通过什么样的方式来监听其他元素的大小改变呢?笔者最近学习发现了一种神奇的方法,通过 scroll 事件来间接实现 resize 事件的监听,本文将对这种方式进行原理的剖析与代码实现。

原理

首先,我们先来看一下 scroll 事件是干嘛的。

The scroll event is fired when the document view or an element has been scrolled.

当文档视图或者元素滚动的时候会触发 scroll 事件。

也就是说元素滚动的时候会触发这个事件,那么什么时候元素会滚动?当元素大于其父级元素,且父级元素允许其滚动的时候,该元素可以进行滚动。换句话说,元素可以滚动意味着父子元素大小不一致,这是这个方法的核心。

那么我们需要让元素大小发生改变时,使得 scrollLeft 或者 scrollTop 发生改变,从而触发 scroll 事件,进一步得知其大小发生了改变。

监听元素变大

元素变大的时候,我们可以看到更多,其内部可滚动区域将慢慢减小,但这并不会造成滚动条位置的改变,但当元素大到让滚动条消失的时候会让 scrollLeft 或者 scrollTop 变成 0,这样我们就知道了元素变大了,因此我们其实只需要 1px 来判断,其图示如下:

JS奇技之利用scroll来监听resize详解

监听元素变小

当元素变小的时候,可滚动区域会变大,滚动条的位置其实并不会进行改变,这里采取的做法是,让可滚动区域和父元素成一定的比例一起缩小,让父元素来挤压滚动区域,从而间接改变滚动条 scrollLeft 或者 scrollTop 的大小,文字描述可能不是很清楚,我们看下图:

JS奇技之利用scroll来监听resize详解

通过以上两种方式,我们可以就可以获得 resize 事件。

实现

首先,为了不影响原有的元素,我们应当创建一个和要监听元素等大的元素,并对其进行相关操作,然后我们需要两个子元素来分别监听元素变大和元素变小两个情况。因此构造如下的 HTML 结构:

<div class="resize-triggers">
 <div class="expand-trigger">
 <div></div>
 </div>
 <div class="contract-trigger"></div>
</div>

他们对应的 CSS 如下:

.resize-triggers {
 visibility: hidden;
 opacity: 0;
}

.resize-triggers,
.resize-triggers > div,
.contract-trigger:before {
 content: " ";
 display: block;
 position: absolute;
 top: 0;
 left: 0;
 height: 100%;
 width: 100%;
 overflow: hidden;
}

.resize-triggers > div {
 overflow: auto;
}

.contract-triggers:before {
 width: 200%;
 height: 200%;
}

其中 .expand-triggers 的子元素宽高应当保持大于父元素 1px,且两个触发器都应当保持在最右下角的状态,因此我们可以实现如下的状态重置函数,并在初始化和每次滚动事件的时候调用:

/**
 * 重置触发器
 * @param element 要处理的元素
 */
const resetTrigger = function(element) {
 const trigger = element.__resizeTrigger__; // 要重置的触发器
 const expand = trigger.firstElementChild; // 第一个子元素,用来监听变大
 const contract = trigger.lastElementChild; // 最后一个子元素,用来监听变小
 const expandChild = expand.firstElementChild; // 第一个子元素的第一个子元素,用来监听变大

 contract.scrollLeft = contract.scrollWidth; // 滚动到最右
 contract.scrollTop = contract.scrollHeight; // 滚动到最下
 expandChild.style.width = expand.offsetWidth + 1 + 'px'; // 保持宽度多1px
 expandChild.style.height = expand.offsetHeight + 1 + 'px'; // 保持高度多1px
 expand.scrollLeft = expand.scrollWidth; // 滚动到最右
 expand.scrollTop = expand.scrollHeight; // 滚动到最右
};

我们可以用如下函数检测元素大小是否发生了改变:

/**
 * 检测触发器状态
 * @param element 要检查的元素
 * @returns {boolean} 是否改变了大小
 */
const checkTriggers = function(element) {
 // 宽度或高度不一致就返回true
 return element.offsetWidth !== element.__resizeLast__.width || element.offsetHeight !== element.__resizeLast__.height;
};

最终,我们可以实现简单的事件监听的添加:

/**
 * 添加大小更改监听
 * @param element 要监听的元素
 * @param fn 回调函数
 */
export const addResizeListener = function(element, fn) {
if (isServer) return; // 服务器端直接返回
 if (attachEvent) { // 处理低版本ie
 element.attachEvent('onresize', fn);
 } else {
if (!element.__resizeTrigger__) { // 如果没有触发器
  if (getComputedStyle(element).position === 'static') {
  element.style.position = 'relative'; // 将static改为relative
  }
createStyles();
  element.__resizeLast__ = {}; // 初始化触发器最后的状态
  element.__resizeListeners__ = []; // 初始化触发器的监听器

  const resizeTrigger = element.__resizeTrigger__ = document.createElement('div'); // 创建触发器
  resizeTrigger.className = 'resize-triggers';
  resizeTrigger.innerHTML = '<div class="expand-trigger"><div></div></div><div class="contract-trigger"></div>';
  element.appendChild(resizeTrigger); // 添加触发器

  resetTrigger(element); // 重置触发器
  element.addEventListener('scroll', scrollListener, true); // 监听滚动事件

  /* Listen for a css animation to detect element display/re-attach */
  // 监听CSS动画来检测元素显示或者重新添加
  if (animationStartEvent) { // 动画开始
  resizeTrigger.addEventListener(animationStartEvent, function(event) { // 增加动画开始的事件监听
   if (event.animationName === RESIZE_ANIMATION_NAME) { // 如果是大小改变事件
   resetTrigger(element); // 重置触发器
   }
  });
  }
 }
 element.__resizeListeners__.push(fn); // 加入该回调
 }
};

以及如下的函数来移除事件监听:

/**
 * 移除大小改变的监听
 * @param element 被监听的元素
 * @param fn 对应的回调函数
 */
export const removeResizeListener = function(element, fn) {
if (attachEvent) { // 处理ie
 element.detachEvent('onresize', fn);
 } else {
 element.__resizeListeners__.splice(element.__resizeListeners__.indexOf(fn), 1); // 移除对应的回调函数
 if (!element.__resizeListeners__.length) { // 如果全部时间被移除
  element.removeEventListener('scroll', scrollListener); // 移除滚动监听
  element.__resizeTrigger__ = !element.removeChild(element.__resizeTrigger__); // 移除对应的触发器,但保存下来
 }
 }
};

其他

其中有部分内容是用来优化的,并不影响基础功能,如对服务器渲染、客户端渲染的区分,对 IE 的特殊处理,以及通过 opacity 的动画来解决 chrome 上的bug。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如有疑问大家可以留言交流,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
Jquery 的outerHeight方法使用介绍
Sep 11 Javascript
JavaScript对象反射用法实例
Apr 17 Javascript
jquery UI Datepicker时间控件的使用方法(加强版)
Nov 07 Javascript
JavaScript继承模式粗探
Jan 12 Javascript
javascript求日期差的方法
Mar 02 Javascript
mpvue中配置vuex并持久化到本地Storage图文教程解析
Mar 15 Javascript
vuex操作state对象的实例代码
Apr 25 Javascript
详解JS转换数值函数Number()、parseInt()、parseFloat()
Aug 24 Javascript
Vue keepAlive 数据缓存工具实现返回上一个页面浏览的位置
May 10 Javascript
jQuery实现轮播图效果demo
Jan 11 jQuery
深入理解Antd-Select组件的用法
Feb 25 Javascript
vue同个按钮控制展开和折叠同个事件操作
Jul 29 Javascript
package.json文件配置详解
Jun 15 #Javascript
一次围绕setTimeout的前端面试经验分享
Jun 15 #Javascript
利用JS对iframe父子(内外)页面进行操作的方法教程
Jun 15 #Javascript
使用Require.js封装原生js轮播图的实现代码
Jun 15 #Javascript
JavaScript实现选中文字提示新浪微博分享效果
Jun 15 #Javascript
详解vue跨组件通信的几种方法
Jun 15 #Javascript
Bootstrap响应式导航由768px变成992px的实现代码
Jun 15 #Javascript
You might like
PHP+MYSQL会员系统的登陆即权限判断实现代码
2011/09/23 PHP
php 访问oracle 存储过程实例详解
2017/01/08 PHP
js获取元素在浏览器中的绝对位置
2010/07/24 Javascript
javascript对talbe进行动态添加、删除、验证实现代码
2012/03/29 Javascript
js获取单选框或复选框值及操作
2012/12/18 Javascript
在JavaScript中重写jQuery对象的方法实例教程
2014/08/25 Javascript
JavaScript charCodeAt方法入门实例(用于取得指定位置字符的Unicode编码)
2014/10/17 Javascript
nodejs中的fiber(纤程)库详解
2015/03/24 NodeJs
JavaScript对象属性检查、增加、删除、访问操作实例
2015/07/08 Javascript
Javascript实现获取及设置光标位置的方法
2015/07/21 Javascript
javascript的 {} 语句块详解
2016/02/27 Javascript
JavaScript中的this使用详解
2016/07/27 Javascript
js中scrollTop()方法和scroll()方法用法示例
2016/10/03 Javascript
JavaScript设计模式之构造函数模式实例教程
2018/07/02 Javascript
jquery 动态遍历select 赋值的实例
2018/09/12 jQuery
vue、react等单页面项目部署到服务器的方法及vue和react的区别
2018/09/29 Javascript
JQuery获取可视区尺寸和文档尺寸及制作悬浮菜单示例
2019/05/14 jQuery
Node.js实现简单的爬取的示例代码
2019/06/25 Javascript
VUE+elementui面包屑实现动态路由详解
2019/11/04 Javascript
在vue中使用axios实现post方式获取二进制流下载文件(实例代码)
2019/12/16 Javascript
[04:21]狐狸妈带你到现场 DOTA2 TI中国区预选赛线下赛路线指引
2014/05/22 DOTA
wxpython 学习笔记 第一天
2009/02/09 Python
Python实现获取命令行输出结果的方法
2017/06/10 Python
对python dataframe逻辑取值的方法详解
2019/01/30 Python
Python paramiko模块使用解析(实现ssh)
2019/08/30 Python
python打印n位数“水仙花数”(实例代码)
2019/12/25 Python
keras获得model中某一层的某一个Tensor的输出维度教程
2020/01/24 Python
通过实例简单了解Python sys.argv[]使用方法
2020/08/04 Python
HTML5单页面手势滑屏切换原理分析
2017/07/10 HTML / CSS
吉列剃须刀美国官网:Gillette美国
2018/07/13 全球购物
庆元旦演讲稿
2014/09/15 职场文书
政府班子四风问题整改措施
2014/10/04 职场文书
酒店财务部岗位职责
2015/04/14 职场文书
教师廉政准则心得体会
2016/01/20 职场文书
Python连续赋值需要注意的一些问题
2021/06/03 Python
nginx日志格式分析和修改
2022/04/28 Servers