小而美的Promise库——promiz源码浅析

背景

在上一篇博客[译]前端基础知识储备——Promise/A+规范中,我们介绍了Promise/A+规范的具体条目。在本文中,我们来选择了promiz,让大家来看下一个具体的Promise库的内部代码是如何运作的。

promiz是一个体积很小的promise库(官方介绍约为913 bytes (gzip)),作为一个ES2015标准中的Promise的polyfill,实现了诸如resolveallrace等API。

知识储备

我们在这里简单回顾一下Promise/A+的主要关键点,如果需要了解详细内容的同学,可以阅读我的上一篇博客。

  • Promise有三个状态,分别为pendingfulfilledrejected,且只能从pendingfulfilled或者rejected,没有其他的流转方式。
  • Promise的返回值是一个新的Promise,原因见上一条。
  • 传递给then函数的两个回调函数,有且仅有一次机会被执行(即执行了onfulfilled就不会执行onrejected函数,且只执行一次)。

代码实现与分析

异步执行器

在介绍Promise之前,我们先介绍一下异步执行器。在Promise中,我们需要一个异步的执行器来异步执行我们的回调函数。在规范中提到,通常情况下,我们可以使用微任务(nextTick)或者宏任务(setTimeout)来实现。但是,如果我们需要兼容Web Worker这种情况的话,我们可能还需要一些更多的方式来处理。具体代码如下:

var queueId = 1
var queue = {}
var isRunningTask = false

// 使用postMessage来执行异步函数
if (!global.setImmediate)
    global.addEventListener('message', function (e) {
        if (e.source == global) {
            if (isRunningTask)
                nextTick(queue[e.data])
            else {
                isRunningTask = true
                try {
                    queue[e.data]()
                } catch (e) {}

                delete queue[e.data]
                isRunningTask = false
            }
        }
    })

/**
 * 异步执行方法
 * @param {function} fn 需要执行的回调函数
 */
function nextTick(fn) {
    if (global.setImmediate) setImmediate(fn)
    // 如果在Web Worker中使用以下方法
    else if (global.importScripts) setTimeout(fn)
    else {
        queueId++
        queue[queueId] = fn
        global.postMessage(queueId, '*')
    }
}

以上代码比较简单,我们简单说明下:

  • 在代码中,promiz使用了setImmediatesetTimeoutpostMessage这三个方法来执行异步函数,其中:
    • setImmedeate,只有IE实现了该方法,在执行完队列中的代码后立即执行。
    • PostMessage,新增的H5中的方法。
    • setTimeout,兼容性最佳,可以适用各种场景。

因此,在promiz的这段代码中,有一定的兼容性问题,应该把setTimeout放到最后作为一个兜底策略,否则无法在老浏览器中执行。

构造函数

说完了异步函数执行器,我们来看下promise的构造函数。

首先我们来看下内存数据,我们需要存储当前promise的状态、成功的值或者失败的原因、下一个promise的引用和成功与失败的回调函数。因此,我们需要以下变量:

// states
// 0: pending
// 1: resolving
// 2: rejecting
// 3: resolved
// 4: rejected
var self = this,
    state = 0, // promise状态
    val = 0, // success callback返回值
    next = [], // 返回的新的promise对象
    fn, er; // then方法中的成功回调函数和失败回调函数

在存储完相关数据后,我们来看下构造函数。

function Deferred(resolver) {
	...
    self = this;
    try {
        if (typeof resolver == 'function')
            resolver(self['resolve'], self['reject'])
    } catch (e) {
        self['reject'](e)
    }
}

构造函数非常简单,除了声明相关的函数,就只有执行传入的callback而已。当然,如果我们不是链式调用的第一个promise,那么我们会没有resolver参数,因此不需要在此执行,我们会在then函数执行resolve方法。

下面我们来看下上面提到的处理函数resovlereject

self['resolve'] = function (v) {
    fn = self.fn
    er = self.er
    if (!state) {
        val = v
        state = 1

        nextTick(fire)
    }
    return self
}

self['reject'] = function (v) {
    fn = self.fn
    er = self.er
    if (!state) {
        val = v
        state = 2

        nextTick(fire)

    }
    return self
}

