摘要:在学习异步编程时,我终于搞懂了 JavaScript 的执行机制。JS 引擎是一行一行同步执行代码的,如果某段代码执行很慢,就会阻塞后面的代码。为了解决这个问题,JS 引入了异步机制,比如用 setTimeout 包裹耗时函数,让它延后执行,不影响主线程继续运
在学习异步编程时,我终于搞懂了 JavaScript 的执行机制。JS 引擎是 一行一行同步执行代码 的,如果某段代码执行很慢,就会 阻塞后面的代码 。为了解决这个问题,JS 引入了 异步机制 ,比如用 setTimeout 包裹耗时函数,让它延后执行,不影响主线程继续运行。
但这也带来新问题:异步的返回值无法立即获取,调用函数时只能拿到 undefined 。于是我们传入 回调函数 来“等待结果”,在异步任务完成后再手动调用回调,把结果传出来。然而一旦对返回值要进行多步操作,就会嵌套多个回调,形成“ 回调地狱 ”。
为了解决这个问题,Promise 诞生了。它可以 存储异步返回值 ,并通过 .then 获取结果,还支持链式调用,让异步流程变得清晰优雅。关键在于 #技术分享,.then 返回的也是一个新的 Promise 实例,我们可以继续 .then ,实现连续处理。
那为什么 Promise 就能“存值再取值”?因为 JS 引擎的执行机制分为 调用栈、微任务队列和宏任务队列 。同步代码直接进入调用栈执行,异步任务(如定时器)进宏队列,而 Promise.then 属于微队列,在当前同步代码执行完后优先执行 。这样一来,resolve 存值时在调用栈中完成,等 .then 所在的微任务开始执行时,值已经准备好 ,自然就能拿到了。
总结一下,异步的本质是“排队”,而 Promise 则是用微任务的优先机制,把“取值”安排在“存值”之后,完美避开了时间错位的问题 。理解这一点后,手写一个 Promise 实现也就顺理成章了。
resolve 是在什么时候被调用的?被包裹在异步操作中(如 setTimeout)时,是在对应 宏任务执行的时候 调用。
resolve 属于宏任务还是微任务?不是任务队列本身的任务 ,它是宏任务中 执行的一段同步代码。
*resolve 是怎么“存”值的?改变内部状态为 fulfilled,并将值保存起来,然后将 .then 回调 *推入微任务队列。
*为什么 .then 能保证在 resolve 之后?因为 .then 的回调是放入 *微任务队列 ,只有 resolve 被执行,状态变了,才会触发微任务执行。
在开始写代码之前,我先来口述一下自己理解的手写 Promise 的思路。
首先我们知道,Promise 本质上是一个 类 。平时我们这样使用它:
const p = new Promise((resolve, reject) => {resolve('hah') })p.then(r => console.log(r))所以我们第一步是创建一个类:
class MyPromise {}这个类的构造函数接受一个 函数类型的参数 ,并且会立即执行这个函数。我们通过参数形式把 resolve 和 reject 这两个方法传给用户,用户调用这两个函数就能 存储数据 。
既然要存值,我们就需要在实例上定义一个属性,比如 this.PromiseResult ,用来存储用户传进来的数据。
接下来是 .then 的实现。我们希望 then 方法能在用户调用 resolve 存值之后,把这个值“拿出来”。那就需要我们在 .then 里注册一个回调函数,等数据准备好了再执行。
到这儿,我们就完成了最简单的 Promise 架构。
--- 但还不够。
我们发现:resolve 应该 只能调用一次 。所以要添加一个 status 状态值,比如初始是 'pending' ,只有在状态是 'pending' 时才允许存储数据。状态一旦变成 'fulfilled' 或 'rejected' ,就不能再修改。
然后我们进一步发现:如果 resolve 是异步的,比如在 setTimeout 里调用的,那 .then 方法执行时,数据还没准备好,这时候就拿不到值。
怎么办?
我们就把 .then 注册时传进来的回调函数,暂时 缓存起来 ,等 resolve 执行的时候再调用它。最简单的做法是设置一个属性,比如 this.callback = fn ,在 resolve 时调用 this.callback(this.PromiseResult) 。
--- 后来我们又发现,这样只能支持 单次调用.then 。我们希望支持链式 .then ,那就需要把回调函数缓存成一个 数组 ,每次调用 .then 就 push 进去,等 resolve 的时候统一 forEach 执行。
至此,我们实现了“多个 then 调用”的功能。
--- 但问题还没完,我们还希望支持 链式调用 ,也就是说每次 .then 都返回一个新的 Promise 实例,允许我们继续调用 .then :
那该怎么做呢?
我们只需要让 .then 方法的返回值是 new MyPromise(...) ,并在它的回调函数内部调用 resolve(...) ,把前一个 .then 处理完的值传进去。就这样,通过包裹和传递,我们就实现了链式的值传递和异步流程控制。
这就是我自己推导出的 Promise 实现思路。下面,我们就用代码一步一步实现它。
const PROMISE_STATE ={ PENDING:0, FULFILLED:1, REJECTED:2}class MyPromise{ #PromiseResult #PromiseState =0 #cbs = constructor(cb){ cb(this.#resolve.bind(this),this.#reject) } #resolve(value){ if(this.#PromiseState !== PROMISE_STATE.PENDING) return this.#PromiseResult = value this.#PromiseState=1 queueMicrotask(=>{ this.#cbs.forEach(cb=>cb) }) } #reject{} then(cb1,cb2){ return new MyPromise((resolve,reject)=>{ if(this.#PromiseState===PROMISE_STATE.PENDING){ this.#cbs.push(=>{ resolve( cb1(this.#PromiseResult)) }) }else if(this.#PromiseState === PROMISE_STATE.PENDING){ queueMicrotask(=>{ resolve(cb1(this.#PromiseResult)) }) } }) }}const p = new MyPromise((resolve,reject)=>{ setTimeout(=>{ resolve('数据') }) }) p.then(r=>{ console.log('数据1',r) return '猪八戒' }).then(r=>console.log(r)) p.then(r=>console.log('数据2',r))编辑
手写 Promise 的本质,其实就是解决“回调地狱”问题,或者说:希望异步代码也能像同步一样,把值存下来,供后续逻辑自由处理。
实现思路并不复杂——一个类、几个属性、一些回调函数——关键在于抽丝剥茧、不断思考、发现问题、解决问题。这不仅锻炼了代码能力,也打磨了我们对 JS 异步机制的理解。
---
来源:墨码行者