JavaScript内功修炼:前端异步编程规范

异步编程背景和Promise的引入原因

异步编程的前置知识

异步编程在JavaScript中出现和发展的原因,主要是由JavaScript的执行环境和其单线程的特性所决定。这里有几个关键点来解释为什么异步编程变得如此重要。

  • 单线程执行环境

    • JavaScript最初被设计为一种在浏览器中运行的脚本语言,用于添加交互性和动态性。它在设计之初就是单线程的,这意味着在任何给定时刻,JavaScript在同一执行上下文中只能执行一个任务。这种设计简化了事件处理和DOM操作,因为它避免了多线程编程中常见的复杂性,如数据竞争和锁定问题。
  • 非阻塞I/O

    • 由于JavaScript是单线程的,阻塞式操作(如长时间运行的计算或网络请求)会冻结整个程序,导致不良的用户体验。为了避免这种情况,JavaScript环境提供了非阻塞I/O操作,这意味着可以在等待某些操作(如数据从服务器加载)完成时,继续执行其他脚本。
  • 事件循环和回调函数

    • JavaScript利用事件循环和回调函数来实现异步编程。事件循环允许JavaScript代码、事件回调和系统I/O等任务在适当的时候从任务队列中被取出执行,而不会阻塞主线程。这种模型支持了异步的回调形式,使得开发者可以编写非阻塞的代码,从而提高应用性能和响应速度。
  • 提高性能和响应性

    • 异步编程允许在等待操作完成(如从服务器获取数据)的同时,继续处理用户界面的交互和其他脚本,从而提高了Web应用的性能和响应性。用户不需要等待所有数据都加载完成才能与页面交互,这对于创建流畅的用户体验至关重要。
  • 发展需求

    • 随着Web技术的发展和应用越来越复杂,对于更高效、更可靠的异步编程模式的需求也随之增加。这推动了诸如Promiseasync/await等新的异步编程模式的出现,使得管理复杂的异步操作和链式调用更加简单和直观。

相关文章:

Promise的引入原因

随着Web应用程序变得越来越复杂,传统的回调方式开始显得力不从心。虽然回调函数提供了一种处理异步操作的手段,但它们也带来了所谓的"回调地狱"(Callback Hell),尤其是在处理多个异步操作时,代码会变得难以理解和维护。因此,为了解决这些问题,Promise应运而生。

  • 简化异步代码Promise提供了一种更优雅的方式来处理异步操作。通过使用Promise,可以避免深层嵌套的回调函数,使代码结构更加清晰和简洁。
  • 链式调用Promise支持链式调用(thenable链),这意味着可以按顺序排列异步操作,而不需要嵌套回调函数。这使得读写代码变得更加直观,也便于理解异步操作的流程。
  • 错误处理 :在传统的回调模式中,错误处理往往比较复杂且容易出错。Promise通过catch方法提供了一种集中处理错误的机制,使得错误处理更加一致和可靠。
  • 状态管理Promise对象有三种状态:pending(等待中)、fulfilled(已成功)和rejected(已失败)。这种状态管理让异步操作的结果和状态变得可预测,并且只能从pending状态转换到fulfilledrejected状态,且状态一旦改变就不会再变,这为异步编程提供了更稳定的基础。
  • 改进的并发控制Promise还提供了Promise.allPromise.race等静态方法,使得并发执行和管理多个异步操作变得更加简单和高效。

Promise的引入是为了解决回调模式中存在的问题,同时提供了一种更强大、更灵活、更易于管理的异步编程解决方案。随后,ES2017标准引入的async/await语法进一步简化了异步操作的编写,但底层机制仍然基于Promise,说明了Promise在现代JavaScript异步编程中的核心地位。

Promise的拆解

拆解resolve和reject

javascript 复制代码
let p1 = new Promise((resolve, reject) => {
    resolve('success')
    reject('fail')
})
console.log('p1', p1)
​
let p2 = new Promise((resolve, reject) => {
    reject('success')
    resolve('fail')
})
console.log('p2', p2)
​
let p3 = new Promise((resolve, reject) => {
    throw('error')
})
console.log('p3', p3)

执行了resolve或者reject后状态会发生改变,分别对应fulfilled和rejected,状态不可逆转,除了Pending状态其他的两个状态只要为其中一个后就不会再发生变更。

Promise中有throw相当于执行了reject

实现resolve与reject

初始状态为Pending,this指向执行它们的MyPromise实例,防止随着函数执行环境的改变而改变。