self['then'] = function (_fn, _er) {
    if (!(this._d == 1))
        throw TypeError()

    var d = new Deferred()

    d.fn = _fn
    d.er = _er
    if (state == 3) {
        d.resolve(val)
    }
    else if (state == 4) {
        d.reject(val)
    }
    else {
        next.push(d)
    }

    return d
}

resolvereject这两个函数中,都是改变了内部promise的状态,给定了参数值,同时异步触发了fire函数。而then方法,则是生成了一个新的Deferred对象,并且完成了相关的初始化(执行完then方法我们就会得到这个新生成的Deferred对象,也就是一个新的Promise);当前一个promise到达resolved状态时,不需要等待则直接出发resolve方法,rejected状态时也一样。那么,让我们来看下fire方法到底是做什么的呢?

function fire() {

    // 检测是不是一个thenable对象
    var ref;
    try {
        ref = val && val.then
    } catch (e) {
        val = e
        state = 2
        return fire()
    }

    thennable(ref, function () {
        state = 1
        fire()
    }, function () {
        state = 2
        fire()
    }, function () {
        try {
            if (state == 1 && typeof fn == 'function') {
                val = fn(val)
            }

            else if (state == 2 && typeof er == 'function') {
                val = er(val)
                state = 1
            }
        } catch (e) {
            val = e
            return finish()
        }

        if (val == self) {
            val = TypeError()
            finish()
        } else thennable(ref, function () {
            finish(3)
        }, finish, function () {
            finish(state == 1 && 3)
        })

    })
}

从上面的代码来看,fire函数只是判断了ref是不是一个thenable对象,然后调用了thenable函数,传递了3个回调函数。那么这些回调函数到底是做什么用的呢?我们需要来看下thenable函数的实现代码。

// ref:指向thenable对象的`then`函数
// cb, ec, cn : successCallback, failureCallback, notThennableCallback
function thennable(ref, cb, ec, cn) {
    // Promises can be rejected with other promises, which should pass through
    if (state == 2) {
        return cn()
    }
    if ((typeof val == 'object' || typeof val == 'function') && typeof ref == 'function') {
        try {

            // cnt变量用来保证成功和失败的回调函数总共只会被执行一次
            var cnt = 0
            ref.call(val, function (v) {
                if (cnt++) return
                val = v
                cb()
            }, function (v) {
                if (cnt++) return
                val = v
                ec()
            })
        } catch (e) {
            val = e
            ec()
        }
    } else {
        cn()
    }
};

在thenable函数中,如果判断当前的promise的状态是处于rejecting时,会直接执行cn,也就是将reject状态传递下去。而如果当ref不是一个thenable对象的then函数时(那么此时值为undefined),那么就会直接执行cn

通过fire函数传递的三个callback我们可以看到,cn是在promise的状态改变时,针对特定的状态来触发相对应的onfulfilled或者onrejected回调函数。

只有当ref是一个thenable时(传递给resolve的是一个promise),代码才会进入上面的try catch逻辑中。

Promise执行流程

看完了上面的各部分代码,我相信大家可能对整个执行流程仍然不够熟悉,下面,我们将这些流程拼接起来,通过几个完整的流程来说明下。

链式调用第一个Promise

当我们声明一个promise式,我们会传入一个resolver。此时,整个Deferred对象的state是0。如果我们在resolver里面调用了resolve方法,那么我们的state就会变成1,然后出发fire函数注册到thenable函数里面的第三个回调函数,从而将值传递给下一个thenable。当thenable的then函数执行完成(即我们看到的Promise后面跟着的then函数执行完成以后),我们的state才会变成3,也就是说上一个Promise才会结束,返回一个新的Promise。

链式调用非第一个Promise

如果不是第一个Promise,那么我们就没有resolver参数。因此,我们的resolve方法并不是通过在resolver中进行调用的,而是将回调函数fn注册进来,在上一个Promise完成后主动调用执行的。也就是说,我们在上一个Promise执行完then函数并且返回一个新的Promise时,我们这个返回的Promise就已经进入了resolving的状态。

resolve传递一个Promise

在Promise/A+规范中,如果我们给resolve传递一个promise,那么我们的通过resolve获取到的值就是传递进去的这个promise返回的值。当然,我们也必须等待作为参数的这个promise处理完成后,才会处理外面的这个promise。

