Promise原理 渐进渐知

一、极简promise雏形

function Promise(fn) {
    var value = null,
        callbacks = [];  //callbacks为数组,因为可能同时有很多个回调
    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
        return this;
    };
    function resolve(value) {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }
    fn(resolve);
}

下面应用一下这个手写的Promise基本原理:

let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('5秒');
    }, 5000);
}).then((tip) => {
    console.log(tip);
})

运行结果如下:

约5秒钟后出现“done 5秒钟”

image

下面运用调试功能,看一下基本的运行逻辑:

let p = new Promise......

这一行上面打断点,调试后代码运行在此行上,点“步入”,进入构造函数

function Promise(fn)

image

callbacks数组为“undefined”

image

从第5步直接跳转至第6步,如下:

image

第7步之后跳到第8步,完成p实例化;第9步开始调用.then函数

image

从第11步可以看出,

(tip) => {
    console.log(tip);

已经压栈进入callbacks数组中。

image

第12步完成.then函数的调用,第13步进入setTimeout异步函数的调用;

image

第14步,输出了“done”

image

第15步,进入resolve函数

image

第16步,value值为“5秒"

image

第17步,相当于第16步时,callback(value)调用的是:

(value) => {
    console.log(value);

image

第18步,

(tip) => {
    console.log(tip);
}

函数调试结束

image

第19步,forEach函数调用结束 ;

image

第20步,resolve函数调用结束 ;

image

第21步,setTimout函数调用结束 ;

至此,调试完毕!

image

综合上述:

上述代码很简单,大致的逻辑是这样的:

  1. 调用then方法,将想要在Promise异步操作成功时执行的回调放入callbacks队列,其实也就是注册回调函数,可以向观察者模式方向思考;
  2. 创建Promise实例时传入的函数会被赋予一个函数类型的参数,即resolve,它接收一个参数value,代表异步操作返回的结果,当一步操作执行成功后,用户会调用resolve方法,这时候其实真正执行的操作是将callbacks队列中的回调一一执行;

首先 new Promise 时,传给 Promise 的函数设置定时器模拟异步的场景,因为setTimeout为异步,没有立即执行,而是紧接着调用 Promise 对象的 then 方法;调用then方法注册异步操作完成后的 onFulfilled(这一步也就相当于将匿名函数压入callbacks数组栈中);然后,又开始执行setTimeout异步函数,在异步函数setTimeout中,调用 resolve(value), 执行 then 方法注册的 onFulfilled。

then 方法注册的 onFulfilled 是存在一个数组中,可见 then 方法可以调用多次,注册的多个onFulfilled 会在异步操作完成后根据添加的顺序依次执行。如下:

function Promise(fn) {
    var value = null,
        callbacks = [];  //callbacks为数组,因为可能同时有很多个回调
    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
        // return this;
    };

    function resolve(value) {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }

    fn(resolve);
}

let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('5秒');
    }, 5000);
})

p.then((tip) => {
    console.log("then1",tip);
})
p.then((tip) => {
    console.log("then2",tip);
})

运行结果如下:

image

上例中,要先定义一个变量 p ,然后 p.then 两次。而规范中要求,then 方法应该能够链式调用。实现也简单,只需要在 then 中添加 return this 即可。如下所示:

function Promise(fn) {
    var value = null,
        callbacks = [];  //callbacks为数组,因为可能同时有很多个回调
    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
        return this;    //this 指向调用这个方法的对象实例;
    };

    function resolve(value) {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }

    fn(resolve);
}

let p = new Promise(resolve => {
    setTimeout(() => {
        console.log('done');
        resolve('5秒');
    }, 5000);
}).then((tip) => {
    console.log("then1",tip);
}).then((tip) => {
    console.log("then2",tip);
})

运行结果如下:

image

二、加入延时机制

上面 Promise 的实现存在一个问题:如果在 then 方法注册 onFulfilled 之前,resolve 就执行了,onFulfilled 就不会执行到了。比如上面的例子中我们把 setTimout 去掉:

function Promise(fn) {
    var value = null,
        callbacks = [];  //callbacks为数组,因为可能同时有很多个回调
    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
        return this;    //this 指向调用这个方法的对象实例;
    };

    function resolve(value) {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }

    fn(resolve);
}

