Promise核心原理解析

作者: HerryLo

本文永久有效链接: https://github.com/AttemptWeb......

Promises对象被用于表示一个异步操作的最终完成 (或失败), 及其结果值。主要是为了解决异步操作的问题。

#Promise对象的状态

一个 Promise对象有以下三种状态:

pending: 初始状态,既不是成功,也不是失败状态。
fulfilled(resolved): 意味着操作成功完成。
rejected: 意味着操作失败。

Promise对象内部运行的一个变化, 变化如下:

1. 当new Promise()被实例化后,即表示Promise 进入pending初始化状态,准备就绪,等待运行。
2. 一旦Promise实例运行成功或者失败之后,实例状态就会变为fulfilled 或者 rejected,此时状态就无法变更。

#Promise函数使用

任何系统或函数都可以简化为输入输出系统,数据输入 ——> 黑箱 ——> 输出,如下图:

我们可以拿上图来类比Promise函数,代码如下:

// 实例化 Promise
new Promise((resolve, reject)=> {
    // 输入
    AjaxRequest.post({
        url: ‘url‘,
        data: {},
        sueccess: ()=> {
            // resolve
            resolve(res)
        },
        fail: (err)=> {
            // reject
            reject(err)
        }
    })
}).then((res)=> {
    // res 输出
    // ...操作
}).catch((err)=> {
    // err 输出
    // ...操作
})

在上面的代码中,Promise函数参数可以作为输入信息,而后经过Promise的内部处理(黑箱),在then函数或者catch函数参数中输出信息,这是一个完整的系统(别被它分散了注意力,这个解释的目的:让你更加关注Promise函数内部实现)。下面我们将解析Promise中黑箱操作。

#pending状态下会运行的函数

Promise函数实例化,会先进入到pending状态,在这个状态下,它会运行如下函数:

  1. 实例化Promise构造函数
  2. then方法注册回调函数
  3. catch方法注册回调函数
  4. 调用doResolve函数执行fn

#实例化Promise构造函数

你可以直接查看源码:Promise函数:54行,对照阅读,同时,在下面的代码中我会做不必要的省略。

// 首先运行,Promise构造函数
function Promise(fn) {
    // ...省略检验

    // _deferreds的类型,1是 single,2是 array
    this._deferredState = 0;
    // 0 - pending
    // 1 - fulfilled(resolved)
    // 2 - rejected
    // 3 - 另一个Promise的状态
    this._state = 0;
    // promise 执行结果
    this._value = null;
    // then注册回调数组
    this._deferreds = null;
    // fn等于noop 即return
    if (fn === noop) return;
    // 接受Promise回调函数 和 this 作为参数
    doResolve(fn, this);
}

Promise构造函数,会初始化属性,其中参数fn就是我们传入的函数。其中doResolve函数接受Promise函数参数 和 this作为参数,this指向它自己,负责执行fn函数。等下面的then函数和catch函数的回调函数注册完之后,doResolve函数将立即执行。

#then方法注册回调函数

可以查看代码,查看源码:then函数:72行。then方法的回调函数会被存储在this._deferreds中。仔细阅读代码中的备注

Promise.prototype.then = function(onFulfilled, onRejected) {
    if (this.constructor !== Promise) {
        // safeThen函数也是通过调用handle函数,return 新的Promise对象
        return safeThen(this, onFulfilled, onRejected);
    }
    // 生成新的Promise对象
    var res = new Promise(noop);
    handle(this, new Handler(onFulfilled, onRejected, res));
    return res;
};

// Handler构造函数
// 它的作用是挂载 then中的回调函数 和 一个空的Promise对象
function Handler(onFulfilled, onRejected, promise){
    // then中的Fulfilled回调函数
    this.onFulfilled = typeof onFulfilled === ‘function‘ ? onFulfilled : null;
    // then中的Rejected回调函数
    this.onRejected = typeof onRejected === ‘function‘ ? onRejected : null;
    // 保存新的Promise
    this.promise = promise;
}
// 保存then注册回调函数,更新回调函数状态
function handle(self, deferred) {
    // 。。。省略

    // pedding 状态
    if (self._state === 0) {
        // deferred == new Handler(onFulfilled, onRejected, res)
        if (self._deferredState === 0) {
            self._deferredState = 1;
            // 存储then回调deferred对象
            self._deferreds = deferred;
            return;
        }
        if (self._deferredState === 1) {
            self._deferredState = 2;
            // 存储then回调deferred对象
            self._deferreds = [self._deferreds, deferred];
            return;
        }
        // 存储then回调函数对象
        self._deferreds.push(deferred);
        return;
    }

    // 只有当进入到非pedding状态,handleResolved才会运行
    handleResolved(self, deferred);
}

Handler函数生成一个deffer对象,用于保存then函数中的onFulfilled和onRejected回调,以及返回的新的promise实例

then方法中的核心函数就是handle函数,它负责接收thisnew Handler对象。若在pedding状态下,handle函数只负责注册回调函数,更新回调函数状态。在非pedding状态下,则会执行handleResolved函数。

