资讯专栏INFORMATION COLUMN

从零开始写一个 Promise 库

Binguner / 928人阅读

摘要:是什么在规范中,是一个类,它的构造函数接受一个函数。在这种情况下,是但处于状态。与一起使用关键字会暂停执行一个函数,直到等待的变成状态。此外,会一直等待调用直到下一个时序。

原文:Write Your Own Node.js Promise Library from Scratch

作者:code_barbarian

Promise 已经是 JavaScript 中异步处理的基石,回调的场景将会越来越少,而且现在可以直接在 Node.js 使用 async/await。async/await 基于 Promise,因此需要了解 Promise 来掌握 async/await。这篇文章,将介绍如何编写一个 Promise 库,并演示如何使用 async/await。

Promise 是什么?

在 ES6 规范中,Promise 是一个类,它的构造函数接受一个 executor 函数。Promise 类的实例有一个 then() 方法。根据规范,Promise 还有其他的一些属性,但在这里可以暂时忽略,因为我们要实现的是一个精简版的库。下面是一个 MyPromise 类的脚手架:

class MyPromise {
    // `executor` 函数接受两个参数,`resolve()` 和 `reject()`
    // 负责在异步操作成功(resolved)或者失败(rejected)的时候调用 `resolve()` 或者 `reject()`
    constructor(executor) {}

    // 当 promise 的状态是 fulfilled(完成)时调用 `onFulfilled` 方法,
    // 当 promise 的状态是 rejected(失败)时调用 `onRejected` 方法
    // 到目前为止,可以认为 "fulfilled" 和 "resolved" 是一样的
    then(onFulfilled, onRejected) {}
}

executor 函数需要两个参数,resolve()reject()。promise 是一个状态机,包含三个状态:

pending:初始状态,既不是成功,也不是失败状态

fulfilled:意味着操作成功完成,返回结果值

rejected:意味着操作失败,返回错误信息

这样很容易就能实现 MyPromise 构造函数的初始版本:

constructor(executor) {
    if (typeof executor !== "function") {
        throw new Error("Executor must be a function")
    }

    // 初始状态,$state 表示 promise 的当前状态
    // $chained 是当 promise 处在 settled 状态时需要调用的函数数组
    this.$state = "PENDING"
    this.$chained = []

    // 为处理器函数实现 `resolve()` 和 `reject()`
    const resolve = res => {
        // 只要当 `resolve()` 或 `reject()` 被调用
        // 这个 promise 对象就不再处于 pending 状态,被称为 settled 状态
        // 调用 `resolve()` 或 `reject()` 两次,以及在 `resolve()` 之后调用 `reject()` 是无效的
        if (this.$state !== "PENDING") {
            return
        }
        // 后面将会谈到 fulfilled 和 resolved 之间存在细微差别
        this.$state = "FULFILLED"
        this.$internalValue = res
        // If somebody called `.then()` while this promise was pending, need
        // to call their `onFulfilled()` function
        for (const { onFulfilled } of this.$chained) {
            onFulfilled(res)
        }
    }
    const reject = err => {
        if (this.$state !== "PENDING") {
            return
        }
        this.$state = "REJECTED"
        this.$internalValue = err
        for (const { onRejected } of this.$chained) {
            onRejected(err)
        }
    }

    // 如规范所言,调用处理器函数中的 `resolve()` 和 `reject()`
    try {
        // 如果处理器函数抛出一个同步错误,我们认为这是一个失败状态
        // 需要注意的是,`resolve()` 和 `reject()` 只能被调用一次
        executor(resolve, reject)
    } catch (err) {
        reject(err)
    }
}

then() 函数的实现更简单,它接受两个参数,onFulfilled()onRejected()then() 函数必须确保 promise 在 fulfilled 时调用 onFulfilled(),在 rejected 时调用 onRejected()。如果 promise 已经 resolved 或 rejected,then() 函数会立即调用 onFulfilled()onRejected()。如果 promise 仍处于 pending 状态,就将函数推入 $chained 数组,因此后续 resolve()reject() 函数仍然可以调用它们。

then(onFulfilled, onRejected) {
    if (this.$state === "FULFILLED") {
        onFulfilled(this.$internalValue)
    } else if (this.$state === "REJECTED") {
        onRejected(this.$internalValue)
    } else {
        this.$chained.push({ onFulfilled, onRejected })
    }
}