kotlin 复制代码
// 第一步定义Promise状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected'
​
// 第二步
class MyPromise {
    // 第三步定义基础属性
    PromiseState;
    PromiseResult;
    constructor(executor) {
        // 初始化状态
        this.initValue()
        // 第七步执行传进来的函数,在Promise中可以捕获抛出的异常
        try{
            // 有个前提,resolve和reject需要绑定执行它的那个Promise实例
            // 给resolve和reject绑定this
            executor(this.#resolve.bind(this),this.#reject.bind(this))
        }catch(error){
            // 如果执行器抛出异常,则调用reject方法,并传入异常
            this.#reject(error)
        }
​
    }
​
    // 第四步初始化Promise的状态
    initValue(){
        this.PromiseState = PENDING;
        this.PromiseResult = undefined;
    }
​
    // 第五步定义统一的状态变更函数
    #changeStatus(PromiseStatus, value){
        // Promise只有成功或失败,如果状态不是默认的Pending就表明已经变更过了,不能执行后续的代码
        if(this.PromiseState !== PENDING) return;
        this.PromiseState = PromiseStatus;
        this.PromiseResult = value;
    }
​
    // 第六五步定义resolve方法和reject方法
    #resolve(value){
        // 调用Promise状态变更函数
        this.#changeStatus(FULFILLED, value)
    }
​
    #reject(reason){
        // 调用Promise状态变更函数
        this.#changeStatus(REJECTED, reason)
    }
​
​
}

测试代码:状态变更

javascript 复制代码
const test1 = new MyPromise((resolve, reject) => {
    resolve('success')
})
console.log(test1) // MyPromise { PromiseState: 'fulfilled', PromiseResult: 'success' }
​
const test2 = new MyPromise((resolve, reject) => {
    reject('fail')
})
console.log(test2) // MyPromise { PromiseState: 'rejected', PromiseResult: 'fail' }

测试代码:状态不可变更

javascript 复制代码
const test1 = new MyPromise((resolve, reject) => {
    // 只以第一次为准
    resolve('success')
    reject('fail')
})
console.log(test1) // MyPromise { PromiseState: 'fulfilled', PromiseResult: 'success' }

测试代码:捕获Promise回调内的异常

javascript 复制代码
const test3 = new MyPromise((resolve, reject) => {
    throw('fail')
})
console.log(test3) // MyPromise { PromiseState: 'rejected', PromiseResult: 'fail' }

拆解then方法

javascript 复制代码
// 马上输出 "success"
const p1 = new Promise((resolve, reject) => {
    resolve('success')
}).then(res => console.log(res), err => console.log(err))
​
// 1秒后输出 "fail"
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject('fail')
    }, 1000)
}).then(res => console.log(res), err => console.log(err))
​
// 链式调用 输出 200
const p3 = new Promise((resolve, reject) => {
    resolve(100)
}).then(res => 2 * res, err => console.log(err))
  .then(res => console.log(res), err => console.log(err))

根据上述代码可以确定:

  1. then接收两个回调,一个是成功回调,一个是失败回调;
  2. 当Promise状态为fulfilled执行成功回调,为rejected执行失败回调;
  3. 如resolve或reject在定时器里,则定时器结束后再执行then;
  4. then支持链式调用,下一次then执行受上一次then返回值的影响;

如何实现?

  1. 结构和初始化

    首先,MyPromise的构造函数需要接收一个执行器函数,此执行器立即执行,并接收两个参数:resolvereject。我们需要定义三种状态(pendingfulfilledrejected),以及用于存储成功/失败回调的数组。

  2. then 方法和状态变更

    then 方法应返回一个新的MyPromise对象,以支持链式调用。在then方法中,我们需要检查MyPromise的当前状态,以决定立即执行回调还是将回调存储起来待状态改变后执行。

    对于定时器或异步操作,当resolvereject在这些操作内部调用时,then注册的回调应在操作完成后执行。这意味着我们需要在状态仍为pending时收集这些回调,并在resolvereject被调用时按顺序执行它们。

  3. 链式调用和值的传递

    为了支持链式调用,每次调用then时都应创建并返回一个新的MyPromise对象。这个新的MyPromise对象的解决或拒绝应基于前一个then回调的返回值。

    如果回调函数返回一个值,这个值应传递给链中下一个then的成功回调。如果回调函数抛出异常,则应将异常传递给链中下一个then的失败回调。如果回调函数返回一个新的MyPromise,则该Promise的结果应决定链中下一个then的调用。