#catch方法注册回调函数

查看源码:catch函数:105行

Promise.prototype[‘catch‘] = function (onRejected) {
  return this.then(null, onRejected);
};

catch方法的回调函数实际是通过then方法来完成保存的。

#调用doResolve函数执行fn

负责运行Promise实例对象中的回调函数参数fn。

// 调用doResolve函数
function doResolve(fn, promise) {
    var done = false;

    // tryCallTwo函数执行 类似于
    // (resolve, reject) => {if(err){reject(err);return};resolve(res)}执行;
    var res = tryCallTwo(fn, function (value) {
        if (done) return;
        done = true;
        resolve(promise, value);
    }, function (reason) {
        if (done) return;
        done = true;
        reject(promise, reason);
    });

    // fn函数调用失败,手动运行reject函数
    if (!done && res === IS_ERROR) {
        done = true;
        reject(promise, LAST_ERROR);
    }
}

doResolve是同步直接调用传入的函数。其中tryCallTwo函数作用是调用函数fn,它接受三个参数。先执行fn函数,根据结果,再执行resolve函数或reject函数。在resolve函数或reject函数被调用之前,Promise对象的状态依然是pending

pending状态下函数调用基本流程如下:

#进入resolve或reject状态时会运行的函数

当初始化完之后,fn函数执行完成,接下来就会运行resolve函数或者reject函数。

  1. 调用resolve函数
  2. 调用finale函数
  3. 调用handleResolved函数

#调用resolve函数

若Promise对象的fn函数执行正常,之后就会调用resolve函数。可以查看源码:resolve函数:131行

function resolve(self, newValue) {
    // 。。。省略

    // newValue存在 & (newValue是一个对象 || newValue是一个函数)
    if (
        newValue &&
        (typeof newValue === ‘object‘ || typeof newValue === ‘function‘)
    ) {
        // 获取then函数
        var then = getThen(newValue);
        // 。。。省略

        if (
            then === self.then &&
            newValue instanceof Promise
        ) {
            // 如果newValue 是一个Promise对象,那么调用finale函数
            self._state = 3;
            self._value = newValue;
            finale(self);
            return;
        } else if (typeof then === ‘function‘) {
            // 如果newValue 是一个函数,就继续调用doResolve函数
            doResolve(then.bind(newValue), self);
            return;
        }
    }
    // 标记完成,进入结束流程
    self._state = 1;
    self._value = newValue;
    finale(self);
}

确认newValue的值,如果newValue是一个函数,就继续循环调用doResolve函数;如果newValue 是一个Promise对象,那么就直接调用finale函数。都不是,则直接调用finale函数。

#调用finale函数

进入结束流程,finale结束。

function finale(self) {
    // 单个回调
    if (self._deferredState === 1) {
        // 执行handle函数,实际是执行handleResolved
        handle(self, self._deferreds);
        self._deferreds = null;
    }
    // 回调数组
    if (self._deferredState === 2) {
        for (var i = 0; i < self._deferreds.length; i++) {
            // 执行handle函数,实际是执行handleResolved
            handle(self, self._deferreds[i]);
        }
        self._deferreds = null;
    }
}

finale函数表示进入结束流程,执行handle函数。同时在上面已经说到,在非pedding状态下,执行handle函数,实际会是执行handleResolved函数。

#调用handleResolved函数

handleResolved负责收尾工作,负责执行then或者catch方法注册的回调函数。仔细阅读代码中的备注

var asap = require(‘asap/raw‘);

function handleResolved(self, deferred) {
    asap(function() {
        var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
        // 不存在 onFulfilled & onRejected
        // deferred.promise 只是一个空的Promise对象
        if (cb === null) {
            // 1 - fulfilled(resolved)
            if (self._state === 1) {
                resolve(deferred.promise, self._value);
            } else {
                reject(deferred.promise, self._value);
            }
            return;
        }
        // 执行cb回调函数
        var ret = tryCallOne(cb, self._value);
        if (ret === IS_ERROR) {
            // 错误,报reject
            reject(deferred.promise, LAST_ERROR);
        } else {
            resolve(deferred.promise, ret);
        }
    });
}

通过异步asap调用,若不存在onFulfilledonRejected,直接调用resolvereject。若存在,则tryCallOne回调的结果,直接调用resolvereject。其中的deferred就是上文提到的new Handler实例对象。真正会影响最后这步流程的,其实是deferred.onFulfilled或者 deferred.onRejected的回调执行,执行完回调后,这个Promise的执行过程就基本完成。

reject函数在这里我就不说了,有兴趣的可以看查看源码:reject函数

Promise对象调用函数的基本流程图,只是一个大致的走向,便于理解:

#参考

Promises/A+ 规范

MDN中文: Promise对象

Github: then/promise 源码

tc39: tc39 ecma262 promise

#感谢

掘金:代码君的自由:解读Promise内部实现原理

简书:乌龟怕铁锤:Promise 源代码解析

