js中的事件机制原理

98 阅读9分钟

一、事件捕获/冒泡

事件捕获:事件捕获会由最先接收到事件的元素然后传向最里边(我们可以将元素想象成一个盒子装一个盒子,而不是一个积木堆积)

事件冒泡事件冒泡即由最具体的元素(文档嵌套最深节点)接收,然后逐步上传至document

示例: 

我们点击一个span,我可能就想点击一个span,事实上他是先点击document,然后点击事件传递到span的,而且并不会在span停下,span有子元素就会继续往下,最后会依次回传至document。

image.png

DOM事件:web端Dom事件分为三类:一类:dom节点通过onclick绑定事件,通过dom.onclick = null来解绑事件;二类:dom节点通过addEventListener()方法注册事件,通过removeEventListener()来注销事件;三类:css中设置ui事件,比如:hover事件,焦点事件(input框)等;

二、DOM2级事件流

1、事件捕获阶段:事件从window对象自上而下向目标节点传播的阶段

2、处于目标阶段:真正的目标节点正在处理事件的阶段

3、事件冒泡阶段:事件从目标节点自下而上向window对象传播的阶段

示例:

image.png

image.png

addEventlistener的第三个参数为ture是阻止事件捕获传递还是false? 都不会阻止事件传递,因为ture捕获阶段触发,false冒泡阶段触发,要阻止事件传递,唯一的办法就是阻止事件冒泡:事件对象调用stopPropagation()

//addEventListener第三个参数  true捕获阶段触发  false冒泡阶段触发 
			var box1=document.querySelector(".box1")
			var box2=document.querySelector(".box2")
			var box3=document.querySelector(".box3")
			// box1.addEventListener("click",(e)=>{
			// 	console.log("box111111111a",e)
			// },true)
			// box2.addEventListener("click",(e)=>{
			// 	console.log("box22222")
			// })
			// box3.addEventListener("click",(e)=>{
			// 	// 阻止事件冒泡
			// 	// e.stopPropagation()
			// 	console.log("box33333")
			// })
			
			//addEventListener的第三个参数为true是阻止事件传递还是false?
			//都不会阻止事件传递,因为true捕获阶段触发  false冒泡阶段触发 
			//要阻止事件传递 唯一的方式就是阻止事件冒泡:事件对象调用stopPropagation()
			
			
			//优化:
			//兼容写法
			// box1.addEventListener("click",(e)=>{
			// 	console.log("box111111111a",e)
				
			// })
			// box3.addEventListener("click",(e)=>{
			// 	console.log("box3333333333a",e)
			// 	//智能浏览器:
			// 	//阻止向父级元素冒泡
			// 	// e.stopPropagation()
			// 	//阻止程序传递执行冒泡
			// 	// e.stopImmediatePropagation()
			// 	//ie8以下
			// 	// e.cancelBubble=false
			// })
			// box3.addEventListener("click",(e)=>{
			// 	console.log("box3333333333b",e)				
			// })

考试题 也是未来企业的常见的笔试面试题