实现then

  • #executeCallbacks执行缓存的promise
  • resolvePromise 处理不同的返回值类型
  • onFulfilledCallbacksonRejectedCallbacks 存储对应状态的执行任务
kotlin 复制代码
// 第一步定义Promise状态
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected'
​
// 第二步
class MyPromise {
    // 第三步定义基础属性
    PromiseState;
    PromiseResult;
    onFulfilledCallbacks = []; // 初始化成功回调的存储数组
    onRejectedCallbacks = []; // 初始化失败回调的存储数组
    constructor(executor) {
        // 初始化状态
        this.initValue()
        // 第七步执行传进来的函数,在Promise中可以捕获抛出的异常
        try {
            // 有个前提,resolve和reject需要绑定执行它的那个Promise实例
            // 给resolve和reject绑定this
            executor(this.#resolve.bind(this), this.#reject.bind(this))
        } catch (error) {
            // 如果执行器抛出异常,则调用reject方法,并传入异常
            this.#reject(error)
        }
​
    }
​
    // 第四步初始化Promise的状态
    initValue() {
        this.PromiseState = PENDING;
        this.PromiseResult = undefined;
    }
​
    // 第五步定义统一的状态变更函数
    #changeStatus(PromiseState, value) {
        // Promise只有成功或失败,如果状态不是默认的Pending就表明已经变更过了,不能执行后续的代码
        if (this.PromiseState !== PENDING) return;
        this.PromiseState = PromiseState;
        this.PromiseResult = value;
​
        // 每次状态变更后都要执行#executeCallbacks方法,根据当前状态执行对应的回调函数
        this.#executeCallbacks()
    }
​
    // 第六五步定义resolve方法和reject方法
    #resolve(value) {
        // 调用Promise状态变更函数
        this.#changeStatus(FULFILLED, value)
    }
​
    #reject(reason) {
        // 调用Promise状态变更函数
        this.#changeStatus(REJECTED, reason)
    }
​
    #executeCallbacks() {
        // 根据当前状态,执行对应的回调函数
        if (this.PromiseState === FULFILLED) {
            while (this.onFulfilledCallbacks.length) {
                // 因为数组本身就和队列的性质一样,通过shift方法可以取出数组中的第一个元素,然后执行里面缓存的回调函数,把当前状态传进去(这里执行的就是存入数组的resolvePromise辅助函数)
                this.onFulfilledCallbacks.shift()(this.PromiseResult)
            }
        } else if (this.PromiseState === REJECTED) {
            while (this.onRejectedCallbacks.length) {
                this.onRejectedCallbacks.shift()(this.PromiseResult)
            }
        }
    }
​
    // 第八步定义then方法接收两个回调 onFulfilled, onRejected
    then(onFulfilled, onRejected) {
        // 判断是否是函数如果不是包装下返回值为函数
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason };
        // 根据上面分析得知,then是支持链式调用,返回的一个包装后的promise对象并且传递给下一个then的成功回调,失败的给失败的
        const thenPromise = new MyPromise((resolve, reject) => {
            // 通过queueMicrotask来异步执行回调,以确保符合Promise规范的异步行为。这也解决了thenPromise变量作用域的问题,因为handleCallback是在thenPromise被定义之后才使用的。
            // 创建辅助函数resolvePromise
            const resolvePromise = (callback, resolve, reject) => {
                // 使用js提供的微任务环境,因为then本身就是微任务
                queueMicrotask(() => {
                    try {
                        // 立即执行传入的onFulfilled或onRejected方法,拿到结果存起来
                        const result = callback(this.PromiseResult)
                        // 判断结果是不是和当前返回的promise对象是同一个,如果是则抛出异常,因为循环引用了
                        if (result && result === thenPromise) {
                            throw new Error('循环引用');
                        }
                        // 判断当前结果是不是一个Promise对象,如果是则调用then方法,把结果传进去,把then返回的promise对象作为结果返回
                        if (result instanceof MyPromise) {
                            result.then(resolve, reject)
                        } else {
                            resolve(result);
                        }
                    } catch (error) {
                        // 拦截thenPromise内部的异常返回回去,然后继续往外抛出
                        reject(error)
                        throw new Error(error);
                    }
                })
            }
​
            // 根据状态处理不同状态的回调函数
            if (this.PromiseState === FULFILLED) {
                // 如果当前为成功状态,执行第一个回调
                resolvePromise(onFulfilled, resolve, reject)
            } else if (this.PromiseState === REJECTED) {
                // 如果当前为失败状态,执行第二个回调
                resolvePromise(onRejected, resolve, reject)
            } else if (this.PromiseState === PENDING) {
                // 如果当前为等待状态,把回调函数存起来,等状态变更后再执行
                this.onFulfilledCallbacks.push(() => resolvePromise(onFulfilled, resolve, reject))
                this.onRejectedCallbacks.push(()=>resolvePromise(onRejected, resolve, reject))
            }
        })
