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 相关文章推荐
jqPlot Option配置对象详解
Jul 25 Javascript
常见JS效果之图片减速度滚动实现代码
Dec 08 Javascript
获取内联和链接中的样式(js代码)
Apr 11 Javascript
jquery时间下拉框小例子
Apr 15 Javascript
JS+CSS实现实用的单击输入框弹出选择框的方法
Feb 28 Javascript
详解Javascript中的Object对象
Feb 28 Javascript
最后说说Vue2 SSR 的 Cookies 问题
May 25 Javascript
vue项目部署上线遇到的问题及解决方法
Jun 10 Javascript
详解无限滚动插件vue-infinite-scroll源码解析
May 12 Javascript
微信小程序如何获取用户头像和昵称
Sep 23 Javascript
js实现点击上传图片并设为模糊背景
Aug 02 Javascript
webpack4从0搭建组件库的实现
Nov 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的memcache类分享(memcache队列)
2014/03/26 PHP
Smarty简单生成表单元素的方法示例
2016/05/23 PHP
利用PHP判断是手机移动端还是PC端访问的函数示例
2017/12/14 PHP
php实现生成PDF文件的方法示例【基于FPDF类库】
2018/07/21 PHP
Laravel关系模型指定条件查询方法
2019/10/10 PHP
Laravel 错误提示本地化的实现
2019/10/22 PHP
读jQuery之十三 添加事件和删除事件的核心方法
2011/08/23 Javascript
jquery取消选择select下拉框示例代码
2014/02/22 Javascript
javascript实现tab切换特效
2015/11/12 Javascript
JavaScript编写点击查看大图的页面半透明遮罩层效果实例
2016/05/09 Javascript
Angularjs上传文件组件flowjs功能
2017/08/07 Javascript
Vue组件通信实践记录(推荐)
2017/08/15 Javascript
使用命令行工具npm新创建一个vue项目的方法
2017/12/27 Javascript
详解Vue的钩子函数(路由导航守卫、keep-alive、生命周期钩子)
2018/07/24 Javascript
node.js微信小程序配置消息推送的实现
2019/02/13 Javascript
微信小程序本地存储实现每日签到、连续签到功能
2019/10/09 Javascript
Element Dropdown下拉菜单的使用方法
2020/07/26 Javascript
在vue中配置不同的代理同时访问不同的后台操作
2020/09/11 Javascript
python dict 字典 以及 赋值 引用的一些实例(详解)
2017/01/20 Python
python实现列表的排序方法分享
2019/07/01 Python
基于Django实现日志记录报错信息
2019/12/17 Python
Python制作一个仿QQ办公版的图形登录界面
2020/09/22 Python
Django admin组件的使用
2020/10/24 Python
CSS3改变浏览器滚动条样式
2019/01/04 HTML / CSS
使用HTML5捕捉音频与视频信息概述及实例
2018/08/22 HTML / CSS
TripAdvisor德国:全球领先的旅游网站
2017/12/07 全球购物
运动会解说词200字
2014/02/06 职场文书
怎样写好创业计划书的内容
2014/02/06 职场文书
食品安全工作方案
2014/05/07 职场文书
高一学年自我鉴定范文(3篇)
2014/09/26 职场文书
房地产财务经理岗位职责
2015/04/08 职场文书
检讨书怎么写
2015/05/07 职场文书
高二化学教学反思
2016/02/22 职场文书
学校教代会开幕词
2016/03/04 职场文书
Java常用函数式接口总结
2021/06/29 Java/Android
我们认为中短波广播场强仪的最佳组合
2022/04/05 无线电