完全理解 Promise 实现

完全理解 Promise 基本实现

网上有很多 Promise 实现方式,看了都不是特别理解。
这里以一种更简单的形式一步一步去理解/实现它。这里仅涉及 Promise 构造函数和 then 方法的实现

首先构造一个最基本的 Promise 类

// version_1
class Promise {
    callbacks = [];
    constructor(executor) {
        executor(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
    }
    _resolve(value) {
        this.callbacks.forEach(callback => callback(value));
    }
}

// test
new Promise(resolve => {
    setTimeout(() => {
        console.log('await 2s');
        resolve('ok');
    }, 2000);
}).then((res) => {
    console.log('then', res);
})
  1. Promise 构造函数会立即执行用户传入的函数 executor,并且把 _resolve 方法作为 executor 的参数,传给用户处理
  2. 调用 then 方法(同步),将 onFulfilled 放入callbacks队列,其实也就是注册回调函数,类似于观察者模式。
  3. executor 模拟了异步,这里是过2s后执行 resolve,对应触发 _resolve 内的 callbacks

.then(onFulfilled) 为何需要用一个数组存放?

then 方法可以调用多次,注册的多个onFulfilled,并且这些 onFulfilled callbacks 会在异步操作完成(执行resolve)后根据添加的顺序依次执行

// then 注册多个 onFulfilled 回调
const p = new Promise(resolve => {
    setTimeout(() => {
        console.log('await 2s');
        resolve('ok');
    }, 2000);
});

p.then(res => console.log('then1', res));
p.then(res => console.log('then2', res));
p.then(res => console.log('then3', res));

异步执行处理 setTimeout vs status

上面 Promise 的实现存在一个问题:如果传入的 executor 不是一个异步函数,resolve直接同步执行,这时 callbacks 还是空数组, 导致后面 then 方法注册的 onFulfilled 回调就不会执行(resolve 比 then 注册先执行)

// 同步执行 resolve
new Promise(resolve => {
    console.log('同步执行');
    resolve('同步执行');
}).then(res => {
    console.log('then', res);
})

我们知道 then 中的回调总是通过异步执行的,我们可以在 resolve 中加入 setTimeout,将 callbacks 的执行时机放置到JS消息队列,这样 then方法的 onFulfilled 会先完成注册,再执行消息队列的 resolve

// version_2
class Promise {
    callbacks = [];
    constructor(executor) {
        executor(this._resolve.bind(this));
    }
    then(onFulfilled) {
        this.callbacks.push(onFulfilled);
    }
    _resolve(value) {
        setTimeout(() => {
            this.callbacks.forEach(callback => callback(value));
        })
    }
}

但是这样仍然有问题,如果我们延迟给 then 注册回调,这些回调也都无法执行。因为
还是 resolve 先执行完了,之后注册的回调就无法执行了。

const p = new Promise(resolve => {
    console.log('同步执行');
    resolve('同步执行');
})

setTimeout(() => {
    p.then(res => {
        console.log('then', res); // never execute
    })
});

可以看出 setTimeout 是无法保证 then 注册的 onFulfilled 正确执行的,所以这里必须加入状态机制(pending、fulfilled、rejected),且状态只能由 pending 转换为解决或拒绝。

// version_3:增加状态机制
class Promise {
    callbacks = [];
    status = 'pending';
    value = undefined;
    constructor(executor) {
        executor(this._resolve.bind(this));
    }
    then(onFulfilled) {
        if (this.status === 'pending') {
            this.callbacks.push(onFulfilled);
        } else {
            onFulfilled(this.value);
        }
    }
    _resolve(value) {
        this.status = 'fulfilled';
        this.value = value;
        this.callbacks.forEach(callback => callback(value));
    }
}

当增加了状态后,setTimeout 就可以去掉了,状态机制让注册的回调总是能正确工作。

  • 当 resolve 同步执行时,立即执行 resolve,将 status 设置为 fulfilled ,并把 value 的值存起来, 在此之后调用 then 添加的新回调,都会立即执行
  • 当 resolve 异步执行时,pending 状态执行 then 会添加回调函数, 等到 resolve 执行时,回调函数会全部被执行。

then的链式调用

链式调用我们可能很直接想到 then 方法中返回 this,这样 Promise 实例就可以多次调用 then 方法,但因为是同一个实例,调用再多次 then 也只能返回相同的一个结果。而我们希望的链式调用应该是这样的:

new Promise(resolve => {
    resolve(1)
}).then(res => res + 2) // 1 + 2 = 3
    .then(res => res + 3) // 3 + 3 = 6
    .then(res => console.log(res)); // expected 6

每个 then 注册的 onFulfilled 都返回不同结果,并把结果传给下一个 onFulfilled 的参数,所以 then 需要返回一个新的 Promise 实例

// version_4:then 的链式调用
class Promise {
    callbacks = [];
    status = 'pending';
    value = undefined;
    constructor(executor) {
        executor(this._resolve.bind(this));
    }
    then(onFulfilled) {
        return new Promise(resolveNext => {
            const fulfilled = (value) => {
                const results = onFulfilled(value); // 执行 onFulfilled
                resolveNext(results); // 再执行 resolveNext
            }
            if (this.status === 'pending') {
                this.callbacks.push(fulfilled);
            } else {
                fulfilled(this.value);
            }  
        })
    }
    _resolve(value) {
        this.status = 'fulfilled';
        this.value = value;
        this.callbacks.forEach(callback => callback(value));
    }
}

这样一个 Promise 就基本实现了,我们可以看到:

  • then 方法中,创建并返回了新的 Promise 实例,这是串行 Promise 的基础
  • 我们把 then 方法传入的 形参 onFulfilled 以及创建新 Promise 实例时传入的 resolveNext 合成一个 新函数 fulfilled,这是衔接当前 Promise 和后邻 Promise 的关键所在

处理返回 Promise 类型的回调

这里还有一种特殊的情况:

  • resolve 方法传入的参数为一个 Promise 对象时
  • onFulfilled 方法返回一个 Promise 对象时

这时我们只需用 res instanceof Promise 判断处理下

// version_5:Promise 参数处理
class Promise {
    callbacks = [];
    status = 'pending';
    value = undefined;
    constructor(executor) {
        executor(this._resolve.bind(this));
    }
    then(onFulfilled) {
        return new Promise(resolveNext => {
            const fulfilled = (value) => {
                const results = onFulfilled(value);
                if (results instanceof Promise) {
                    // 如果当前回调函数返回Promise对象,必须等待其状态改变后在执行下一个回调
                    results.then(resolveNext);
                } else {
                    // 否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
                    resolveNext(results);
                }
            }
            if (this.status === 'pending') {
                this.callbacks.push(fulfilled);
            } else {
                fulfilled(this.value);
            }  
        })
    }
    _resolve(value) {
        this.status = 'fulfilled';
        /**
         * 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后,
         * 当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态 
        */
        if (value instanceof Promise) {
            value.then(nextValue => {
                this.value = nextValue;
                this.callbacks.forEach(callback => callback(value));
            })
        } else {
            this.value = value;
            this.callbacks.forEach(callback => callback(value));
        }
    }
}

拓展练习

尝试实现下面函数 LazyMan 的功能

LazyMan('Jack').sleep(3).eat('apple');
// Hi! Jack
// await 3s
// Jack eat apple~
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容