JS 基础与高级应用: JS运行机制 & Promise
JavaScript 作为一种脚本语言,其运行原理涉及到进程、线程、事件循环等概念。在浏览器环境中,JavaScript 的运行方式和其他语言有所不同,深入了解 JavaScript 的运行原理对于开发者来说至关重要。同时,Promise 作为 JavaScript 中处理异步操作的一种机制,也是 JavaScript 开发中常用的工具之一。本文将从进程、线程、浏览器原理开始介绍 JavaScript 的运行原理,然后深入探讨 Promise 的概念、原理以及手写 Promise 的实现。

JS 运行原理机制
进程与线程
在讨论 JavaScript 的运行原理之前,我们先来了解一下进程和线程的概念。进程是操作系统中资源分配的最小单位,而线程是 CPU 调度的最小单位。在并发与并行的概念中,我们可以理解为并发是指多个任务交替进行,而并行是指多个任务同时进行。进程和线程之间的区别在于进程是独立的资源分配单位,而线程是 CPU 调度的最小单位。
浏览器原理与线程协同方式
在浏览器环境中,JavaScript 是单线程运行的,这意味着它一次只能执行一个任务。但是,浏览器本身是多线程的,它包括了多个线程来处理不同的任务,比如 GUI 渲染线程、JavaScript 引擎线程、定时器线程、异步请求线程、事件触发器等。这些线程需要协同工作,以实现页面的渲染和交互。
- JavaScript 引擎线程:
- 作用: 负责解析和执行 JavaScript 代码。
- 协作关系: 当 JavaScript 引擎线程执行 JavaScript 代码时,会阻塞 GUI 渲染线程的执行,因此需要尽量减少 JavaScript 代码的执行时间,以避免页面出现卡顿现象。
- GUI 渲染线程:
- 作用: 负责解析 HTML、CSS,构建 DOM 树和 CSSOM,并进行页面的布局和绘制。
- 协作关系: 当 GUI 渲染线程执行页面的布局和绘制时,会阻塞 JavaScript 引擎线程的执行。因此,为了保持页面的流畅性,需要尽量减少 JavaScript 代码的执行时间。
- 定时器线程:
- 作用: 负责处理定时器任务的执行。
- 协作关系: 定时器线程会接收 JavaScript 引擎线程分配的定时器任务,并按时执行这些任务。定时器任务的执行不会阻塞其他线程的执行。
- 异步请求线程:
- 作用: 负责处理网络请求等异步操作。
- 协作关系: 异步请求线程会接收 JavaScript 引擎线程分配的异步网络请求任务,并执行这些任务。异步请求的执行不会阻塞其他线程的执行。
- 事件触发线程:
- 作用: 负责接收和处理事件,并将事件回调插入到任务队列中。
- 协作关系: 事件触发线程接收到各种事件,如鼠标点击、键盘输入等,然后将相应的事件回调插入到任务队列中,等待 JavaScript 引擎线程的执行。
事件循环 & 任务队列
JavaScript 的事件循环机制是其运行的核心机制之一。事件循环由主调用栈和任务队列组成。在 JavaScript 中,永远只有一个线程在执行代码,而事件循环机制确保了任务的顺序执行。当主调用栈为空时, 事件循环的执行顺序遵循先执行微任务,再执行宏任务的原则。微任务包括 Promise 的 then 方法、MutationObserver 等,而宏任务则包括定时器回调、DOM 事件回调等。
- 事件循环(Event Loop):
- 定义: 是 JavaScript 引擎处理任务和事件的机制,保证代码的顺序执行。
- 主要组成部分: 主调用栈(Call Stack)和任务队列(Task Queue)。
- 原理: 当主调用栈为空时,事件循环会从任务队列中取出一个任务执行,然后继续等待下一个任务。
- 执行顺序: 遵循先执行微任务(Microtask),再执行宏任务(Macrotask)的原则。
- 示例: 在浏览器中,事件循环负责处理 DOM 事件、定时器回调等任务的执行。
- 任务队列(Task Queue):
- 定义: 是存放待执行任务的队列,包括宏任务和微任务。
- 种类: 分为宏任务队列(Macrotask Queue)和微任务队列(Microtask Queue)。
- 特点:
- 宏任务队列:存放宿主环境触发的任务,如定时器回调、DOM 事件回调等。
- 微任务队列:存放 JavaScript 引擎发起的任务,如 Promise 的 then 方法、MutationObserver 的回调等。
- 执行优先级: 微任务的执行优先级高于宏任务,会在宏任务执行之前立即执行。
- 示例: 微任务队列中的任务通常用于处理异步操作的结果或状态更新,宏任务队列中的任务用于处理宿主环境触发的事件和定时器回调等。
宏任务 & 微任务
- 宏任务(Macrotask):
- 调度方式: 由宿主环境(浏览器或 Node.js)在合适的时机决定执行。
- 示例: 包括定时器回调、DOM 事件回调、网络请求回调等,这些任务由宿主环境触发并放入任务队列中等待执行。
- 优先级: 宏任务的优先级较低,通常会在微任务执行完毕后才执行。
- 微任务(Microtask):
- 调度方式: 由 JavaScript 引擎主动发起调度,通常在宏任务执行完毕之后立即执行。
- 示例: 主要包括 Promise 的 then 方法、MutationObserver 的回调等,这些任务由 JavaScript 引擎在特定时刻触发并放入微任务队列中。
- 优先级: 微任务的优先级较高,会在宏任务执行之前立即执行,确保了优先级较高的任务能够及时执行。
Tick 的概念
Tick 是事件循环中的一个计时单位,可以看作是一个事件循环的周期。在每个 Tick 中,事件循环会执行一次循环,处理主调用栈中的任务和任务队列中的任务。
事件循环的周期
一个完整的事件循环周期包括以下步骤:
- 执行主调用栈中的任务: 在一个 Tick 开始时,首先执行主调用栈中的任务,直到主调用栈为空。
- 处理微任务队列中的任务: 当主调用栈为空时,事件循环会检查微任务队列中是否有任务。如果有,会依次执行所有微任务,直到微任务队列为空。
- 处理宏任务队列中的任务: 如果微任务队列为空,事件循环会检查宏任务队列中是否有任务。如果有,会从宏任务队列中取出一个任务执行,直到执行完所有的宏任务或者遇到一个长时间运行的任务。
- 更新渲染: 在每个 Tick 结束时,浏览器会检查是否需要更新页面的渲染,然后进行页面的重绘和重排。
- 等待下一个 Tick: 一次完整的事件循环周期结束后,事件循环会等待下一个 Tick 开始,继续执行下一个周期。
了解事件循环与任务队列的工作原理以及 Tick 的概念对于理解 JavaScript 的异步编程模型至关重要。通过掌握事件循环的周期,我们可以更好地理解 JavaScript 的执行顺序,并优化代码以提高性能和用户体验。
Promise
出现背景
在 Web 开发中,处理异步操作是一项常见的任务,例如发起网络请求、读取文件、处理定时器等。在早期,这些异步操作通常通过回调函数来处理,但随着业务复杂度的增加,回调函数嵌套的问题变得越来越突出,导致代码难以理解和维护,形成了所谓的 "回调地狱"。为了解决这个问题,Promise 应运而生。
Promise 的状态:
- pending(等待):Promise 对象初始状态,表示异步操作还在进行中,还未完成也未失败。
- fulfilled(已履行):表示异步操作成功完成,此时会调用 Promise 的 resolve 方法,将结果值传递给 then 方法。
- rejected(已拒绝):表示异步操作失败,此时会调用 Promise 的 reject 方法,将错误原因传递给 then 方法或 catch 方法。
执行流程:
- 创建 Promise 对象:通过 new Promise() 创建一个 Promise 实例,传入执行器函数 executor。
- executor 函数执行:executor 函数立即执行,可能是异步操作,也可能是同步操作。
- 状态变更 :
- 如果 executor 函数调用了 resolve 方法,Promise 的状态变为 fulfilled,并将结果值传递给 then 方法。
- 如果 executor 函数调用了 reject 方法,Promise 的状态变为 rejected,并将错误原因传递给 then 方法或 catch 方法。
- 注册回调函数 :
- 使用 then 方法注册回调函数,then 方法接收两个参数:onFulfilled 和 onRejected,分别表示状态变为 fulfilled 和 rejected 时的回调函数。
- 使用 catch 方法注册错误处理函数,捕获 Promise 链中的错误。
- 链式调用 :
- then 方法返回一个新的 Promise 对象,可以实现链式调用,即可以在一个 then 方法中继续调用下一个 then 方法。
- catch 方法也返回一个新的 Promise 对象,可以用于捕获前面 then 方法中的错误。
- 状态传递 :
- then 方法中的回调函数可以返回一个新的 Promise 对象,以实现状态传递,即将一个 Promise 对象的状态传递给另一个 Promise 对象。
- 执行顺序 :
- 执行顺序遵循先执行微任务(Promise 的 then 方法、MutationObserver 等),再执行宏任务(定时器回调、DOM 事件回调等)的原则。
- 在每个任务(微任务或宏任务)执行完毕后,会检查是否有微任务需要执行,如果有,则立即执行微任务队列中的所有微任务,直到微任务队列为空为止。
Promise 的组成部分
Promise 是 JavaScript 中处理异步操作的对象,它代表了一个异步操作的最终完成或失败,并可以返回其结果值或错误原因。
executor 函数:
Promise 构造函数接收一个执行器函数(executor),该函数在 Promise 对象被创建时立即执行。executor 函数有两个参数:resolve 和 reject,用于改变 Promise 的状态为成功态(fulfilled)或失败态(rejected)。
实例方法:
- then 方法:Promise 实例的 then 方法用于注册在 Promise 对象状态发生变化时的回调函数,接收两个参数:onFulfilled 和 onRejected,分别表示状态变为 fulfilled 和 rejected 时的回调函数。then 方法返回一个新的 Promise 对象,可以实现链式调用。
- catch 方法:Promise 实例的 catch 方法是 then 方法的简化版本,用于注册 Promise 对象状态变为 rejected 时的回调函数。它等价于 then(null, onRejected),用于捕获 Promise 链中的错误。
全局 API:
- Promise.all:接收一个 Promise 数组作为参数,当所有 Promise 对象都变为 fulfilled 时,返回一个新的 Promise 对象,并将所有 Promise 的结果值以数组形式传递给 then 方法。
- Promise.race:接收一个 Promise 数组作为参数,返回一个新的 Promise 对象,一旦有任何一个 Promise 对象变为 fulfilled 或 rejected,就立即将该 Promise 对象的结果传递给 then 方法。
手写 Promise
js
const PENDING = 'PENDING'; // 定义常量 PENDING,表示 Promise 的初始状态
const FULFILLED = 'FULFILLED'; // 定义常量 FULFILLED,表示 Promise 的成功状态
const REJECTED = 'REJECTED'; // 定义常量 REJECTED,表示 Promise 的失败状态
// 完整版 Promise 类
class myPromise {
constructor(executor) {
// 初始化 Promise 的状态、值和原因
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
// 存储成功和失败状态下的回调函数
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
// 定义 resolve 函数,用于将状态从 pending 变为 fulfilled,并执行成功状态下的回调函数
let resolve = value => {
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach(fn => fn());
}
};
// 定义 reject 函数,用于将状态从 pending 变为 rejected,并执行失败状态下的回调函数
let reject = reason => {
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn => fn());
}
};
try {
// 执行执行器函数 executor,并传入 resolve 和 reject 函数
executor(resolve, reject);
} catch (error) {
// 如果执行过程中出现异常,则将状态变为 rejected,并将异常原因传递给 reject 函数
reject(error);
}
}
// then 方法用于注册成功和失败状态下的回调函数,并返回一个新的 Promise 对象
then(onFulfilled, onRejected) {
// 判断回调函数是否为函数类型,如果不是,则返回一个默认的回调函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error };
let promise2 = new myPromise((resolve, reject) => {
// 处理状态为 fulfilled 时的逻辑
if (this.status === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
// 处理状态为 rejected 时的逻辑
if (this.status === REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
}
// 处理状态为 pending 时的逻辑
if (this.status === PENDING) {
// 将成功和失败状态下的回调函数添加到对应的数组中
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
this.resolvePromise(promise2, x, resolve, reject);
} catch (error) {
reject(error);
}
}, 0);
});
}
});
// 返回新的 Promise 对象
return promise2;
}
// catch 方法用于捕获 Promise 链中的错误,并返回一个新的 Promise 对象
catch(onRejected) {
// 调用 then 方法,注册失败状态下的回调函数
return this.then(null, onRejected);
}
// resolvePromise 方法用于处理 Promise 的链式调用
resolvePromise(promise2, x, resolve, reject) {
// 如果 promise2 和 x 指向同一个对象,则抛出错误,防止循环引用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
// 标识是否已经调用过成功或失败状态的回调函数
let called = false;
// 如果 x 是 Promise 对象,则递归地调用 resolvePromise 方法
if (x instanceof myPromise) {
x.then(
y => {
this.resolvePromise(promise2, y, resolve, reject);
},
error => {
reject(error);
}
);
}
// 如果 x 是对象或函数,则尝试调用其 then 方法
else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then;
if (typeof then === 'function') {
then.call(
x,
y => {
if (called) return;
called = true;
this.resolvePromise(promise2, y, resolve, reject);
},
error => {
if (called) return;
called = true;
reject(error);
}
);
} else {
// 如果 x 是普通对象,则直接将其作为成功状态的返回值
resolve(x);
}
} catch (error) {
if (called) return;
called = true;
reject(error);
}
} else {
// 如果 x 是基本类型,则直接将其作为成功状态的返回值
resolve(x);
}
}
}
// 测试用例
const asyncOperation = new myPromise((resolve, reject) => {
setTimeout(() => {
resolve('异步逻辑 等待1s');
}, 1000);
});
asyncOperation
.then(value => {
console.log('then:', value);
return '1. 第一个then的返回';
})
.then(value => {
console.log('Second then:', value);
return new myPromise((resolve, reject) => {
setTimeout(() => {
resolve('2. 第二个then的返回');
}, 500);
});
})
.then(value => {
console.log('Third then:', value);
throw new Error('3. 第三个then会报一个错误');
})
.catch(error => {
console.error('Catch:', error.message);
});
扩展
栈(Stack)
栈是一种后进先出(Last In First Out,LIFO)的数据结构,类似于我们日常生活中的一摞书或者一堆盘子。在栈中,最后入栈的元素会被最先弹出。
特点:
- 后进入栈的元素会先出栈,先进入栈的元素会后出栈。
- 只允许在栈顶进行插入(压栈)和删除(弹栈)操作。
- 栈的操作主要包括压栈(Push)和弹栈(Pop)。
应用场景:
- 函数调用栈:函数调用时会将函数调用信息压入栈中,每次函数返回时会从栈顶弹出。
- 浏览器历史记录:浏览器的后退按钮可以通过栈来实现。
- 表达式求值:编译器和解释器常常使用栈来对表达式进行求值。
队列(Queue)
队列是一种先进先出(First In First Out,FIFO)的数据结构,类似于我们日常生活中排队的场景。在队列中,先进入队列的元素会先出队列。
特点:
- 先进入队列的元素会先出队列,后进入队列的元素会后出队列。
- 队列允许在队尾进行插入(入队)操作,在队首进行删除(出队)操作。
- 队列的操作主要包括入队(Enqueue)和出队(Dequeue)。
应用场景:
- 任务调度:操作系统中的任务调度通常使用队列来管理任务的执行顺序。
- 网络数据传输:网络通信中,数据包通常按照先进先出的顺序进行传输。
- 缓存:缓存队列可以用于限制系统资源的使用,并控制请求的处理速度。