ps: 微信公众号:Yopai,有兴趣的可以关注,每周不定期更新,分享可以增加世界的快乐

原文地址:https://www.cnblogs.com/liuheng/p/11599563.html

时间: 2024-10-14 00:34:40

Promise核心原理解析的相关文章

angular核心原理解析3:指令的执行过程

指令的执行过程分析. 我们知道指令的执行分两个阶段,一个是compile,一个是link. 我们可以在指令中自定义compile和link. 首先,我们来讲解如何自定义link函数 举个例子: <!doctype html> <html ng-app="myModule"> <head> </head> <body> <hello></hello> </body> <script sr

GeoHash核心原理解析 - OPEN 开发经验库

阅读目录 引子 一.感性认识GeoHash 二.GeoHash算法的步骤 三.GeoHash Base32编码长度与精度 三.GeoHash算法 四.使用注意点 引子 机机是个好动又好学的孩子,平日里就喜欢拿着手机地图点点按按来查询一些好玩的东西.某一天机机到北海公园游玩,肚肚饿了,于是乎打开手机地图,搜索北海公园附近的餐馆,并选了其中一家用餐. 饭饱之后机机开始反思了,地图后台如何根据自己所在位置查询来查询附近餐馆的呢?苦思冥想了半天,机机想出了个方法:计算所在位置P与北京所有餐馆 的距离,然

angular核心原理解析1:angular自启动过程

angularJS的源代码整体上来说是一个自执行函数,在angularJS加载完成后,就会自动执行了. angular源代码中: angular = window.angular || (window.angular = {}) 定义一个全局的angular空对象. 然后: bindJQuery(); //绑定jQuery publishExternalAPI(angular); //扩展angular对象的方法和属性 jqLite(document).ready(function() { an

GeoHash核心原理解析

引子 机机是个好动又好学的孩子,平日里就喜欢拿着手机地图点点按按来查询一些好玩的东西.某一天机机到北海公园游玩,肚肚饿了,于是乎打开手机地图,搜索北海公园附近的餐馆,并选了其中一家用餐. 饭饱之后机机开始反思了,地图后台如何根据自己所在位置查询来查询附近餐馆的呢?苦思冥想了半天,机机想出了个方法:计算所在位置P与北京所有餐馆的距离,然后返回距离<=1000米的餐馆.小得意了一会儿,机机发现北京的餐馆何其多啊,这样计算不得了,于是想了,既然知道经纬度了,那它应该知道自己在西城区,那应该计算所在位置

angular核心原理解析2:注入器的创建和使用

上一课没有讲到创建注入器的方法createInjector. 此方法,会创建两种不同的注入器:第一种叫做providerInjector,第二种叫做instanceInjector.providerInjector是用来创建provider的,instanceInjector是用来创建一个对象实例的. 我们可以在js代码中直接使用注入器: var myModule = angular.module("myModule", []); myModule.factory("pers

GeoHash原理解析

GeoHash 核心原理解析       引子 一提到索引,大家脑子里马上浮现出B树索引,因为大量的数据库(如MySQL.oracle.PostgreSQL等)都在使用B树.B树索引本质上是对索引字段进行排序,然后通过类似二分查找的方法进行快速查找,即它要求索引的字段是可排序的,一般而言,可排序的是一维字段,比如时间.年龄.薪水等等.但是对于空间上的一个点(二维,包括经度和纬度),如何排序呢?又如何索引呢?解决的方法很多,下文介绍一种方法来解决这一问题.   思想:如果能通过某种方法将二维的点数

深入解析Koa之核心原理

这篇文章主要介绍了玩转Koa之核心原理分析,本文从封装创建应用程序函数.扩展res和req.中间件实现原理.异常处理的等这几个方面来介绍,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下.如有不足之处,欢迎批评指正. Koa作为下一代Web开发框架,不仅让我们体验到了async/await语法带来同步方式书写异步代码的酸爽,而且本身简洁的特点,更加利于开发者结合业务本身进行扩展. 本文从以下几个方面解读Koa源码: 封装创建应用程序函数 扩展res和req 中间件实现原理

Spring Boot启动原理解析

Spring Boot启动原理解析http://www.cnblogs.com/moonandstar08/p/6550758.html 前言 前面几章我们见识了SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会吃亏.所以这次博主就跟你们一起一步步揭开SpringBoot的神秘面纱,让它不在神秘. 正文 我们开发任何一个Spring Boot项目,都会用到如下的启动类 从上面代码可以看出,Annotation定义(@Sp

Request 接收参数乱码原理解析二:浏览器端编码原理

上一篇<Request 接收参数乱码原理解析一:服务器端解码原理>,分析了服务器端解码的过程,那么浏览器是根据什么编码的呢? 1. 浏览器解码 浏览器根据服务器页面响应Header中的“Content-Type: text/html; charset=gb2312”解码.修改web.config中“responseEncoding=utf-8”,发现服务器页面响应Header变成了“Content-Type: text/html; charset=utf8”. <system.web&g