let p = new Promise(resolve =>{
        console.log('同步执行1');
        resolve('同步执行2');
}).then((tip) => {
    console.log("then1",tip);
}).then((tip) => {
    console.log("then2",tip);
})

运行结果如下

image

执行结果显示,只有 "同步执行1" 被打印了出来,后面的”同步执行2“ 、"then1" 和 "then2" 均没有打印出来。再回去看下 Promise 的源码,也很好理解,在调用resolve函数 执行时,callbacks 还是空数组,还没有onFulfilled 注册上来,所以resolve、then1、then2都没有执行。

在前面的调试中已经看到,是先执行.then函数将其中的匿名函数注册到callbacks数组中,再调用的setTimeout函数中的resolve,因为callbacks数组中已经压入了匿名函数,所以resolve函数可以调用并执行。纵观整个Promise,执行顺序为then->setTimeout->resolve,这样就保证了Promise的顺利执行。

Promises/A+规范明确要求回调需要通过异步方式执行,用以保证一致可靠的执行顺序。因此要加入一些处理,保证在 resolve 执行之前,then 方法已经注册完所有的回调:

function Promise(fn) {
    var value = null,
        callbacks = [];  //callbacks为数组,因为可能同时有很多个回调
    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
        return this;    //this 指向调用这个方法的对象实例;
    };

    //改写resolve函数,增加setTimeout;
    function resolve(value) {
    setTimeout(function() {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }, 0)}

    fn(resolve);
}

let p = new Promise(resolve =>{
        console.log('同步执行1');
        resolve('同步执行2');
}).then((tip) => {
    console.log("then1",tip);
}).then((tip) => {
    console.log("then2",tip);
})

运行结果如下

image

上述代码中,我们在 resolve 中增加定时器,通过 setTimeout 机制,将 resolve 中执行回调的逻辑放置到JS任务队列末尾,以保证先执行then方法的 onFulfilled注册完成,再执行resolve函数(注:这时callbacks数组中已经压入then1和then2两个匿名函数,resolve函数就可以调用并使用了)。

但是这样依然存在问题,在 resolve 执行后,再通过 then 注册上来的 onFulfilled 都没有机会执行了。如下所示,我们加了延迟后,then1 和 then2 可以打印出来了,但下例中的 then3 依然打印不出来。所以我们需要增加状态,并且保存 resolve 的值。

function Promise(fn) {
    var value = null,
        callbacks = [];  //callbacks为数组,因为可能同时有很多个回调
    this.then = function (onFulfilled) {
        callbacks.push(onFulfilled);
        return this;    //this 指向调用这个方法的对象实例;
    };

    //改写resolve函数,增加setTimeout;
    function resolve(value) {
    setTimeout(function() {
        callbacks.forEach(function (callback) {
            callback(value);
        });
    }, 0)}

    fn(resolve);
}

let p = new Promise(resolve =>{
        console.log('同步执行1');
        resolve('同步执行2');
}).then((tip) => {
    console.log("then1",tip);
}).then((tip) => {
    console.log("then2",tip);
})

setTimeout(() => {
    p.then(tip => {
        console.log('then3', tip);
    })
});

运行结果如下

image

三、增加状态

为了解决上一节抛出的问题,我们必须加入状态机制,也就是大家熟知的pendingfulfilledrejected

Promises/A+规范中的2.1Promise States中明确规定了,pending可以转化为fulfilledrejected并且只能转化一次,也就是说如果pending转化到fulfilled状态,那么就不能再转化到rejected。并且fulfilledrejected状态只能由pending转化而来,两者之间不能互相转换。如下图所示:

image

增加状态后的Promise如下

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];
    this.then = function (onFulfilled) {
        if (state === 'pending') {
            callbacks.push(onFulfilled);
            return this;
        }
        onFulfilled(value);
        return this;
    };
    function resolve(newValue) {
        value = newValue;
        state = 'fulfilled';
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                callback(value);
            });
        }, 0);
    }
    fn(resolve);
}

let p = new Promise((resolve,reject) => {
    setTimeout(()=>{
        resolve('morrain');
    },1000)
}).then(value => {
    console.log('value=',value);
}).then(value => {
    console.log('value=',value);
})

运行结果如下

image

resolve 执行时,会将状态设置为 fulfilled ,并把 value 的值存起来,在此之后调用 then 添加的新回调,都会立即执行,直接返回保存的value值。

