解密浏览器事件与请求核心原理:从事件流到Fetch实战,前端通信必备指南

今天我们来深入探讨前端开发中两个至关重要的主题:浏览器事件模型和网络请求方式。这些都是日常开发中频繁使用的技术,但你真的了解它们的底层原理和最佳实践吗?

系列文章目录

解密JavaScript面向对象(一):从新手到高手,手写call/bind实战
解密JavaScript面向对象(二):深入原型链,彻底搞懂面向对象精髓
解密作用域与闭包:从变量访问到闭包实战一网打尽
深度解密JavaScript异步编程:从入门到精通一站式搞定

一、浏览器事件模型之DOM事件

浏览器事件模型是浏览器中处理用户交互和系统事件的一整套机制,它定义了事件如何被触发、传播和处理的基本规则。

DOM(Document Object Model,文档对象模型)是针对HTML文档和XML文档的一个API。DOM描绘了一个层次化的节点树,允许开发人员添加、移出和修改页面的某一部分,DOM 脱胎于Netscape 及微软公司创始的 DHTML(动态HTML)。

1997 年的 6 月和 10 月,由于 IE4 和 Netscape Navigator4 分别支持不同的 DHTML,为了统一标准,W3C开始制定 DOM
1998 年10 月 W3C 总结了 IE 和 Navigator4 的规范,制定了 DOMLevel 1即 DOM1,之前 IE 与 Netscape 的规范则被称为 DOMLevel 0 即 DOM0 。

1.1 DOM0级事件

事件就是用户或浏览器自身执行的某种操作,如click、load、mouseover等,都是事件的名字,而响应某个事件的函数就被称为事件处理程序。

btn.onclick = function(e){
   console.log('this is a click event');
   console.log(e);  //  事件对象
}

click 事件并不会调用后才可以执行,click 事件并不确定什么时候发生,而当浏览器发现用户点击该按钮时,浏览器就检测btn.onclick是否有值,如果有,就会执行。
参数则是事件对象 event,该对象也可以通过 arguments[0] 来访问,它包含了事件相关的所有信息
(IE 中,在使用 DOM0 级方法添加事件处理程序时,event 是作 window 对象的一个属性而存在的)。

btn.onclick = function(e){
   console.log('this is a click event');
   const event = e || window.event; //兼容IE
   console.log(e);  //  事件对象
}

PS:1级DOM标准中并没有定义事件相关的内容,所以没有所谓的DOM1级事件模型

1.2 DOM2级事件

DOM2级规范开始尝试以一种符合逻辑的方式来标准化 DOM事件。
DOM0级 可以认为 onclick 等是 btn 的一个属性,DOM2级 则将属性升级为队列

DOM2级事件定义了两个方法,用于处理指定和删除事件处理程序的操作,addEventListener()和removeEventListener(),所有的 DOM 节点中都包含这两个方法。参数(事件名,函数,执行阶段:布尔值)

btn.addEventListener('click',function(){
  //  do something1
}, true);
btn.addEventListener('click',function(){
  //  do something2
}, false);
// true 代表在捕获阶段调用事件处理程序,false 表示在冒泡阶段调用事件处理程序,默认为 false;

addEventListener()将事件加入到监听队列中,当用户点击按钮时,click 队列中依次执行相关函数。
IE8 及之前,方法是attachEvent()和detachEvent(),仅接受两个参数(事件名,函数),IE8 之前的只支持事件冒泡。

1.3 总结

1)DOM2级的好处是可以添加多个事件处理程序;DOM0对每个事件只支持一个事件处理程序
2)通过DOM2添加的匿名函数无法移除。addEventListener和removeEventListener的handler必须同名;
3)触发顺序:添加多个事件时,DOM2会按照添加顺序执行,IE会以相反的顺序执行;
4)跨浏览器的事件处理程序要考虑IE的兼容

var btn = document.getElementById('btn');
btn.onClick = () => {
  console.log('我是DOM0级事件处理程序');
}
btn.onClick = null;
// 匿名函数无法移除
btn.addEventListener('click', () => {
  console.log('我是DOM2级事件处理程序');
}, false);
btn.removeEventListener('click', handler, false)
btn.attachEvent('onclick', () => {
  console.log('我是IE事件处理程序')
})
btn.detachEvent('onclick', handler);

