笔记-6-JavaScript高级补充(跨域/事件循环)
一、什么是跨域?什么是同源策略?
跨域问题通常是由浏览器的同源策略(Same-Origin Policy,SOP)引起的访问问题。
同源策略是浏览器的一个重要安全机制,他用于限制一个来源的文档或脚本如何能够与另一个来源的资源进行交互。
当两个URL的协议、主机(Host)、端口都相同时才被认为是同源。
在早期,服务器渲染的网站不会有跨域问题。随着前后端分离的出现,前端代码和后端API经常部署在不同的服务器上,就会引发跨域问题。
CORS(Cross-Origin Resource Sharing,跨域资源共享)
它使用额外的HTTP头来告诉浏览器允许从其他域加载资源。
通过CORS,服务器可以显示声明哪些源站点有权限访问它的资源。
预请求和实际请求:
对于复杂请求(如PUT、DELETE或者自定义头),浏览器会先发一个OPTIONS请求,询问服务器是否允许跨域请求。
服务器如果允许跨域,响应会包含CORS信息。
如果预请求被允许,浏览器会发送实际请求,并在请求头总包含一些CORS的信息。
在日常的开发中我们普遍使用的还是正向代理的方式。
在webpack、vite中配置代理。
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
}
},
},
})
它们底层是开启一个新的Node服务器代理来解决跨域的。
Vite 或者 Webpack 使⽤ http-proxy 或 http-proxy-middleware 来创建代理中间件。
生产环境一般使用Nginx做反向代理。如果Nginx只代理API服务器,需要在Nginx配置中添加CORS信息。(Access-Control-Allow-Origin等)
正向代理:
代表客户端,向服务端发起请求。客户端知道代理的存在,服务端可能不知道真实的发起者。
反向代理:
代表服务端,接收来自客户端的请求。服务端知道代理的存在,客户端不知道真实的服务端。(负载均衡、防火墙、压缩)
二、事件循环
Event Loop
浏览器的事件循环是一个在javascript引擎和渲染引擎之间协调工作的机制。
因为javascript是单线程的,所以所有需要被执行的操作都需要通过一定的机制来协调它们有序的进行。
JS代码在同一时刻只能做一件事情,如果这件事情非常耗时,就意味着当前线程被阻塞
所以真正耗时的操作比如网络请求、定时器等,是用其他线程操作的,只需要在特定的时机,执行对应的回调函数即可
目前多数浏览器是多进程的,每打开一个tab页面就会开启一个新进程,每个进程中又有多个线程,其中包括执行JavaScript代码的线程。
调用栈(Call Stack)和任务队列(Task Queue)
调用栈:存储在程序执行过程中创建的所有执行上下文,函数调用时它的执行上下文被压入栈,函数执行完毕,弹出。先进后出。
任务队列:存储待处理的事件,比如定时器到期、网络请求完成、点击等等,先进先出。(又分宏任务队列和微任务队列)
JavaScript单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
当调用栈为空时,事件循环会从任务队列中取出任务执行
优先处理微任务队列,微任务队列清空后,开始执行宏任务。再执行下一个宏任务前会检查微任务队列,如有执行微任务。
宏任务是一个比较大的任务单位,可看作是一个独立的工作单元。
当一个宏任务执行完毕后,浏览器可以在两个宏任务之间进行页面渲染或处理其他事务(比如执行微任务)
微任务的执行优先级高于宏任务
MutationObserver:监视DOM变更的API,在Vue2源码中也有使用它来实现微任务的调度
宏任务(MacroTasks):包括脚本(script)、setTimeout、setInterval、I/O、UI rendering、setImmediate(node中)等
微任务(MicroTasks):包括Promise.then、MutationObserver、process.nextTick(仅在node中)、queueMicrotask(显示创建微任务API)等
1.执行当前宏任务
2.执行完当前宏任务后,检查并执行所有微任务。在微任务执行期间产生的新的微任务也会被连续执行,直到微任务队列清空。
3.渲染更新界面(如果有必要)
4.请求在一个宏任务,重复上述过程
执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务(microTask)队列是否为空,如果为空的话,就执行Task(宏任务),否则就一次性执行完所有微任务。
每次单个宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环。
由于微任务具有较高的执行优先级,它们适合用于需要尽快执行的小任务,比如处理异步的状态更新
宏任务适合用于分割较大的、需要较长时间执行的任务,以避免阻塞UI更新或其他高优先级的操作
以上是浏览器中的事件循环。
浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同浏览器可能会有不同的实现。
而Node中的事件循环是由libuv实现的,这是一个处理异步事件的C库。
libuv主要维护了一个EventLoop和worker threads(线程池)
EventLoop负责调用系统的一些其他操作:文件IO、Network、child- processes等
Node.js的事件循环包含⼏个主要阶段,每个阶段都有⾃⼰的特定类型的任务(宏任务)
从上往下依次执行,然后再次回到顶部 就叫一次循环
对每个阶段进⾏详细的解释:
timers(定时器):这⼀阶段执⾏setTimeout和setInterval的回调函数。
Pending Callbacks(待定回调):executes I/O callbacks deferred to the next loop iteration(官⽅的解释)
这意味着在这个阶段,Node.js处理⼀些上⼀轮循环中未完成的I/O任务。
具体来说,这些是⼀些被推迟到下⼀个事件循环迭代的回调,通常是由于某些操作⽆法在它们被调度的
那⼀轮事件循环中完成。
⽐如操作系统在连接TCP时,接收到ECONNREFUSED(连接被拒绝)。
idle, prepare(空闲、准备):只⽤于系统内部调⽤。
poll(轮询):检索新的 I/O 事件;执⾏与 I/O 相关的回调。
检索新的I/O事件:这⼀部分,libuv负责检查是否有I/O操作(如⽂件读写、⽹络通信)完成,并准备好了相应的回调函数。
执⾏I/O相关的回调:⼏乎所有类型的I/O回调都会在这⾥执⾏,除了那些特别由 timers 和setImmediate 安排的回调以及某些关闭回调( close callbacks )。
check(检查): setImmediate() 的回调在这个阶段执⾏。
close callbacks(关闭回调):如 socket.on('close', ...) 这样的回调在这⾥执⾏。
我们会发现从⼀次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:
宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件;
微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask;
但是,Node中的事件循环中微任务队列划分的会更加精细:(又分成两个)
next tick queue:process.nextTick;
other queue:Promise的then回调、queueMicrotask;
整体的执行顺序是:next tick > 微任务 > 宏任务
那么它们的整体执⾏时机是怎么样的呢?
- 调⽤栈执⾏:Node.js ⾸先执⾏全局脚本或模块中的同步代码。这些代码在调⽤栈中执⾏,直到栈被清空。
- 处理 process.nextTick() 队列:⼀旦调⽤栈为空,Node.js 会⾸先处理 process.nextTick() 队列中的所有回调。这确保了任何在同步执⾏期间通过process.nextTick() 安排的回调都将在进⼊任何其他阶段之前执⾏。
- 处理其他微任务:处理完 process.nextTick() 队列后,Node.js 会处理 Promise 微任务队列。这些微任务包括由 Promise.then() 、 Promise.catch() 或 Promise.finally() 安排的回调。
然后
开始事件循环的各个阶段:(宏任务)
timers阶段:处理 setTimeout() 和 setInterval() 回调。
I/O 回调阶段:处理⼤多数类型的I/O相关回调。
poll阶段:等待新的I/O事件,处理poll队列中的事件。
check阶段:处理 setImmediate() 回调。
close回调阶段:处理如 socket.on('close', ...) 的回调。
这⾥有⼀个特别的Node处理:微任务在事件循环过程中的处理
- 在事件循环的任何阶段之间,以及在上述每个阶段内部的任何单个任务后
- Node.js 会再次处理 process.nextTick() 队列和 Promise 微任务队列。
- 这确保了在事件循环的任何时刻,微任务都可以优先并迅速地被处理。
描述process.nextTick在Node.js中事件循环的执⾏顺序,以及其与微任务的关系
在 Node.js 中, process.nextTick() 是⼀个在事件循环的各个阶段之间允许开发者插⼊操作的功能:
其特点是具有极⾼的优先级,可以在当前操作完成后、任何进⼀步的I/O事件(包括由事件循环管理的其他微任务)处理之前执⾏。
process.nextTick() 的执⾏顺序:
1.调⽤栈清空:Node.js ⾸先执⾏完当前的调⽤栈中的所有同步代码。
2.执⾏ process.nextTick() process.nextTick() 队列:⼀旦调⽤栈为空,Node.js 会检查 process.nextTick() 队列。
如果队列中有任务,Node.js 会执⾏这些任务,即使在当前事件循环的迭代中有其他微任务或宏任务排队等待。
3.处理其他微任务:在 process.nextTick() 队列清空之后,Node.js 会处理由 Promises 等产⽣的微任务队列。
4.继续事件循环:处理完所有微任务后,Node.js 会继续进⾏到事件循环的下⼀个阶段(例如 timers、I/Ocallbacks、poll 等)。
与微任务的关系:
- 优先级: process.nextTick() 创建的任务和 Promise 是不同的,但它们是⼀种微任务,并且在所有微任务中具有最⾼的执⾏优先级。这意味着 process.nextTick() 的回调总是在其他微任务(例如 Promise 回调)之前执⾏。
- 微任务队列:在任何事件循环阶段或宏任务之间,以及在宏任务内部可能触发的任何点,Node.js 都可能执⾏
process.nextTick() 。执⾏完这些任务后,才会处理 Promise 微任务队列。
process.nextTick() 的命名在 Node.js 社区中曾经引起过⼀些讨论,因为它可能会导致⼀些误解。