在promiz的代码中,我们如果通过resolve接收到一个promise,那么我们在fire函数中就会吧promise.then的引用传递给thenable函数。在thenable函数中,我们会将我们当前promise需要执行的onfulfilledonrejected封装成一个函数,传递给作为参数的promise的then函数。因此,当作为参数的promise执行任意结果的回调函数时,就会将参数传递给外层的promise,执行对应的回调函数。

全局执行方法

Promise.all

让我们先看代码。

Deferred.all = function (arr) {
    if (!(this._d == 1))
        throw TypeError()

    if (!(arr instanceof Array))
        return Deferred.reject(TypeError())

    var d = new Deferred()

    function done(e, v) {
        if (v)
            return d.resolve(v)

        if (e)
            return d.reject(e)

        var unresolved = arr.reduce(function (cnt, v) {
            if (v && v.then)
                return cnt + 1
            return cnt
        }, 0)

        if (unresolved == 0)
            d.resolve(arr)

        arr.map(function (v, i) {
            if (v && v.then)
                v.then(function (r) {
                    arr[i] = r
                    done()
                    return r
                }, done)
        })
    }

    done()

    return d
}

Promise.all中,我们使用了一个计数器来进行统计,在每一个Promise后面都增加一个then函数用于增加计数。当Promise成功时则计数+1。当整个数组中的Promise都已经进入resolved状态时,我们才会执行thenable的then函数。如果有一个失败的话,则立即进入reject流程。

总结

从代码设计层面来看,promiz的代码量较少,阅读也较为简单。但是,在某些细节的设计上,promiz还是体现出了较为巧妙的思路,如在处理作为入参的promise时,能够在这个promise后面动态的添加一个then函数,从而获取数据给外面的promise。

如果大家有兴趣,建议自己根据本文的说明阅读一遍源码,配合Promise/A+规范来看下是如何实现每一条规范的。

下一篇博客,我们将为大家从头开始,来实现一个Promise库。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏开源优测

Selenium3源码之异常模块篇

Selenium3源码之异常模块篇 简介 本文分析Selenium3源码中关于异常处理的通用代码,要分析的代码位置如下(路径为:***python3/lib/s...

3886
来自专栏偏前端工程师的驿站

Velocity魔法堂系列二:VTL语法详解

一、前言                               Velocity作为历史悠久的模板引擎不单单可以替代JSP作为Java Web的服务端网页...

2185
来自专栏xiaoxi666的专栏

c++ 继承类强制转换时的虚函数表工作原理

本文通过简单例子说明子类之间发生强制转换时虚函数如何调用,旨在对c++继承中的虚函数表的作用机制有更深入的理解。

1643
来自专栏公众号文章

Golang 入门系列(二)Go语言基础语法及需要注意的坑

上一章节我们已经了解了 Go 环境的配置,不了解的,请查看前面的文章 https://www.cnblogs.com/zhangweizhong/p/94599...

620
来自专栏思考的代码世界

Python基础学习06天

1484
来自专栏Java编程技术

并发队列-无界非阻塞队列ConcurrentLinkedQueue原理探究

常用的并发队列有阻塞队列和非阻塞队列,前者使用锁实现,后者则使用CAS非阻塞算法实现,使用非阻塞队列一般性能比较好,下面就看看常用的非阻塞ConcurrentL...

1251
来自专栏雨尘分享

4.Block的类型

1625
来自专栏菜鸟计划

angularjs promise详解

一、什么是Promise Promise是对象,代表了一个函数最终可能的返回值或抛出的异常,就是用来异步处理值的。 Promise是一个构造函数,自己身上有al...

2995
来自专栏无所事事者爱嘲笑

React事件绑定几种方法测试

1993
来自专栏程序员互动联盟

【编程基础】C++ Primer快速入门三:两种控制语句

语句总是顺序执行的:第一条语句执行完了接着是第二条,第三条等等。这是最简单的情况,为了更好的控制语句的运行,程序设计语言提供了多种控制结构支持更为复杂的语句执行...

3399

扫码关注云+社区

领取腾讯云代金券