*除此之外:ES6 规范表示,如果在已经 resolved 或 rejected 的 promise 调用 .then(), 那么 onFulfilled()onRejected() 将在下一个时序被调用。由于本文代码只是一个教学示例而不是规范的精确实现,因此实现会忽略这些细节。

Promise 调用链

上面的例子特意忽略了 promise 中最复杂也是最有用的部分:链式调用。如果 onFulfilled() 或者 onRejected() 函数返回一个 promise,则 then() 应该返回一个 “locked in” 的新 promise 以匹配这个 promise 的状态。例如:

p = new MyPromise(resolve => {
    setTimeout(() => resolve("World"), 100)
})

p
    .then(res => new MyPromise(resolve => resolve(`Hello, ${res}`)))
    // 在 100 ms 后打印 "Hello, World"
    .then(res => console.log(res))

下面是可以返回 promise 的 .then() 函数实现,这样就可以进行链式调用。

then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
        // 确保在 `onFulfilled()` 和 `onRejected()` 的错误将导致返回的 promise 失败(reject)
        const _onFulfilled = res => {
            try {
                // 如果 `onFulfilled()` 返回一个 promise, 确保 `resolve()` 能正确处理
                resolve(onFulfilled(res))
            } catch (err) {
                reject(err)
            }
        }
        const _onRejected = err => {
            try {
                reject(onRejected(err))
            } catch (_err) {
                reject(_err)
            }
        }
        if (this.$state === "FULFILLED") {
            _onFulfilled(this.$internalValue)
        } else if (this.$state === "REJECTED") {
            _onRejected(this.$internalValue)
        } else {
            this.$chained.push({ onFulfilled: _onFulfilled, onRejected: _onRejected })
        }
    })
}

现在 then() 返回一个 promise,但是还需要完成一些工作:如果 onFulfilled() 返回一个 promise,resolve() 要能够正确处理。所以 resolve() 函数需要在 then() 递归调用,下面是更新后的 resolve() 函数:

const resolve = res => {
    // 只要当 `resolve()` 或 `reject()` 被调用
    // 这个 promise 对象就不再处于 pending 状态,被称为 settled 状态
    // 调用 `resolve()` 或 `reject()` 两次,以及在 `resolve()` 之后调用 `reject()` 是无效的
    if (this.$state !== "PENDING") {
        return
    }

    // 如果 `res` 是 thenable(带有then方法的对象)
    // 将锁定 promise 来保持跟 thenable 的状态一致
    if (res !== null && typeof res.then === "function") {
        // 在这种情况下,这个 promise 是 resolved,但是仍处于 "PENDING" 状态
        // 这就是 ES6 规范中说的"一个 resolved 的 promise",可能处在 pending, fulfilled 或者 rejected 状态
        // http://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects
        return res.then(resolve, reject)
    }

    this.$state = "FULFILLED"
    this.$internalValue = res
    // If somebody called `.then()` while this promise was pending, need
    // to call their `onFulfilled()` function
    for (const { onFulfilled } of this.$chained) {
        onFulfilled(res)
    }

    return res
}

为了简单起见,上面的例子省略了一旦 promise 被锁定用以匹配另一个 promise 时,调用 resolve() 或者 reject() 是无效的关键细节。在上面的例子中,你可以 resolve() 一个 pending 的 promise ,然后抛出一个错误,然后 res.then(resolve, reject) 将会无效。这仅仅是一个例子,而不是 ES6 promise 规范的完全实现。

上面的代码说明了 resolved 的 promise 和 fulfilled 的 promise 之间的区别。这种区别是微妙的,并且与 promise 链式调用有关。resolved 不是一种真正的 promise 状态,但它是ES6规范中定义的术语。当对一个已经 resolved 的 promise 调用 resolve(),可能会发生以下两件事之一:

在调用 resolve(v)时,如果 v 不是一个 promise ,那么 promise 立即成为 fulfilled。在这种简单的情况下,resolved 和 fulfilled 就是一样的。

在调用 resolve(v)时,如果 v 是另一个 promise,那么这个 promise 一直处于 pending 直到 v 调用 resolve 或者 reject。在这种情况下, promise 是 resolved 但处于 pending 状态。

与 Async/Await 一起使用

关键字 await 会暂停执行一个 async 函数,直到等待的 promise 变成 settled 状态。现在我们已经有了一个简单的自制 promise 库,看看结合使用 async/await 中时会发生什么。向 then() 函数添加一个 console.log() 语句:

then(onFulfilled, onRejected) {
    console.log("Then", onFulfilled, onRejected, new Error().stack)
    return new MyPromise((resolve, reject) => {
        /* ... */
    })
}

现在,我们来 await 一个 MyPromise 的实例,看看会发生什么。

run().catch(error => console.error(error.stack))

async function run() {
    const start = Date.now()
    await new MyPromise(resolve => setTimeout(() => resolve(), 100))
    console.log("Elapsed time", Date.now() - start)
}

注意上面的 .catch() 调用。catch() 函数是 ES6 promise 规范的核心部分。本文不会详细讲述它,因为 .catch(f) 相当于 .then(null, f),没有什么特别的内容。

以下是输出内容,注意 await 隐式调用 .then() 中的 onFulfilled()onRejected() 函数,这是 V8 底层的 C++ 代码(native code)。此外,await 会一直等待调用 .then() 直到下一个时序。

Then function () { [native code] } function () { [native code] } Error
    at MyPromise.then (/home/val/test/promise.js:63:50)
    at process._tickCallback (internal/process/next_tick.js:188:7)
    at Function.Module.runMain (module.js:686:11)
    at startup (bootstrap_node.js:187:16)
    at bootstrap_node.js:608:3
Elapsed time 102
更多

async/await 是非常强大的特性,但掌握起来稍微有点困难,因为需要使用者了解 promise 的基本原则。 promise 有很多细节,例如捕获处理器函数中的同步错误,以及 promise 一旦解决就无法改变状态,这使得 async/await 成为可能。一旦对 promise 有了充分的理解,async/await 就会变得容易得多。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94257.html

相关文章

  • 从零开始实现一个自己的Promise

    摘要:所以,这篇文章我会带大家从零开始,手写一个基本能用的。首先,规定对象是一个构造函数,用来生成实例。然后,这个构造函数接受一个函数作为参数,该函数的两个参数分别是和。对象通过自身的状态,来控制异步操作。 刚开始写前端的时候,处理异步请求经常用callback,简单又顺手。后来写着写着就抛弃了callback,开始用promise来处理异步问题。promise写起来确实更加优美,但由于缺乏...

    paulquei 评论0 收藏0
  • 前端之从零开始系列

    摘要:只有动手,你才能真的理解作者的构思的巧妙只有动手,你才能真正掌握一门技术持续更新中项目地址求求求源码系列跟一起学如何写函数库中高级前端面试手写代码无敌秘籍如何用不到行代码写一款属于自己的类库原理讲解实现一个对象遵循规范实战手摸手,带你用撸 Do it yourself!!! 只有动手,你才能真的理解作者的构思的巧妙 只有动手,你才能真正掌握一门技术 持续更新中…… 项目地址 https...

    Youngdze 评论0 收藏0
  • JavaScript 异步

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。写一个符合规范并可配合使用的写一个符合规范并可配合使用的理解的工作原理采用回调函数来处理异步编程。 JavaScript怎么使用循环代替(异步)递归 问题描述 在开发过程中,遇到一个需求:在系统初始化时通过http获取一个第三方服务器端的列表,第三方服务器提供了一个接口,可通过...

    tuniutech 评论0 收藏0
  • 从零开始搭建React同构应用(二):浏览器端工程化

    摘要:从零开始搭建同构应用二项目工程化浏览器端在从零开始同构开发一中我们已经能实现基本的配置和编译了。接下来我们需要将编译工作工程化。配置作用自动生成自动在引入,。文件内容如下同构开发配置自动刷新这里我们用到的修饰器特性。 从零开始搭建React同构应用(二) 项目工程化(浏览器端) 在从零开始React同构开发(一)中我们已经能实现基本的React配置和编译了。接下来我们需要将编译工作工程...

    wwq0327 评论0 收藏0
  • javascript知识点

    摘要:模块化是随着前端技术的发展,前端代码爆炸式增长后,工程化所采取的必然措施。目前模块化的思想分为和。特别指出,事件不等同于异步,回调也不等同于异步。将会讨论安全的类型检测惰性载入函数冻结对象定时器等话题。 Vue.js 前后端同构方案之准备篇——代码优化 目前 Vue.js 的火爆不亚于当初的 React,本人对写代码有洁癖,代码也是艺术。此篇是准备篇,工欲善其事,必先利其器。我们先在代...

    Karrdy 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<