事件代理: 网页设计中一种设计思想 利用事件对象中引用的目标对象这个技术来实现的

  • 无论事件触发时 是不是目标对象的监听器 在监听器内部的事件对象event中都可以访问这个事件的目标对象,利用这个特点去绑定事件给父级元素 来代理子级元素的业务,这种设计就是事件代理

      	// var box2s=document.querySelectorAll(".box2")
      	// box2s.forEach(el=>{
      	// 	el.addEventListener("click",function(e){
      	// 		console.log(this.innerHTML)
      	// 	})
      	// })
      	// //这样设计有两个缺点
      	// //1.静态的事件绑定:如果动态的添加元素进去  添加进去的元素没有这个事件
      	// //2.性能消耗更高  业务却相同
      	function load1(){
      		// var box1=document.querySelector(".box1")
      		// box1.innerHTML+='<div class="box2">hello5</div>'
      		var box1=document.querySelector(".box1")
      		var box2=document.createElement("div")
      		box2.innerHTML="hello5"
      		box2.className="box2"
      		box1.appendChild(box2)			
      	}
      	
      	
      	
      	
      	// 代理
      		// var box1=document.querySelector(".box1")
      		// box1.addEventListener("click",function(e){
      		// 	console.log(e.target.innerHTML)
      		// })
      		
      		
      		
      				
      				
      				
      </script>
              
      //额外的一些技术
              
      <div id="box5" data-hqyj1="200" data-src1="http://xxxx">
      	666
      </div>
      <script>
      	// document.onclick=function(){
      		
      	// }
      	
      	// var box1=document.querySelector(".box1")
      	// box1.addEventListener("click",function(e){
      	// 	console.log(e.target)//事件的目标对象
      	// 	console.log(e.srcElement)//事件的目标对象
      	// 	console.log(document.documentElement)//html元素
      	// 	console.log(document.body)//body元素
      	// 	console.log(document)//根节点
      	// })
      	// console.log(box5.dataset)
      	
      	// document.write("hello")
      </script>
    

重绘与回流

什么是重绘?

重绘: 当渲染树中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格,而不会影响布局的操作,比如 background-color,我们将这样的操作称为重绘。

什么是回流?

回流:当渲染树中的一部分(或全部)因为元素的规模尺寸、布局、隐藏等改变而需要重新构建的操作,会影响到布局的操作,这样的操作我们称为回流。

常见引起回流属性和方法:

  • 任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流。

(1)添加或者删除可见的 DOM 元素; (2)元素尺寸改变——边距、填充、边框、宽度和高度 (3)内容变化,比如用户在 input 框中输入文字 (4)浏览器窗口尺寸改变——resize事件发生时 (5)计算 offsetWidth 和 offsetHeight 属性 (6)设置 style 属性的值 (7)当你修改网页的默认字体时。

// 重绘 和 回流/回档
			//重绘 就是按照文档树的结构 重新绘制页面 渲染
			//回流/回档 就是页面的元素排版布局数量和节点在树中位置等发生了改变 就是回流
			//回流必然引起重绘  但是重绘不一定引起回流
			
			function  change1 () {
				var box=document.querySelector(".box")
				// box.innerHTML="6666"//回档
				// box.style.visibility= "hidden";//重绘不回流
				// box.style.display= "none";//回流
			}
                        
//我们的程序执行时 常常会操作页面  常常引起重绘/回流  
//频繁的重绘/回流会造成页面性能不好==>页面卡顿 迟缓 手机发烫
//因此操作页面时: 尽量避免高频重绘  使用框架(MVVM)

小结 :

回流必定会发生重绘,重绘不一定会引发回流。回流所需的成本比重绘高的多,改变父节点里的子节点很可能会导致父节点的一系列回流。

前端js元素style的样式操作

1.获取script脚本节点后面加载的元素节点 是获取不了的 
			//因为文档的加载是按照文档树的顺序加载的
			// var box2=document.querySelector(".box2")
			// console.log(box2)


			//解决方案一:当页面加载完成的事件触发 再去执行获取节点的方式
			// function fn(){
			// 	var box2=document.querySelector(".box2")
			// 	console.log(box2)
			// }
			// window.onload=fn


			// 解决方案二:
			//script-- defer  async  修饰src如何加载外部js资源的异步属性
			// 代码写到js文件中:
			// var box2 = document.querySelector(".box2")
			// console.log(box2)

案列:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
		<style type="text/css">
			*{
				margin: 0;
				padding: 0;
			}
			div{
				width: 300px;
				height: 300px;
				text-align: center;
				background-color: yellow;
				line-height: 150px;
				margin: 60px auto;
				font-size: 40px;
			}
		</style>
	</head>
	<body>
		<div>哈哈哈哈哈哈哈哈中午吃啥</div>
		
		
	</body>
