手写一个 Promise

  

  Promise 的作用是优化异步操作,解决“回调地狱” 问题。

  对于异步操作,可以将其封装为 promise 对象,通过链式调用替代回调函数的嵌套。例如:

let print = function (msg) {
    return new Promise((resolve, reject) => {
        try {
            setTimeout(() => {
                msg += "+";
                console.log(msg);
                resolve(msg);
            }, 0);
        } catch (err) {
            reject(err);
        }
    });
}

  对于发送 XHR 请求等异步操作,与 setTimeout 的封装思路是一致的,在回调中恰当的地方调用 resolve 及 reject 方法,将调用结果传出。

  这样,在顺序的调用多次该异步函数时,调用方法如下:

print("msg").then((msg) => {
    return print(msg);
}).then((msg) => {
    return print(msg);
}).then((msg) => {
    return print(msg);
});

  打印结果如下:

 

  异步调用的结果被顺序的传递了下去。

  对于同步函数,封装思路一致,关键在于 resolve 和 reject 的调用。当然在实际生产时,使用 promise 封装同步函数是一种脱裤子放 X 的行为,这里只是用来加深对 promise 的理解。比如:

let print = function (msg) {
    return new Promise((resolve, reject) => {
        msg += "+";
        console.log(msg);
        resolve(msg);
    })
}

  调用方式:

print("msg").then((msg) => {
    return print(msg);
}).then((msg) => {
    return print(msg);
}).then((msg) => {
    return print(msg);
});

  调用结果:

  在实际使用中,then 方法中传入的方法不是一定要返回 Promise 对象,也可以做一些常规操作,比如:

let print = function (msg) {
    return new Promise((resolve, reject) => {
        try {
            setTimeout(() => {
                msg += "+";
                console.log(msg);
                resolve(msg);
            }, 0);
        } catch (err) {
            reject(err);
        }
    });
}

print("msg").then((msg) => {
    console.log(msg);
    msg += "+";
    return msg;
}).then((msg) => {
    console.log(msg);
    msg += "+";
    return msg;
}).then((msg) => {
    console.log(msg);
    msg += "+";
    return msg;
});

  结果:

  也就是说,调用 then 方法除了串行化的执行一些异步函数,并传递它们的返回值外,还可以设计单独的函数对这些返回值进行常规处理,可以极大优化代码的结构和可读性。

  Promise 的基本使用介绍完毕,下面来手撕一个 Promise 对象。

  将异步操作串行化,放在一些后端语言中,最容易想到的思路应该是线程等待(挂起)/唤醒的处理。比如在 JAVA 中,使用 wait/notify 来进行线程间的协作。

  在 JS 中,异步操作的本质与其它语言相同,异步任务交给其它线程处理,比如浏览器中有专门执行 setTimeout 计时的线程、专门用来发送 XHR 请求的线程。但对于编程者来说,JS 没有提供操作线程的相关方法,我们写的代码都运行在 JS 的主线程(js引擎线程)上,包括我们传递给异步任务的回调方法,也是在主线程中执行的。这种模式虽然局限了手脚,但也帮我们屏蔽了线程安全方面的问题。

  执行异步操作的线程将任务执行的结果与回调函数打包压入主线程的任务栈中,主线程执行完同步任务,从任务栈中取出异步任务的回调任务执行,周而复始。

  这是再熟悉不过的处理思路,操作系统的线程调度以这种方式响应进程信号(比如 linux 中常用的 kill 指令)、JAVA 的 sleep 以类似方式响应 interrupt 信号等。在主任务循环开始或结束部分预留额外任务的切入点,为我们在运行过程中给主任务插入额外的临时任务留下口子。

  所以目前的问题是,使用单线程,只能通过回调函数获取异步操作结果的情况下,如何对异步函数进行串行化处理。

  单线程环境下的发布订阅模式非常适合上述场景。

  将异步操作封装为对象,存储异步操作的结果、状态、以及回调任务。resolve 方法作为传入异步结果的入口、reject 方法作为传入异常信息的入口。将执行成功、失败异步任务分别存储在不同的任务队列中,执行 resolve 、 reject 时,传入数据并执行已经注册的任务:

var $MyPromise = function (task) {
let self = this;
this.state = "pending";
this.successData = "";
this.errorMsg = "";
this.successCallBackQueue = [];
this.errorCallBackQueue = [];
this.resolve = function (data) {
if (self.state != "pending") return;
self.state = "fulfilled";
self.successData = data;
for (let item of self.successCallBackQueue) item(self.data);
}
this.reject = function (msg) {
if (self.state != "pending") return;
self.state = "rejected";
self.errorMsg = msg;
for (let item of self.errorCallBackQueue) item(self.msg);
}
try {
task(this.resolve, this.reject);
} catch (err) {
self.reject(err);
}
}

  这样,在异步任务中,正确的调用 resolve,reject 方法,即可将执行结果、异常信息封装到 Promise 中。

  注意 resolve,reject 函数中的 this 指向,因为它们的执行环境往往在传入的任务函数中以全局环境执行,该情况下直接在这两个函数中写 this 会指向 window 对象。

  当 then 方法先于 resolve,reject 调用时,将任务压入 successCallBackQueue、errorCallBackQueue 队列,resolve,reject 调用时会将队列中的任务都执行一遍。

  当 then 方法后于 resolve,reject 调用时,因为执行结果、异常信息已经存入 successData、errorMsg,直接执行任务即可。

  所以将异步调用的相关数据封装后,无论 then 方法、resolve,reject 方法调用的先后顺序如何,then 方法中传入的任务都可以被正确的执行,都可以正确的获取异步任务的结果及异常信息。

  那么 then 方法需要做的便是,如果当前对象状态位 pending(执行中),将任务压入 successCallBackQueue、errorCallBackQueue 队列;如果当前状态为 rejected(异常),直接执行传入的异常处理任务,并向任务中传入异步任务的异常信息 errorMsg。如果当前状态为 fulfilled(执行成功),直接执行传入的下一步任务,并传入异步任务执行的结果 successData。