​
        return thenPromise;
    }
​
}

测试用例

javascript 复制代码
// 测试用例 1: 基本的resolve和链式调用
const promise1 = new MyPromise((resolve, reject) => {
    resolve(1);
});
promise1.then(value => {
    console.log(value); // 应打印 1
    return value + 1;
}).then(value => {
    console.log(value); // 应打印 2
});
​
// 测试用例 2: 使用setTimeout来模拟异步操作
const promise2 = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve(2);
    }, 1000);
});
promise2.then(value => {
    console.log(value); // 1秒后应打印 2
    return new MyPromise((resolve, reject) => {
        setTimeout(() => {
            resolve(value + 2);
        }, 1000);
    });
}).then(value => {
    console.log(value); // 2秒后应打印 4
});
​
// 测试用例 3: 错误处理
const promise3 = new MyPromise((resolve, reject) => {
    throw new Error('Test Error');
});
promise3.then(value => {
    console.log(value);
}, error => {
    console.error(error.message); // 应打印 "Test Error"
})
​

queueMicrotask

相关链接:queueMicrotask

GPT的解释

queueMicrotask是一个在现代浏览器和Node.js环境中内置的全局函数,用于将一个函数安排在所有正在执行的宏任务(例如setTimeout、setInterval、I/O操作等)和当前正在执行的微任务(例如Promise的回调)之后、但在下一个宏任务开始之前执行。它提供了一种方式来异步执行代码,而不会延迟到下一个宏任务,从而能够在当前任务和下一个事件循环之间快速地运行一个任务。

queueMicrotask的主要用途是安排微任务(microtask),这是执行异步操作的一种方式,比起宏任务来说,微任务具有更高的优先级。在Promise相关操作中使用queueMicrotask可以确保按照正确的顺序执行异步代码,尤其是在实现自定义Promise或处理与Promise相关的微任务队列时。

javascript 复制代码
console.log('Script start');

setTimeout(() => {
  console.log('setTimeout'); // 宏任务
}, 0);

queueMicrotask(() => {
  console.log('queueMicrotask'); // 微任务
});

Promise.resolve().then(() => {
  console.log('Promise.then'); // 微任务
});

console.log('Script end');

手写queryMicrotask

ini 复制代码
function runMicroTask(runc) {
    if (typeof process === 'object' && typeof process.nextTick === 'function') {
        // Node.js 环境
        process.nextTick(runc);
    } else if (typeof MutationObserver === 'function') {
        // 浏览器环境,使用 MutationObserver
        let counter = 1;
        const observer = new MutationObserver(() => {
            runc();
            observer.disconnect(); // 清理,避免重复调用和内存泄漏
        });
        const textNode = document.createTextNode(String(counter));
        observer.observe(textNode, {
            characterData: true
        });
        counter = (counter + 1) % 2; // 切换值以触发MutationObserver
        textNode.data = String(counter);
    } else {
        // 作为最后的回退,使用 setTimeout
        setTimeout(runc, 0);
    }
}
相关推荐
小小李程序员24 分钟前
css边框修饰
前端·css
我爱画页面1 小时前
使用dom-to-image截图html区域为一张图
前端·html
忧郁的西红柿1 小时前
HTML-DOM模型
前端·javascript·html
思茂信息1 小时前
CST电磁仿真77GHz汽车雷达保险杠
运维·javascript·人工智能·windows·5g·汽车
bin91531 小时前
【油猴脚本】00010 案例 Tampermonkey油猴脚本,动态渲染表格-添加提示信息框,HTML+Css+JavaScript编写
前端·javascript·css·bootstrap·html·jquery
Stanford_11061 小时前
C++入门基础知识79(实例)——实例 4【求商及余数】
开发语言·前端·javascript·c++·微信小程序·twitter·微信开放平台
Maer092 小时前
Cocos Creator3.x设置动态加载背景图并且循环移动
javascript·typescript
Good_Luck_Kevin20182 小时前
速通sass基础语法
前端·css·sass
大怪v2 小时前
前端恶趣味:我吸了juejin首页,好爽!
前端·javascript