二、事件模型之事件流

2.1 事件流

事件流描述的是从页面中接收事件的顺序,包含三个阶段:捕获阶段 → 目标阶段 → 冒泡阶段
理解这个机制是处理事件的基础。(IE仅包含冒泡流)
事件流

<!-- 示例HTML结构 -->
<div id="grandparent">
    <div id="parent">
        <button id="child">点击我</button>
    </div>
</div>
<script>
// 事件流演示
document.getElementById('grandparent').addEventListener('click', function() {
    console.log('祖父元素 - 捕获阶段', event.eventPhase);
}, true); // 第三个参数为true时,在捕获阶段响应
document.getElementById('parent').addEventListener('click', function() {
    console.log('父元素 - 目标阶段', event.eventPhase);
}); // 第三个参数默认false,即在冒泡阶段响应
document.getElementById('child').addEventListener('click', function() {
    console.log('子元素 - 冒泡阶段', event.eventPhase);
}, false); // 第三个参数为false时,在冒泡阶段响应,默认值为false

// 点击按钮后的输出顺序:
// 祖父元素 - 捕获阶段 (1)
// 子元素 - 冒泡阶段 (2)
// 父元素 - 目标阶段 (3)
</script>

解析:
1)事件捕获:最不具体的节点最先收到事件,而最具体的节点最后收到事件。顺序为:grandparent --> parent --> child
2)事件冒泡: 从最具体节点先收到事件,到父类各节点。顺序为:child --> parent --> grandparent
3) grandparent定义click事件接收的第三个参数为true,则在捕获阶段触发,grandparent先被触发;child跟parent均在冒泡阶段触发,因此先child再parent。
PS:绝大多数事件都存在该三阶段,以下常见几个不冒泡的事件需额外记住:
focus 和 blur、load 和 unload 、mouseenter 和 mouseleave 、resize、scroll

2.2 事件对象

事件触发时,事件处理程序都会自动传入事件event对象。
event对象中需要关心的两个属性如下:
1)target:始终指向最初触发事件的目标元素;
2)eventPhase:调用事件处理程序时所处阶段,捕获阶段1、处于目标2、冒泡阶段3;

事件常用方法
1)preventDefault:比如链接被点击会导航到其href指定的URL,这个就是默认行为;
2)stopPropagation:立即停止事件在DOM层次中的传播,包括捕获和冒泡事件;

document.getElementById('myButton').addEventListener('click', function(event) {
    // 事件目标相关
    console.log('target:', event.target); // 实际触发元素
    console.log('eventPhase:', event.eventPhase); // 触发元素所处阶段
    console.log('currentTarget:', event.currentTarget); // 当前处理元素
    // 事件控制
    event.preventDefault(); // 阻止默认行为
    event.stopPropagation(); // 阻止事件传播
    // 其他:鼠标键盘事件信息
    console.log('clientX:', event.clientX, 'clientY:', event.clientY);
    console.log('key:', event.key); // 键盘事件键值
});

2.3 事件委托

事件委托(Event Delegation)是一种利用事件冒泡机制的技术,它将事件监听器绑定在父级元素上,而不是每个子元素上。当子元素上的事件触发并冒泡到父元素时,父元素上的事件监听器会触发,然后通过判断事件的目标(event.target)来识别是哪个子元素触发的事件。

<div>
	<ul id="myLinks">
	  <li id="goSomewhere">Go somewhere</li>
	  <li id="doSomething">Do something</li>
	  <li id="sayHi">Say hi</li>
	</ul>
</div>
<script>
	document.getElementById('myLinks').addEventListener("click", function(event) {
  	event = event || window.event;
  	const target = event.target;
  	switch(target.id) {
  		case "doSomething":
  			document.title = "I changed the document's title";
  			break;
  		case "goSomewhere":
  			location.href = "https://blog.youkuaiyun.com/diypp2012?type=blog";
  			break;
  		case "sayHi":
  			alert("hi");
  			break; 
  	}
}
</script>

事件委托必要性:
1)性能优化:可减少内存占用(单个委托监听器 vs 数百个独立监听器)
2)动态内容支持: 通过js添加的li元素,可自动适应变化;
3)代码易维护性:统一行为(都是click事件),集中管理便于扩展