</html>
<script type="text/javascript">
	var div=document.querySelector("div");
	//我们一般通过标签.style.属性值=值;进行赋值操作
	//那我们该如何取值呢
	var a=div.style.width;
	//这样取不可行
	//此操作正能对行间引用的css样式起作用 本质还是对象的.属性
	//对于外部引用的css样式或者head引用的css无法取出css样式的值
	
	console.log(a);
	//这是正确的去css样式值的方式
	//getComputedStyle(获取想要的标签样式)
	var obj=getComputedStyle(div);
	console.log(obj);
	console.log(obj.width);
	console.log(obj.height);
	console.log(obj["font-size"]);
	//取整操作
	var a="1782px";
	console.log(parseInt(a));
	var a1="哈哈哈1782px";
	console.log(parseInt(a1));
	var a2="1782哈哈哈px";
	console.log(parseInt(a2));
	var b=89.6767;
	var c=parseInt(b);
	console.log(c);
	//去小数操作
	a=10;
	console.log(parseFloat(a));
	a="241.31哈哈";
	console.log(parseFloat(a));
</script>

image.png

getComputedStyle (标签,null ) .属性用于获取标签的当前样式,第一个参数是标签,第二个参数是伪元素,第二个参数不填空。 getComputedStyle与ie8及以下浏览器不兼容。

标记. currentStyle用于获取标记的当前样式,但currentStyle仅在ie浏览器中可用。

  • 综上所述,如果我们在style .属性中获取内联样式,而在css中也写入了内联样式,则必须将getComputedStyle和currentStyle组合起来。 getComputedStyle和currentStyle可以编写在兼容浏览器中获取样式值的方法

防抖与节流

防抖

  • 防抖:在一段时间内允许多次触发函数,但是只在最后一次有效(执行)
    做法:
    每次触发函数清除掉原来的定时器,重新开始计时。
    如果在规定时间内不再触发函数,则执行,否则,清除掉之前的定时器,重新计时。

    <!-- 防抖:防抖和节流不同点在于,函数在一段时间内可以多次调用,尽仅在最后一次调用有效 -->
    <!-- 每一次的点击都要清除掉之前的定时器,重新定时 -->
    <input type="text">
    <button>提交</button>
    <script>
        var btn = document.querySelector('button');
        btn.addEventListener('click', debounce(submit, 2000));
        function submit() {
            console.log('点击了');
        }
        //   为submit函数添加防抖
        function debounce(func, delay) {
            var timeout;
            return function () {
                clearTimeout(timeout);
                timeout = setTimeout(() => {
                    func.apply(this, arguments);
                }, delay)
            }
        }
    </script>
</body>

节流

节流:在一段时间内,只能触发一次函数。
做法:触发函数时判断是否到达了指定时间,如果到达了指定时间,执行;否则不执行弹出警告

  1. 时间戳法

    <input type="text">
    <button>提交</button>
    <script>
          var btn = document.querySelector('button');
        btn.addEventListener('click', throttle(submit, 2000));
         function thorttle(func,delay){ //传入需要节流的函数,延迟时间
            var last = 0; // 上一次触发的时间
            return function(){
                var now = Date.now(); //获取当前时间
                if(now >= delay + last){
                    //当前时间已经符合再次触发条件了
                    func(this,arguments);
                    // console.log(this);  //this指向window
                    last = now; //当前的时间就变为了上次的
                }else{
                    console.log('时间未到');
                }
            }
        }
    </script>

  1. 定时器法
<body>
    <input type="text">
    <button>提交</button>
    <script>
          var btn = document.querySelector('button');
        btn.addEventListener('click', throttle(submit, 2000));
        function thorttle(func, delay) {
            //设置一个空的定时器
            var timer = null;
            return function () {
                if (!timer) {
                    // 如果定时间为空,执行操作
                    func.apply(this, arguments);
                    // 设置定时器,并且在delays后定时器变为空时,可再次操作
                    timer = setTimeout(() => {
                        timer = null;
                    }, delay);
                } else {
                    console.log('时间未到');
                }
            }
        }
    </script>

  1. 时间戳加定时器
