今天我们来深入探讨前端开发中两个至关重要的主题:浏览器事件模型和网络请求方式。这些都是日常开发中频繁使用的技术,但你真的了解它们的底层原理和最佳实践吗?
系列文章目录
解密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,全面掌握前端模块化核心知识!
如果觉得有帮助,请关注+点赞+收藏,这是对我最大的鼓励! 如有问题,请评论区留言

被折叠的 条评论
为什么被折叠?



