JS 基础与高级应用: JS运行机制 & Promise

JS 基础与高级应用: JS运行机制 & Promise

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

JS 运行原理机制

进程与线程

在讨论 JavaScript 的运行原理之前,我们先来了解一下进程和线程的概念。进程是操作系统中资源分配的最小单位,而线程是 CPU 调度的最小单位。在并发与并行的概念中,我们可以理解为并发是指多个任务交替进行,而并行是指多个任务同时进行。进程和线程之间的区别在于进程是独立的资源分配单位,而线程是 CPU 调度的最小单位。

浏览器原理与线程协同方式

在浏览器环境中,JavaScript 是单线程运行的,这意味着它一次只能执行一个任务。但是,浏览器本身是多线程的,它包括了多个线程来处理不同的任务,比如 GUI 渲染线程、JavaScript 引擎线程、定时器线程、异步请求线程、事件触发器等。这些线程需要协同工作,以实现页面的渲染和交互。

  1. JavaScript 引擎线程:
    • 作用: 负责解析和执行 JavaScript 代码。
    • 协作关系: 当 JavaScript 引擎线程执行 JavaScript 代码时,会阻塞 GUI 渲染线程的执行,因此需要尽量减少 JavaScript 代码的执行时间,以避免页面出现卡顿现象。
  2. GUI 渲染线程:
    • 作用: 负责解析 HTML、CSS,构建 DOM 树和 CSSOM,并进行页面的布局和绘制。
    • 协作关系: 当 GUI 渲染线程执行页面的布局和绘制时,会阻塞 JavaScript 引擎线程的执行。因此,为了保持页面的流畅性,需要尽量减少 JavaScript 代码的执行时间。
  3. 定时器线程:
    • 作用: 负责处理定时器任务的执行。
    • 协作关系: 定时器线程会接收 JavaScript 引擎线程分配的定时器任务,并按时执行这些任务。定时器任务的执行不会阻塞其他线程的执行。
  4. 异步请求线程:
    • 作用: 负责处理网络请求等异步操作。
    • 协作关系: 异步请求线程会接收 JavaScript 引擎线程分配的异步网络请求任务,并执行这些任务。异步请求的执行不会阻塞其他线程的执行。
  5. 事件触发线程:
    • 作用: 负责接收和处理事件,并将事件回调插入到任务队列中。
    • 协作关系: 事件触发线程接收到各种事件,如鼠标点击、键盘输入等,然后将相应的事件回调插入到任务队列中,等待 JavaScript 引擎线程的执行。

事件循环 & 任务队列

JavaScript 的事件循环机制是其运行的核心机制之一。事件循环由主调用栈和任务队列组成。在 JavaScript 中,永远只有一个线程在执行代码,而事件循环机制确保了任务的顺序执行。当主调用栈为空时, 事件循环的执行顺序遵循先执行微任务,再执行宏任务的原则。微任务包括 Promise 的 then 方法、MutationObserver 等,而宏任务则包括定时器回调、DOM 事件回调等。

  1. 事件循环(Event Loop):
    • 定义: 是 JavaScript 引擎处理任务和事件的机制,保证代码的顺序执行。
    • 主要组成部分: 主调用栈(Call Stack)和任务队列(Task Queue)。
    • 原理: 当主调用栈为空时,事件循环会从任务队列中取出一个任务执行,然后继续等待下一个任务。
    • 执行顺序: 遵循先执行微任务(Microtask),再执行宏任务(Macrotask)的原则。
    • 示例: 在浏览器中,事件循环负责处理 DOM 事件、定时器回调等任务的执行。
  2. 任务队列(Task Queue):
    • 定义: 是存放待执行任务的队列,包括宏任务和微任务。
    • 种类: 分为宏任务队列(Macrotask Queue)和微任务队列(Microtask Queue)。
    • 特点:
      • 宏任务队列:存放宿主环境触发的任务,如定时器回调、DOM 事件回调等。
      • 微任务队列:存放 JavaScript 引擎发起的任务,如 Promise 的 then 方法、MutationObserver 的回调等。
    • 执行优先级: 微任务的执行优先级高于宏任务,会在宏任务执行之前立即执行。
    • 示例: 微任务队列中的任务通常用于处理异步操作的结果或状态更新,宏任务队列中的任务用于处理宿主环境触发的事件和定时器回调等。