增加代码:

setTimeout(() => {
    p.then(tip => {
        console.log('then3', tip);
    })
});

改造代码如下

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];
    this.then = function (onFulfilled) {
        if (state === 'pending') {
            callbacks.push(onFulfilled);
            return this;
        }
        onFulfilled(value);
        return this;
    };
    function resolve(newValue) {
        value = newValue;
        state = 'fulfilled';
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                callback(value);
            });
        }, 0);
    }
    fn(resolve);
}

let p = new Promise((resolve,reject) => {
    setTimeout(()=>{
        resolve('morrain');
    },1000)
}).then(value => {
    console.log('value=',value);
}).then(value => {
    console.log('value=',value);
})
//改造增加的代码如下:
setTimeout(() => {
    p.then(tip => {
        console.log('then3', tip);
    })
});

运行结果如下

image 

 从上述代码的调试可以看到,增加的代码

setTimeout(() => {
    p.then(tip => {
        console.log('then3', tip);
    })
});

此时的state已经是fulfilled状态了,所以,走的是

onFulfilled(value);
        return this;

这个分支并执行的。

至此,一个初具功能的Promise就实现好了,它实现了 then,实现了链式调用,实现了状态管理等等。但仔细想想,链式调用的实现只是在 then 中 return 了 this,因为是同一个实例,调用再多次 then 也只能返回相同的一个结果,这显然是不能满足我们的要求的。下面,讲述如何实现真正的链式调用。

三、链式调用

通常,我们希望的链式调用是如下这样的:

const p1 = Promise.resolve(42);
const p2 = p1.then(value => value + 1); // 返回 43
p2.then(console.log); // 输出: 43

每个 then 注册的 onFulfilled 都返回了不同的结果,层层递进,很明显在 then 方法中 return this 不能达到这个效果。引入真正的链式调用,then 返回的一定是一个新的Promise实例