三、浏览器网络请求演进

最早的网络请求方式:只具备基础能力,同步请求且页面整体刷新HTTP Reload;或页面内嵌iframe,实现iframe内的reload加载,看似是局部页面的重新加载。
XMLHttpRequest (XHR) :实现首个真正意义上的AJAX技术,实现了页面无刷新数据交互,需要考虑同源策略跟复杂的状态管理。
AJAX:Asynchronous JavaScript and XML,即异步数据请求。

var xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log(xhr.responseText);
    }
};
xhr.send();

**Fetch API:**ES6引入,用于替代XMLHttpRequest。它使用Promise,提供了更强大和灵活的功能,但默认不会携带cookie,且需要手动处理HTTP错误(因为只有网络错误才会reject,而HTTP错误如404、500等不会reject)。

四、手写Ajax

AJAX:Ajax 是一种思想,指的是实现异步数据交换,允许页面局部更新。它并不特指某一种具体的实现方式,但基于XMLHttpRequest 实现 Ajax 是最常见的方式。
例如:jQuery库中的AJAX功能就是基于浏览器原生的XMLHttpRequest对象构建的。

实现思路:
1)创建异步请求
通过XMLHttpRequest构造函数创建一个异步对象xmlhttp,该对象上有很多属性和方法,常用的包括:
i)readyState:请求状态码,表示异步对象目前的状态。
1:表示连接已建立
2:请求已接收,正在处理中
3:请求处理中,通常响应中已有部分数据可用了
4:请求已完成
ii)onreadystatechange:
iii)status:http状态码,表示成功的代码为

xmlHttp.status >= 200 && xmlHttp.status < 300 || xmlHttp.status == 304

iv)responseText:返回的字符串形式的响应数据
v)responseXML:返回的XML形式的响应数据

// 创建异步请求

let xmlHttp;
if (window.XMLHttpRequest) {
  // code for IE7+, Firefox, Chrome, Opera, Safari
  xmlHttp = new XMLHttpRequest();
} else {
  // code for IE6, IE5
  xmlHttp = new ActiveXObject('Microsoft.XMLHTTP');
}

2)设置请求方式和请求地址
通过open()函数设置ajax请求方式和请求地址。
open函数包含三个参数:1. 请求的类型;2:请求的url地址;3:设置请求方法是不是异步async,设置为true即可。
如果发送POST请求,使用setRequestHeader()来添加 HTTP请求头

xmlHttp.open("POST","ajax_test.html",true); 
xmlHttp.setRequestHeader("Content-type","application/x-www-form-urlencoded"); 

3)发送请求
通过send()发送请求,当发送的是POST请求时,参数为要传递要发送的数据。

xmlHttp.send("name=XiaoWang&age=18"); //POST请求传参
xmlHttp.send(); //GET请求传参

4)监听状态变化
当异步对象的readyState发生改变,会触发onreadystatechange函数,当readyState变成为4时,表示当前状态是请求完毕的状态,同时当http的响应码status为200到300之间(包括200和300)或为304时,表示ajax请求成。
5)处理返回的结果
通过responseText属性来获取返回的字符串。

具体实现如下

