异步与 Promise
计算机原理:进程与线程
- 进程:CPU 资源分配的最小单位
- 线程:CPU 调度的最小单位
- 解读:每一辆车代表我们日常用的每个程序,洗车场代表电脑的内存空间。每个停车位就代表了每个进程,小轿车占用小车位,大巴车占用大车位,等同于每个应用程序也会根基自身的需要的运行内存,分配不一样的进程空间。每个洗车工人代表一个线程,一个洗车位可以用多个洗车工人在干活,例如:洗车外面的,清理内饰的同时在进行;等同于一个应用程序的进程空间可以多个线程在同时干活,例如:网易云音乐,在播发音乐的同时,你还可以发表评论。同一个洗车位内的工人可以共用这个洗车位内的所以工具资源,同一个进程内的线程共用这个进程内的资源空间。一个车位至少一位洗车工,一个进程至少一个线程。
[外链图片转存中…(img-Bgpf0KIE-1692330469647)]
面试题:浏览器新开一个窗口,是进程还是线程?
- 答案:进程,从区别概念回答
- 发散 1:窗口(进程间)通信怎么通讯?storage、session、cookie => storage、cookie 的区别? => 怎么样操作 cookie,前端操作 cookie 有什么风险?=> cookie 的应用场景?
- 发散 2:浏览器原理?
浏览器原理-网页
- GUI 渲染线程:
- 解析 HTML CSS 构建 DOM 树->布局->绘制
- 与 JS 引擎线程互斥,当执行 JS 引擎线程时,GUI 渲染会被挂起,当任务队列空闲时,主线程才回去执行 GUI
- JS 引擎线程:
- 处理 JS,解析执行脚本
- 分配、处理、执行了待执行脚本同时,处理待执行事件,维护事件队列
- 阻塞 GUI 渲染。js 为何会阻塞 GUI 渲染:因为网页是单线程的,js 执行的时候,渲染会被挂起。
- 定时器触发线程:
- 异步定时器的处理和执行 - setTimeout / setInterval
- 接收 JS 引擎分配的定时器任务,并执行
- 处理完成后交于事件触发线程
- 异步 HTTP 请求线程:
- 异步执行请求类操作
- 接收 JS 引起线程异步请求操作
- 监听回调,交给事件触发线程做处理
- 事件触发线程:
- 接收所有来源的事件
- 将回调的事件依次加入到任务队列的队尾,交给 JS 引擎执行
event loop
事件循环(Event Loop)是 JavaScript 的执行机制之一,用于管理和调度异步任务的执行顺序。
事件循环的核心思想是基于一个事件队列(Event Queue)和一个调用栈(Call Stack)来实现。当 JavaScript 引擎执行代码时,同步任务会被直接放入调用栈中执行,而异步任务则会被放入事件队列中等待执行。
事件队列中的任务分为宏任务(macrotask)和微任务(microtask)两种类型。
当调用栈为空时,事件循环开始执行。首先,事件循环会将队列中的微任务一次性地全部取出并执行完毕,然后检查浏览器是否需要进行页面渲染。接下来,事件循环会从宏任务队列中取出一个任务,并放入调用栈中执行。当宏任务执行完毕后,再次执行微任务,然后重新渲染页面(如果需要),继续循环这个过程。
通过事件循环的机制,JavaScript 可以实现非阻塞的异步编程,使得程序能够同时处理多个任务,并提高了程序的响应性和性能。同时,了解事件循环的原理也有助于我们更好地理解 JavaScript 的执行过程,并编写出更高效的异步代码。
微任务与宏任务
宏任务(macro-task)和微任务(micro-task)是指在 JavaScript 执行过程中,不同类型的任务分类。
宏任务通常包括以下几种类型:
- 渲染事件(如页面加载、重新渲染)
- 用户交互事件(如点击、滚动)
- 定时器事件(如 setTimeout、setInterval)
- 网络请求完成、文件读写完成等 I/O 操作的回调
- 执行整体的 JavaScript 代码(同步代码)
而微任务则包括以下几种类型:
- Promise 的 resolve 回调函数
- MutationObserver 的回调函数
在事件循环中,当一个宏任务执行完毕后,事件循环会检查微任务队列,并将其中的所有微任务连续执行,直到微任务队列为空。这意味着,微任务会在当前宏任务执行结束后立即执行,而不会等待下一个宏任务。
具体来说,当浏览器遇到一个宏任务时,它会进入宏任务队列,并等待执行。而当浏览器遇到一个微任务时,它会立即执行,并将所有产生的新的微任务加入微任务队列中。
通过合理利用微任务,我们可以在宏任务的执行间隙执行一些高优先级的任务,从而提高页面的响应性和交互性。常见的应用场景是使用 Promise 进行异步操作,并在操作完成后立即执行相关的处理逻辑。
总结来说,宏任务表示较为宏观的、需要较长时间才能执行完毕的任务,而微任务则表示较小粒度、需要快速执行的任务。掌握宏任务和微任务的执行顺序和使用场景,可以帮助我们更好地处理异步代码和优化程序性能。
event loop 基本过程
- 初始化阶段:事件循环从全局上下文开始,并执行同步代码。任何遇到的异步任务将被放入相应的任务队列中。
- 宏任务阶段:执行异步任务队列中的微任务。
- 渲染阶段:如果 DOM 被改变,渲染 DOM。
- 宏任务阶段:在微任务阶段结束后,事件循环会从宏任务队列中选择一个任务。这个任务将被推送到调用栈中执行,直到任务执行完毕。
- 重复循环:重复进行微任务阶段、宏任务阶段和渲染阶段,直到没有更多的任务。
这就是事件循环的基本过程,它通过不断地处理微任务和宏任务,以及渲染阶段的操作,使得 JavaScript 能够处理异步任务并保持页面的响应性。注意,这只是一个简化的描述,实际的事件循环机制可能会有更多的细节和特殊情况。
上面整个流程图就是 event loop 的流程,在执行宏任务 1时会继续产生一个这样的 event loop 流程,直至宏任务 1被执行完,然后执行宏任务 2再产生这样的 event loop 流程,直到所有宏任务执行结束
执行栈/调用栈示例 demo
function fn2() {
throw new Error('抛出一个错误')
}
function fn1() {
fn2()
}
function run() {
fn1()
console.log('fn2抛出错误后,这里已经不执行了')
}
run()
// 执行栈后进先出
// 执行栈:run->fn1->fn2
// 执行完成顺序:fn2->fn1->run
// 先执行完成fn2再往下执行fn1,执行完成fn1再往下执行run,因为fn2报错了,所以fn1和run都无法往下执行完成
面试题 1:死循环
js 堆栈执行顺序与堆栈溢出/ 爆栈 / 性能卡顿 / => js 性能优化
死循环例子:
function fn() {
fn()
}
fn()
正确情况下不会写出这样的死循环,如果在 vue 的 computed 中, 对某个和 computed 有关系的的变量进行复制就会造成这样的死循环。
例如:
{
data() {
return {
bbb: 222,
ccc: 333,
}
},
computed: {
aaa() {
this.bbb = 111
return this.bbb + this.ccc
}
}
}
面试题 2:执行顺序
setTimeout(() => {
console.log('setTimeout') // 5. 宏任务2
})
new Promise((resolve, reject) => {
console.log('new Promise') // 1. 属于同步进入主线程 宏任务1
resolve()
})
.then(() => {
console.log('Promise then') // 3. 微任务1
})
.then(() => {
console.log('Promise then then') // 4. 微任务2
})
console.log('hi') // 2. 同步代码 宏任务1
解题思路:任务维度
promise 有哪些状态?
- pending/待处理的, fulfilled/满足的, rejected/被驳回的
- executor: new Promise() 的时候立即执行,接收两个参数 resolve, reject
promise 的默认状态是什么?状态是如何流转的?
- 默认:pending
- 状态流转:pending => fulfilled | rejected
手写 Promise
Promise 规范
- Promise 实例有三个状态: pending fulfilled rejected,默认状态为 pending
- 状态只能从 pending 向 fulfilled | rejected 流转
- Promise(executor) 接收一个参数 executor
- executor(resolve, reject) 是同步执行的,同时接收两个两个参数 resolve reject
- resolve(value) 成功的回调,接收一个任何类型的参数作为成功的返回值,执行 resolve(value) 会把实例的状态变为 fulfilled
- reject(reason) 失败的回调,接收一个参数通常是字符串作为失败的原因,执行 reject(reason) 会把实例的状态变为 rejected
- 实例内部需要有一个 result 属性,用来存储 resolve(value) reject(reason) 传进来的的 value 或 reason
- Promise 的 then(onResolved, onRejected) 方法接收两个参数 onResolved, onRejected,目的是为了可以拿到内部的 result
- 当状态为 fulfilled 时执行 onResolved,result 为 resolve(value) 传进来的 value
- 当状态为 rejected 时执行 onRejected,result 为 reject(reason) 传进来的 reason
- then 返回的是一个 Promise 实例,这个实例的状态是由 onResolved onRejected 的返回结果决定的,onResolved onRejected 的返回结果不是一个 Promise 实例时,then 方法都会返回一个状态为 fulfilled 的 Promise 实例, onResolved onRejected 的返回结果是一个 Promise 实例时,这个 实例的状态就是 then 方法返回实例的状态
- Promise 的 catch(onRejected) 方法接收一个参数 onRejected
- catch 返回的同样是一个 Promise 实例,这个实例的状态是由 onRejected 的返回结果决定的
1. 基本框架
class Promise {
// 用静态属性定义三个状态,方便后面使用
static PENDING = 'pending'
static FULFILLED = 'fulfilled'
static REJECTED = 'rejected'
constructor(executor) {
this.result = undefined // 存储 resolve(value) reject(reason) 传进来的的 value 或 reason
this.state = Promise.PENDING // 默认状态为 pending
const resolve = (value) => {
if (this.state === Promise.PENDING) {
this.state = Promise.FULFILLED
this.result = value
}
}
const reject = (reason) => {
if (this.state === Promise.PENDING) {
this.state = Promise.REJECTED
this.result = reason
}
}
// 这里用 try catch 是为了解决像 p3 的情况,在 executor 实参的函数体中有报错
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
}
const p1 = new Promise((resolve, reject) => {
resolve('test')
})
console.log(p1)
const p2 = new Promise((resolve, reject) => {
reject('test')
})
console.log(p2)
const p3 = new