真正的链式 Promise 是指在当前 Promise 达到 fulfilled 状态后,即开始进行下一个 Promise(后邻 Promise)。那么我们如何衔接当前 Promise 和后邻 Promise 呢?(这是理解 Promise 的难点

链式调用的实现

就是在then方法里面return一个promise就好啦。

function Promise(fn) {
    var state = 'pending',
        value = null,
        callbacks = [];
    this.then = function (onFulfilled) {
        return new Promise(function (resolve) {
            handle({
                onFulfilled: onFulfilled || null,
                resolve: resolve
            });
        });
    };
    function handle(callback) {
        if (state === 'pending') {
            callbacks.push(callback);
            return;
        }
        //如果then中没有传递任何东西
        if(!callback.onFulfilled) {
            callback.resolve(value);
            return;
        }
        var ret = callback.onFulfilled(value);
        callback.resolve(ret);
    }

    function resolve(newValue) {
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }
    fn(resolve);
}

let p = new Promise((resolve,reject) => {
    setTimeout(()=>{
        resolve(42);
    },1000)
}).then(
    value => value + 1
).then(console.log)

上述代码的大体逻辑与前面的测试截图基本是一致的,这里面最大的难点就是理解

this.then = function (onFulfilled) {
        return new Promise(function (resolve) {
            handle({
                onFulfilled: onFulfilled || null,
                resolve: resolve
            });
        });
    };

这一段代码中,new Promise实例中调用的handle函数为什么是上一个Promise实例的handle方法?

为了跟踪,我们在代码中加入验证相关代码,如下

// 用于生成Promise实例的唯一ID
let promiseId = 0;

function Promise(fn) {
    // 给每个Promise实例添加唯一标识
    this.id = ++promiseId;
    var state = 'pending',
        value = null,
        callbacks = [];

    // 打印:当前实例初始化,展示ID和空的callbacks
    console.log(`[Promise ${this.id}] 初始化,callbacks初始值:`, callbacks);

    this.then = function (onFulfilled) {
        // 创建新的Promise实例,并打印关联信息
        const newPromise = new Promise(function (resolve) {
            handle({
                onFulfilled: onFulfilled || null,
                resolve: resolve
            });
        });
        // 打印:当前实例调用then,返回新实例的ID
        console.log(`[Promise ${this.id}] 调用then,返回新Promise ${newPromise.id}`);
        return newPromise;
    };

    function handle(callback) {
        if (state === 'pending') {
            callbacks.push(callback);
            // 打印:当前实例的callbacks新增回调,展示当前callbacks长度
            console.log(`[Promise ${this.id}] callbacks新增回调,当前长度:`, callbacks.length);
            return;
        }
        // 如果then中没有传递任何东西
        if(!callback.onFulfilled) {
            callback.resolve(value);
            return;
        }
        var ret = callback.onFulfilled(value);
        callback.resolve(ret);
    }

    function resolve(newValue) {
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        // 打印:当前实例状态变为fulfilled,准备执行callbacks
        console.log(`[Promise ${this.id}] 状态变为fulfilled,准备执行callbacks(长度:${callbacks.length})`);

        setTimeout(function () {
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }

    // 绑定this到handle和resolve,否则内部this指向window
    handle = handle.bind(this);
    resolve = resolve.bind(this);

    fn(resolve);
}

// 测试代码
let p = new Promise((resolve,reject) => {
    setTimeout(()=>{
        console.log('执行resolve(42)');
        resolve(42);
    },1000)
}).then(
    value => value + 1
).then(console.log)

在上述代码中

new Promise((resolve,reject) => {
    setTimeout(()=>{
        console.log('执行resolve(42)');
        resolve(42);
    },1000)
})

上面这段代码产生的Promise实例为p1

.then(
    value => value + 1
)

上面这段代码,即第一个then函数产生的new Promise实例为p2

.then(console.log)

上面这段代码,产生的new Promise实例为p3

在下面位置处打断点,并开始调试

image

当调试从.then(value => value + 1)进入到this.then = function(onFullfilled)函数内部时,newPromise的id是2,但是在console.log显示当前的上下文环境还是id=1,也就是说,还是p1的实例环境。

通俗理解就是:newPromise函数内调用的handle是上一个Promise实例的handle。

彻底搞清楚为什么新 Promise 会关联到上一个 Promise 的 handle 函数,这是理解 Promise 实现本质的关键,下面进行具体解读:

核心前提:先明确两个基础概念

 

在分析前,先巩固两个底层概念,这是理解的基石:

 
  1. 词法作用域(静态作用域):函数的作用域在定义时就确定了,而非执行时。比如函数 A 定义在函数 B 内部,A 就能访问 B 的变量,不管 A 在哪里执行。也就是说,JavaScript 引擎在解析阶段(编译阶段)就确定了每个变量和函数的作用域。
  2. 作用域链:函数执行时,会先在自己的作用域找变量,找不到就沿着定义时的作用域链向上找,直到全局作用域。

 

从作用域视角拆解 Promise 执行逻辑

 
你的 Promise 实现中,handlethenresolve 等函数的词法作用域绑定,是 “新 Promise 关联上一个 handle” 的根本原因。我们结合作用域链,逐环节分析:
 

1. Promise 构造函数的作用域结构(定义阶段)

 
先看 Promise 构造函数的作用域嵌套关系(用伪代码展示作用域层级):
// 全局作用域
let promiseId = 0;
function Promise(fn) { // 构造函数作用域(记为 Scope-P)
    const self = this; // 指向当前 Promise 实例(如 P1、P2)
    this.id = ++promiseId;
    var state = 'pending',
        value = null,
        callbacks = [];

    // -------------- then 函数:定义在 Scope-P 中 --------------
    this.then = function (onFulfilled) { // then 函数作用域(记为 Scope-then)
        // Scope-then 能访问外层的 Scope-P(self、state、callbacks、handle 等)
        const nextPromise = new Promise(function (resolve) { // 新 Promise 的构造函数作用域(Scope-P2)
            // 核心:这里调用的 handle 是外层 Scope-P 的 handle(上一个 Promise 的 handle)
            handle({
                onFulfilled: onFulfilled || null,
                resolve: resolve
            });
        });
        return nextPromise;
    };

    // -------------- handle 函数:定义在 Scope-P 中 --------------
    function handle(callback) { // handle 函数作用域(Scope-handle)
        // Scope-handle 能访问外层 Scope-P 的所有变量:self、state、value、callbacks
        if (state === 'pending') {
            callbacks.push(callback);
            return;
        }
        if(!callback.onFulfilled) {
            callback.resolve(value);
            return;
        }
        var ret = callback.onFulfilled(value);
        callback.resolve(ret);
    }

    // -------------- resolve 函数:定义在 Scope-P 中 --------------
    function resolve(newValue) { // resolve 函数作用域(Scope-resolve)
        // Scope-resolve 能访问外层 Scope-P 的所有变量:self、state、value、callbacks、handle
        if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
            var then = newValue.then;
            if (typeof then === 'function') {
                then.call(newValue, resolve);
                return;
            }
        }
        state = 'fulfilled';
        value = newValue;
        setTimeout(function () {
            // 这里调用的 handle 是 Scope-P 的 handle(当前 Promise 实例的 handle)
            callbacks.forEach(function (callback) {
                handle(callback);
            });
        }, 0);
    }

    resolve._owner = this;
    fn(resolve);
}

2. 关键:then 函数中 handle 的作用域绑定(核心原因)

 
当你调用 P1.then(...) 时,then 函数的执行逻辑如下(结合作用域链):
// P1 是第一个 Promise 实例,其构造函数作用域是 Scope-P1
P1.then(function (value) { return value + 1 }) 
  • Step 1:执行 P1.then,进入 then 函数的作用域(Scope-then1),这个作用域词法上绑定到 Scope-P1(因为 then 函数定义在 P1 的构造函数作用域里)。
  • Step 2:在 Scope-then1 中创建新 Promise 实例 P2,执行 P2 的构造函数(Scope-P2),并传入一个匿名函数:
function (resolve) { // 这个函数定义在 Scope-then1 中,作用域链指向 Scope-then1 → Scope-P1 → 全局
    handle({ onFulfilled: ..., resolve: resolve });
}

Step 3:执行这个匿名函数时,要找 handle 变量:

  • 先在自己的作用域(匿名函数作用域)找 → 没有;
  • 沿着作用域链向上找,到 Scope-then1 → 没有;
  • 再向上到 Scope-P1(P1 的构造函数作用域)→ 找到 handle 函数(P1 的 handle);
  • 因此,这里调用的 handleP1 的 handle,而非 P2 的 handle。

 

这就是最底层的原因:then 函数内调用的 handle,通过词法作用域链,绑定到了定义 then 函数的那个 Promise 实例(上一个 Promise)的 handle,而非新创建的 Promise 实例的 handle
 

3. 回调执行阶段:作用域链的延续

 
当 P1 执行 resolve(42) 后,触发 callbacks.forEach(handle)
 
  • callbacks 是 Scope-P1 中的数组,里面存的是包含 P2 的 resolve 函数的回调对象;
  • 遍历执行 handle(callback) 时,handle 依然是 Scope-P1 中的函数(作用域绑定未变);
  • handle 中执行 callback.resolve(ret) 时,callback.resolve 是 P2 的 resolve 函数(定义在 Scope-P2 中),但执行环境还是 Scope-P1 的 handle 作用域;
  • P2 的 resolve 执行后,又会触发 P2 的 handle(Scope-P2 中的 handle),处理 P3 的 resolve —— 作用域链层层传递,形成链式调用。

 

从词法作用域和作用域链的底层视角,核心结论如下:
 
  1. 词法绑定是根本then 函数定义在某个 Promise 实例(如 P1)的构造函数作用域内,因此 then 内部调用的 handle 会通过作用域链,绑定到该实例的 handle(P1 的 handle),而非新创建的实例(P2)的 handle
  2. 作用域链传递链式调用:每个 Promise 实例的 handle 作用域,会调用下一个 Promise 实例的 resolve,而 resolve 又绑定到新实例的作用域,从而通过作用域链的层层传递,实现 Promise 的链式执行。
  3. 执行时作用域不变:函数的作用域在定义时确定,不管 handleresolve 在哪里执行,都会沿着定义时的作用域链找变量,这是 JavaScript 静态作用域的核心特征,也是 Promise 链式调用能实现的底层保障。

 当第一个 Promise 成功时,resolve 方法将其状态置为 fulfilled ,并保存 resolve 带过来的value。然后取出 callbacks 中的对象,执行当前 Promise的 onFulfilled,返回值通过调用第二个 Promise 的 resolve 方法,传递给第二个 Promise。

经上思路参考如下文章:

https://mengera88.github.io/2017/05/18/Promise%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/

https://zhuanlan.zhihu.com/p/102017798

posted @ 2026-01-27 22:03  chenlight  阅读(16)  评论(0)    收藏  举报