const ajax = option => {
	// 处理option中的data数据,将object转为string
  const objToString = data => {
    let res = [];
    for (let key in data) {
      //需要将key和value转成非中文的形式,因为url不能有中文。使用encodeURIComponent();
      res.push(encodeURIComponent(key) + ' = ' + encodeURIComponent(data[key]));
    }
    return res.join('&');
  };
  const dataStr = objToString(option.data || {});

  //  1.创建一个异步对象xmlHttp;
  let xmlHttp, timer;
  if (window.XMLHttpRequest) {
    xmlHttp = new XMLHttpRequest();
  } else if (xmlHttp) {
    // code for IE6, IE5
    xmlHttp = new ActiveXObject('Microsoft.xmlHttp');
  }
  //  2.设置请求方式和请求地址;
  if (option.type.toLowerCase() === 'get') {
  	//get方式
    xmlHttp.open(option.type, option.url + '?t=' + str, true);
    //  3.发送请求;
    xmlHttp.send();
  } else {
  	//post方式
    xmlHttp.open(option.type, option.url, true);
    // 注意:在post请求中,必须在open和send之间添加HTTP请求头:setRequestHeader(header,value);
    xmlHttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    //  3.发送请求,携带参数;
    xmlHttp.send(dataStr);
  }
  //  4.监听状态的变化;
  xmlHttp.onreadystatechange = function () {
  	// 清除定时器(ajax支持请求超时处理)
    clearInterval(timer);
    if (xmlHttp.readyState === 4) {
      if ((xmlHttp.status >= 200 && xmlHttp.status < 300) || xmlHttp.status == 304) {
        //  5.处理返回的结果;
        option.success(xmlHttp.responseText); //成功后回调;
      } else {
        option.error(xmlHttp.responseText); //失败后回调;
      }
    }
  };
  //判断外界是否传入了超时时间
  if (option.timeout) {
    timer = setInterval(function () {
      xmlHttp.abort(); //中断请求
      clearInterval(timer);
    }, option.timeout);
  }
};

五、fetch详解

将fetch与 Ajax 作比较,是不对的。因为常说的 Ajax 是指使用 XMLHttpRequest 实现的 Ajax,因此fetch应该和 XMLHttpRequest 作比较。

fetch()的功能与 XMLHttpRequest 基本相同,但有三个差异:
1)fetch使用 Promise,不使用回调函数,写法简洁;
2)fetch采用模块化设计,API 分散在多个对象上(Response 对象、Request 对象、Headers 对象),更合理一些;
3)fetch通过数据流(Stream 对象)处理数据,可以分块读取,有利于提高网站性能表现,减少内存占用,对于请求大文件或者网速慢的场景相当有用。XMLHTTPRequest 对象不支持数据流,所有的数据必须放在缓存里,不支持分块读取,必须等待全部拿到后,再一次性吐出来。

fetch('https://api.github.com/users/ruanyf')
  .then(response => response.json()) // 返回的是数据流,通过json函数转换为JSON对象
  .then(json => console.log(json))
  .catch(err => console.log('Request Failed', err)); 

5.1 Response对象

fetch()请求成功以后,得到的是一个 Response 对象。该对象有很多属性,常用的有status、text、

response.status只有等于 2xx (200~299),才能认定请求成功。因而fetch仅在网络错误时才会报错,服务器返回4xx 或 5xx时,fetch不会报错(即内部Promise不会变成rejected状态);
response.ok为true表示请求成功。同status含义。
response.clone:因为结果为数据流,只能读取一次,读取完就没了,因此需要clone一下。
Response.body是 Response 对象暴露出的底层接口,返回一个 ReadableStream 对象,供用户分块读取内容(用于下载进度显示)
获取返回结果的方法也很多:
-response.text():得到文本字符串;
-esponse.json():得到 JSON 对象;
-response.blob():得到二进制 Blob 对象;
-response.formData():得到 FormData 表单对象;
-response.arrayBuffer():得到二进制 ArrayBuffer 对象;
其他具体见:Response对象

5.2 option API

fetch()请求的底层用的是 Request 对象的接口,参数完全一样
完整参数API如下:

const response = fetch(url, {
  method: "GET",
  headers: {
    "Content-Type": "text/plain;charset=UTF-8"
  },
  body: undefined,
  referrer: "about:client",
  referrerPolicy: "no-referrer-when-downgrade",
  mode: "cors", // 指定请求的模式,cors允许跨域,same-origin同源,no-cors金=仅可发送GET、POST 和 HEAD
  credentials: "same-origin", //指定是否发送Cookie,same-origin同源的发,include一律发,omit不发
  cache: "default", //如何处理缓存
  redirect: "follow", // 指定HTTP跳转的处理方式 follow随HTTP跳转,error跳转就报错,manual不随之跳转
  integrity: "", //指定哈希值,用于检查传回数据是否被改
  keepalive: false, //页面关闭后,是否后台保持数据连接,为true时可继续向服务器发送数据
  signal: undefined
});

六、实际应用场景解析

场景1:大型数据列表的优化加载