宏任务 & 微任务

  1. 宏任务(Macrotask):
    • 调度方式: 由宿主环境(浏览器或 Node.js)在合适的时机决定执行。
    • 示例: 包括定时器回调、DOM 事件回调、网络请求回调等,这些任务由宿主环境触发并放入任务队列中等待执行。
    • 优先级: 宏任务的优先级较低,通常会在微任务执行完毕后才执行。
  2. 微任务(Microtask):
    • 调度方式: 由 JavaScript 引擎主动发起调度,通常在宏任务执行完毕之后立即执行。
    • 示例: 主要包括 Promise 的 then 方法、MutationObserver 的回调等,这些任务由 JavaScript 引擎在特定时刻触发并放入微任务队列中。
    • 优先级: 微任务的优先级较高,会在宏任务执行之前立即执行,确保了优先级较高的任务能够及时执行。

Tick 的概念

Tick 是事件循环中的一个计时单位,可以看作是一个事件循环的周期。在每个 Tick 中,事件循环会执行一次循环,处理主调用栈中的任务和任务队列中的任务。

事件循环的周期

一个完整的事件循环周期包括以下步骤:

  1. 执行主调用栈中的任务: 在一个 Tick 开始时,首先执行主调用栈中的任务,直到主调用栈为空。
  2. 处理微任务队列中的任务: 当主调用栈为空时,事件循环会检查微任务队列中是否有任务。如果有,会依次执行所有微任务,直到微任务队列为空。
  3. 处理宏任务队列中的任务: 如果微任务队列为空,事件循环会检查宏任务队列中是否有任务。如果有,会从宏任务队列中取出一个任务执行,直到执行完所有的宏任务或者遇到一个长时间运行的任务。
  4. 更新渲染: 在每个 Tick 结束时,浏览器会检查是否需要更新页面的渲染,然后进行页面的重绘和重排。
  5. 等待下一个 Tick: 一次完整的事件循环周期结束后,事件循环会等待下一个 Tick 开始,继续执行下一个周期。

了解事件循环与任务队列的工作原理以及 Tick 的概念对于理解 JavaScript 的异步编程模型至关重要。通过掌握事件循环的周期,我们可以更好地理解 JavaScript 的执行顺序,并优化代码以提高性能和用户体验。

Promise

出现背景

在 Web 开发中,处理异步操作是一项常见的任务,例如发起网络请求、读取文件、处理定时器等。在早期,这些异步操作通常通过回调函数来处理,但随着业务复杂度的增加,回调函数嵌套的问题变得越来越突出,导致代码难以理解和维护,形成了所谓的 "回调地狱"。为了解决这个问题,Promise 应运而生。

Promise 的状态:

  • pending(等待):Promise 对象初始状态,表示异步操作还在进行中,还未完成也未失败。
  • fulfilled(已履行):表示异步操作成功完成,此时会调用 Promise 的 resolve 方法,将结果值传递给 then 方法。
  • rejected(已拒绝):表示异步操作失败,此时会调用 Promise 的 reject 方法,将错误原因传递给 then 方法或 catch 方法。

执行流程:

  1. 创建 Promise 对象:通过 new Promise() 创建一个 Promise 实例,传入执行器函数 executor。
  2. executor 函数执行:executor 函数立即执行,可能是异步操作,也可能是同步操作。
  3. 状态变更
    • 如果 executor 函数调用了 resolve 方法,Promise 的状态变为 fulfilled,并将结果值传递给 then 方法。
    • 如果 executor 函数调用了 reject 方法,Promise 的状态变为 rejected,并将错误原因传递给 then 方法或 catch 方法。
  4. 注册回调函数
    • 使用 then 方法注册回调函数,then 方法接收两个参数:onFulfilled 和 onRejected,分别表示状态变为 fulfilled 和 rejected 时的回调函数。
    • 使用 catch 方法注册错误处理函数,捕获 Promise 链中的错误。
  5. 链式调用
    • then 方法返回一个新的 Promise 对象,可以实现链式调用,即可以在一个 then 方法中继续调用下一个 then 方法。
    • catch 方法也返回一个新的 Promise 对象,可以用于捕获前面 then 方法中的错误。
  6. 状态传递
    • then 方法中的回调函数可以返回一个新的 Promise 对象,以实现状态传递,即将一个 Promise 对象的状态传递给另一个 Promise 对象。
  7. 执行顺序
    • 执行顺序遵循先执行微任务(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)。

应用场景:

  • 任务调度:操作系统中的任务调度通常使用队列来管理任务的执行顺序。
  • 网络数据传输:网络通信中,数据包通常按照先进先出的顺序进行传输。
  • 缓存:缓存队列可以用于限制系统资源的使用,并控制请求的处理速度。
相关推荐
Serene_Dream14 分钟前
JVM 并发 GC - 三色标记
jvm·面试
xjt_090115 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农26 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式