// 节流throttle代码(时间戳+定时器):
var throttle = function(func, delay) {     
    var timer = null;     
    var startTime = Date.now();     
    return function() {             
        var curTime = Date.now();             
        var remaining = delay - (curTime - startTime);             
        var context = this;             
        var args = arguments;             
        clearTimeout(timer);              
        if (remaining <= 0) {                    
            func.apply(context, args);                    
            startTime = Date.now();              
        } else {                    
            timer = setTimeout(func, remaining);              
        }      
    }
}

function handle() {      
    console.log(Math.random());
} 

window.addEventListener('scroll', throttle(handle, 1000));

总结

  • 函数防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

  • 函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。

预加载和懒加载

前端页面进行浏览页面时都会发生加载的行为,其中又可分为预加载和懒加载。

  • 1.预加载:顾名思义,预加载就是在你还没有看到的地方就已经开始加载,好处就是极大的提高了用户的使用体验,但是不好的是会增加流量的消耗,而且会增加前端的压力等。

  • 2.懒加载:懒加载的主要目的是优化前端性能,减少请求数或延迟请求数。可以延迟加载,可视区域加载,或者说利用定时器进行延迟加载。但是可能会加载缓慢,页面显示延迟等。

接下来让大家更直观地看到这个区别

预加载

    <script type="text/javascript">  
            var images = new Array()  
            function preload() {  
                for (i = 0; i < preload.arguments.length; i++) {  
                    images[i] = new Image()  
                    images[i].src = preload.arguments[i]  
                }  
            }  
            // 图片文件路径,也可以是网络文件
            preload(  
                "../image/image-001.jpg",  
                "../image/image-002.jpg",  
                "../image/image-003.jpg"  
            )  
    </script>  

懒加载

实现懒加载思路就会更加清晰,咱们只需要对看见的地方进行加载,看不见的不进行加载,所以就需要获取到当前页面的位置。从而进行下一步的操作。

//页面加载时先调用一次loadPage函数
loadPage()
window.onscroll = function () {
   loadPage()
}
//封装 加载方法
function loadPage() {
    var viewHeight = document.documentElement.clientHeight;
    var viewTop = document.documentElement.scrollTop || document.body.scrollTop;
    //这里我获取的是类名,可以根据页面改为直接获取img
    //注意获取的元素要有自己的宽高,不然会直接加载整个页面的所有图片,功能也就失效了
    var img = document.getElementsByClassName("img_pdf");
    for (var i = 0; i < img.length; i++) {
       if (offsetTop1(img[i]) < (viewHeight + viewTop)) {
           var src = img[i].getAttribute("data-url");
           //赋值
           img[i].src = src;
        }
     }
 }
 
//封装获取元素离上方的高度的函数
 function offsetTop1(obj) {
    var t = obj.offsetTop;
    while (obj.offsetparent) {
        obj = obj.offsetparent;
        t = t + obj.offsetTop;
    }
    return t;
 }

这样页面向下滚动下面会开始加载,当停止滚动的时候页面也就不加载了,极大的节省了流量的消耗。

懒加载和预加载的对比

1)概念 懒加载也叫延迟加载:JS图片延迟加载,延迟加载图片或符合某些条件时才加载某些图片。 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。

2)技术 两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一 定的缓解压力作用,预加载则会增加服务器前端压力。

3)实现方式

懒加载:
  1.第一种是纯粹的延迟加载,使用setTimeOut或setInterval进行加载延迟.
  2.第二种是条件加载,符合某些条件,或触发了某些事件才开始异步下载。
  3.第三种是可视区加载,即仅加载用户可以看到的区域,这个主要由监控滚动条来实现,一般会在距用户看到
  某图片前一定距离遍开始加载,这样能保证用户拉下时正好能看到图片。(推荐写法)
预加载:
  1.CSSJavaScript实现预加载
  如果用CSS实现预加载,如果中间某一张图太大,可能会导致后面加载不进来的情况,在前端渲染可能就渲染出来。
  2.仅使用JavaScript实现预加载(推荐写法)
  3.使用Ajax实现预加载(推荐写法)