class OptimizedDataLoader {
    constructor(container, apiUrl) {
        this.container = container;
        this.apiUrl = apiUrl;
        this.data = [];
        this.loading = false;
        this.page = 1;
        this.init();
    }
    init() {
        // 滚动加载
        this.container.addEventListener('scroll', this.handleScroll.bind(this));
        // 初始加载
        this.loadData();
    }
    // 初始加载
    async loadData() {
        if (this.loading) return;
        // 显示loading
        this.loading = true;
        this.showLoading();
        try {
            // AbortController
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), 10000);
            const response = await fetch(`${this.apiUrl}?page=${this.page}`, {
                signal: controller.signal
            });
            clearTimeout(timeoutId);
            
            if (!response.ok) throw new Error('加载失败');
            // 处理返回结果
            const newData = await response.json();
            this.data = [...this.data, ...newData];
            this.renderData();
            this.page++;
        } catch (error) {
            if (error.name !== 'AbortError') {
                this.showError('数据加载失败');
            }
        } finally {
            this.loading = false;
            this.hideLoading();
        }
    }
    // 接近底部时加载更多
    handleScroll() {
        const { scrollTop, scrollHeight, clientHeight } = this.container;
        // 接近底部时加载更多
        if (scrollHeight - scrollTop - clientHeight < 100) {
            this.loadData();
        }
    }
    // 虚拟渲染优化,只渲染可见区域
    renderData() {
        this.container.innerHTML = this.data
            .map(item => `<div class="item">${item.name}</div>`)
            .join('');
    }
}

七、经典面试题解析

问题1:事件委托的性能优化

题目:如何优化以下代码,该代码包含大量子元素的事件处理性能?

// 传统方式:每个子元素绑定事件(性能差)
document.querySelectorAll('.item').forEach(item => {
    item.addEventListener('click', function() {
        console.log('点击了:', this.textContent);
    });
});

问题:代码冗余,浪费内存,不支持item项动态添加
优化:

// 事件委托优化:父元素统一处理
document.getElementById('list').addEventListener('click', function(event) {
    if (event.target.classList.contains('item')) {
        console.log('点击了:', event.target.textContent);
    }
});

问题2:请求竞态条件处理

题目:如何防止快速连续请求导致的竞态条件?

class RequestController {
    constructor() {
        // 存储待处理的request
        this.pendingRequests = new Map();
    }
    async request(url, options = {}) {
        // 合成唯一key值
        const key = this.generateKey(url, options);
        // 取消重复请求
        if (this.pendingRequests.has(key)) {
            this.pendingRequests.get(key).abort();
        }
        // AbortController控制器对象
        const controller = new AbortController();
        this.pendingRequests.set(key, controller);
        try {
            const response = await fetch(url, {
                ...options,
                signal: controller.signal
            });
            // 执行完成后删除url
            this.pendingRequests.delete(key);
            if (!response.ok) throw new Error('请求失败');
            return response.json();
            
        } catch (error) {
            this.pendingRequests.delete(key);
            if (error.name !== 'AbortError') {
                throw error;
            }
        }
    }
    // 生成唯一key
    generateKey(url, options) {
        return `${url}_${JSON.stringify(options)}`;
    }
   	// 放弃所有请求
    cancelAll() {
        this.pendingRequests.forEach(controller => controller.abort());
        this.pendingRequests.clear();
    }
}

// 使用示例
const apiController = new RequestController();
// 快速连续调用,只会保留最后一次结果
async function searchUsers(query) {
    try {
        const users = await new RequestController().request(`/api/users?q=${query}`);
        // 渲染结果
        renderUsers(users);
    } catch (error) {
        if (error.name !== 'AbortError') {
            showError(error.message);
        }
    }
}

八、总结

理解事件模型的事件流三阶段,掌握性能优化的关键事件委托。了解网络请求方式演进趋势,巧用fetch,掌握常用场景及应用原理。

下期预告

下一篇将深入探讨JavaScript模块化的发展历程及实战应用,带你从IIFE到ES6 Modules,全面掌握前端模块化核心知识!

如果觉得有帮助,请关注+点赞+收藏,这是对我最大的鼓励! 如有问题,请评论区留言

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序媛小王ouc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值