MyPromise.prototype.then = function (resolve, reject) {
    if (this.state === "pending") {
        this.successCallBackQueue.push(resolve);
        this.errorCallBackQueue.push(reject);
    } else if (this.state === "fulfilled") resolve(this.successData);
    else if (this.state === "rejected") reject(this.errorMsg);
}

  上面的写法已经可以支持异步任务的串行化调用,但无法支持链式调用。

  链式调用需要返回一个与本对象同属一类的新对象,来达到继续调用相关方法的目的(返回本对象没有意义,因为每个 Promise 是对一次异步任务的封装,返回一个新对象表示返回下个任务的 Promise 对象)。

  链式调用的目的除了语法上的方便,还应可以不断的向下传递上次任务的结果。因此返回新 Promise 对象分为两种情况:then 中的任务返回的是具体值;then中的任务又返回了一个 Promise

对象。

  对于情况 1 ,只需要将返回的具体值传递给下个 Promise 即可,对于情况 2 ,需要将返回 Promise 的 successData 与 errorMsg 传递给外层 Promise,向内层 Promise 的 then 传入返回 Promise 的 resolve,reject 即可:

$MyPromise.prototype.then = function (successCallback, errorCallback) {
let self = this;
if (this.state === "pending") {
return new $MyPromise((resolve, reject) => {
self.successCallBackQueue.push(() => {
let re = successCallback(self.successData);
if (re instanceof $MyPromise) {
re.then(resolve, reject);
} else resolve(re);
});
self.errorCallBackQueue.push(() => {
let re = errorCallback(self.errorMsg);
if (re instanceof $MyPromise) {
re.then(resolve, reject);
} else resolve(re);
});
})
} else if (this.state === "fulfilled") {
return new $MyPromise((resolve, reject) => {
let re = successCallback(self.successData);
if (re instanceof $MyPromise) {
re.then(resolve, reject);
} else resolve(re);
})
} else if (this.state === "rejected") {
return new $MyPromise((resolve, reject) => {
let re = errorCallback(self.errorMsg);
if (re instanceof $MyPromise) {
re.then(resolve, reject);
} else resolve(re);
})
}
}

  完整代码:


var $MyPromise = function (task) {
let self = this;
this.state = "pending";
this.successData = "";
this.errorMsg = "";
this.successCallBackQueue = [];
this.errorCallBackQueue = [];
this.resolve = function (data) {
if (self.state != "pending") return;
self.state = "fulfilled";
self.successData = data;
for (let item of self.successCallBackQueue) item(self.data);
}
this.reject = function (msg) {
if (self.state != "pending") return;
self.state = "rejected";
self.errorMsg = msg;
for (let item of self.errorCallBackQueue) item(self.msg);
}
try {
task(this.resolve, this.reject);
} catch (err) {
self.reject(err);
}
}

$MyPromise.prototype.then = function (successCallback, errorCallback) {
let self = this;
if (this.state === "pending") {
return new $MyPromise((resolve, reject) => {
self.successCallBackQueue.push(() => {
let re = successCallback(self.successData);
if (re instanceof $MyPromise) {
re.then(resolve, reject);
} else resolve(re);
});
self.errorCallBackQueue.push(() => {
let re = errorCallback(self.errorMsg);
if (re instanceof $MyPromise) {
re.then(resolve, reject);
} else resolve(re);
});
})
} else if (this.state === "fulfilled") {
return new $MyPromise((resolve, reject) => {
let re = successCallback(self.successData);
if (re instanceof $MyPromise) {
re.then(resolve, reject);
} else resolve(re);
})
} else if (this.state === "rejected") {
return new $MyPromise((resolve, reject) => {
let re = errorCallback(self.errorMsg);
if (re instanceof $MyPromise) {
re.then(resolve, reject);
} else resolve(re);
})
}
}

  测试一下,异步函数链式调用:

  同步函数链式调用:

  Promise 嵌套 Promise 的处理个人感觉比较惊艳,自认自己去想的话,设计不出这么巧妙的结构。

  受这种类似递归设计的启发,个人以为在设计类或者设计函数时,一定要符合单一职责原则,尽量准确简单的明确其语义,才有希望写出如此精妙的结构。

  总结一下:

  面向对象时对象封装的不一定是实体,比如 Promise 就封装了一次具体调用的过程。

  封装类时,注意成员函数会被谁在什么环境下调用,注意 this 指向。

  发布订阅模式的实现。

  链式调用中 then 与 resolve 的使用很惊艳,设计函数或类时符合单一职责原则,尽量准确简单的明确语义。

posted @ 2021-08-16 22:44  牛有肉  阅读(209)  评论(0编辑  收藏  举报