WebSocket应用研究
本文探讨了在React中使用react-use-websocket库来实现WebSocket功能的最佳实践,包括如何封装WebSocket逻辑、处理连接状态、发送和接收消息,以及使用ArrayBuffer传输文件。此外,文中还介绍了Socket.IO的工作原理及其与WebSocket的区别,提供了Node.js后端服务器的示例代码,展示了如何实现低延迟的双向通信和进度反馈。
本文深入探讨了浏览器的事件循环机制,解释了JavaScript的执行过程、事件循环的必要性以及如何实现非阻塞调用。通过分析JS引擎的单线程特性和渲染线程的互斥,提出了通过消息队列和事件循环来处理异步任务的方案。此外,文章还介绍了宏任务与微任务的优先级机制,并通过示例代码展示了事件循环的执行顺序和现代浏览器的多线程模型。
之前在腾讯文档前端面试的时候,考官问我经典八股之一的浏览器事件循环,编程题也是与之相关的。我直接被问瓜了,深感自己在JS基础这方面的知识薄弱,虽然以后可能不做前端了,但还是来恶补一下这方面的知识为好。
为了理解事件循环,我们需要首先简要理解JS引擎的执行过程。
众所周知,Javascript是解释型的脚本语言,即运行环境在拿到它要运行的JS任务的时候得到的是一串文本,而不是编译好的二进制文件,这就要求JS解释器要立刻解析语法并生成二进制代码用于执行,以Chromium的V8引擎为例:
V8引擎模拟操作系统调用,分为两个核心部分:执行栈(execution stack)和堆(heap)
函数在运行过程中分为创建阶段(creation phase)和执行阶段(execution phase)
this
*指针上一个试图在浏览器中直接运行二进制的Flash坟头草已经三尺高了,而WebAssembly其实采取了一种折衷方案,相当于C->汇编->机器码中省去了汇编过程,但编译和链接的过程仍保留了下来
要回答这个问题,我们要先从JS语言和它的解释器本身的特性谈起:
前提3也导致了NodeJS中的事件循环和浏览器中并不一样,理论上JS解释器标准由ECMA委员会制定(只对JS语言特性负责),而浏览器事件循环标准由HTML委员会制定,因此不同Host环境下事件循环可能是不一样的,且与JS解释器彼此独立。
从以上三个前提我们可以看出,由于JS解释器只会一根筋练死劲儿(一口气跑到底),还不管其他人的死活(渲染管线),这样的话JS执行过程中用户界面出现无响应的情况(如获取数据的过程中网页假死),也无法实现即时事件响应(如按下按钮调用函数),这对于响应式网页UI来说是致命的缺陷。
为了弥补这些缺陷,实现异步和响应的需求,我们就必须引入事件循环机制(event loop)。
为了实现我们的并行JS宏图,我们可以先参考操作系统中是如何实现并行多任务的。
回顾专业知识,我们可以总结现代计算机操作系统的异步实现为:宏观上并行,微观上串行。
然而,在前面我们提到JS是单线程的,解释器和渲染管线互斥地使用一个线程,显然不能直接套用操作系统的多线程模型来实现代码并行,但我们仍可以借鉴操作系统的线程队列思路。
我们可以为某些“等待态”的任务设计一个挂起队列,即在调用栈外再维护一个“任务队列”(暂且把它称为callback queue)用于存储可能产生阻塞但无需让程序等待的任务,在这样的任务进入调用栈后就放入挂起队列中,解释器继续运行下一行代码,这样就可以既保持JS解释器原有的特性,一次性执行完全部代码,又保持页面的响应性。
我们现在让这些任务等待了,但是又迎来了新的问题,该怎么唤醒呢?答案还是藏在我们熟悉的OS里:消息机制和事件响应。
参考OS的设计,我们也可以在浏览器环境中加入类似消息队列和事件循环机制来处理异步式调用的请求和事件,具体来说就是在JS引擎和渲染线程外单独再开一个线程,维护事件和消息循环与队列。
首先,我们要明确一个概念,事件循环本质上是一个任务调度器,和操作系统的线程调度是对等的。
然后,要知道事件循环是什么、做了什么,我们就要首先明白浏览器在渲染和执行一个网页的时候做了什么。
总结上文,我们可以得到一个浏览器的结构至少需要实现三个常驻线程:
其中JS引擎是单线程运行JS脚本的;渲染引擎由于DOM是临界资源所以和JS引擎是互斥的;消息线程则是独立于二者存在的一个循环体,作用就是反复检查挂起队列的任务,并在JS线程空闲(执行栈为空)的时候将挂起队列中的回调函数压入执行栈。
可以说事件循环(event loop)就是消息线程的主要任务,它的任务就是处理这些回调函数的执行。
可以说事件循环就是浏览器的消息机制,因此它也有自己的消息队列数据结构,我们叫它Callback Queue(或者说Event Queue, Message Queue)都行。
然而JS解释器依然是单线程的,它只能通过引入一些其他线程来实现自己的异步执行,因此最后的工作流程就变成了下面这样:
fetch
, setTimeOut
时,就会把他们的执行权交给Web API(浏览器环境,这里可能不止一个线程来执行这些任务)管理(虽然JS解释器是单线程的,但我可以把它的任务交给另亿个线程啊 (浏览器环境,这里可能不止一个线程来执行这些任务)管理(虽然JS解释器是单线程的,但我可以把它的任务交给另亿个线程啊 fetch
或click
)时,就会把对应的回调函数放入Callback Queue中等待。可以在网站Javascript Online Playground上看到JS事件循环中各数据结构的动态过程,如下图所示:
在实际的生产应用中,对所有的异步任务都一视同仁显然是不合理的,毕竟操作系统里都有优先级队列呢,更强调响应和交互的网页理应对更重要的事件有更高的响应优先级(如IO、UI变化等),为此我们也为浏览器的消息队列(Callback Queue)引入了自己的优先级队列:宏任务(macrotask,如**<script>
、setTimeOut
)和微任务(microtask,如Promise
****)**。
二者的执行顺序如下:
<script>
标签),并按JS引擎的规矩一次性执行完毕宏任务的所有同步指令,这个过程中可能注册若干个宏任务和微任务常见的常见的**宏任务(macrotask)**宏任务(macrotask) 有: 有:setTimeOut
、setInterval
、setImmediate
、<script>
、I/O操作、UI渲染等
常见的常见的**微任务(microtask)**微任务(microtask) 有: 有:Promise
、process.nextTick
等
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(() => {
return console.log(3)
}).then(() => {
console.log(4)
})
console.log(5)
正确答案:1 5 3 4 2
setTimeOut
宏任务,一个Promise
微任务then()
,打印3,再入列一个Promise
微任务then()
,打印4Note:
这个地方我犯了一个错误,在Promise的第一个then()里我认为return的值是console.log的回调,并没有实际执行,但实质上这里返回的是console.log(3)作为表达式计算后的值,如果要返回回调应该写return () => console.log(3),这样3就不会被打印出来
现代浏览器不仅是多线程(任务共享资源,拥有独立上下文,靠切换上下文实现并行)的,还是多进程(资源彼此独立)的,模型也比上面的基本模型复杂得多(Chromium的内核代码量据说和操作系统相当)。
在Chrome浏览器中,一个标签页就对应了一个进程,每个进程都有自己独立的线程模型:
作者: ChlorineC
创建于: 2023-04-25 16:39:00
更新于: 2025-02-12 15:41:00
版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
本文探讨了在React中使用react-use-websocket库来实现WebSocket功能的最佳实践,包括如何封装WebSocket逻辑、处理连接状态、发送和接收消息,以及使用ArrayBuffer传输文件。此外,文中还介绍了Socket.IO的工作原理及其与WebSocket的区别,提供了Node.js后端服务器的示例代码,展示了如何实现低延迟的双向通信和进度反馈。
作者分享了个人博客框架迭代历程:从Hexo迁移到Astro的尝试,到发现Elog平台带来的启发,最终思考将Notion作为内容管理系统的新方案。文章重点描述了作者对博客系统的核心需求——解决创作同步问题,以及在探索NotionNext等解决方案过程中遇到的技术限制和思考。
前端模块化的演变包括CJS、AMD、CMD、UMD和ESM,现代浏览器支持ESM,webpack负责模块处理和打包,确保在浏览器中高效运行模块,支持代码拆分和Tree Shaking以优化性能。
本文探讨了前端开发的复杂性,提倡回归以后端为中心的架构,强调使用简单的工具和技术,如模板引擎、jQuery、alpine.js和htmx,来简化开发流程。作者认为,现代前端工具链的复杂性逐渐偏离了简化开发的初衷,建议在不引入过多复杂性的情况下,利用Web Components等标准技术来实现可复用组件,从而提高开发效率。