手写一个Promise
JavaScript
2022-01-09
138
0

从异步说起

我们知道,JavaScript是单线程的。JavaScript设计成单线程的原因,是因为最初它就是为了浏览器操作DOM的而生的,而单线程恰恰能保证页面的上的DOM操作是可预期的。

为了防止线程长时间被某项任务所阻塞,JavaScript设计了一种事件循环(Event Loop)的机制,这种机制可以将某些需要占用较长时间的任务完成后所要做的任务放入一个异步队列中,等待所有同步任务完成后再去执行异步任务,这也就是所谓的回调函数的由来。

下图是一个事件循环的示意图:

v2-0b35a3df0b2e2712839ce551062e6d7f_1440w.jpg

异步任务有哪些类型?

任务队列中的异步任务分为宏任务和微任务,他们是两个不同的队列。

宏任务包含以下内容:

  • setTimeout/setInterval
  • I/O 、 Ajax
  • 事件(onclick...)
  • postMessage
  • UI渲染

微任务包含以下内容:

  • Promise.then()
  • MutationObserver
  • process.nextTick(Node.js)

javascript引擎会优先处理微任务,然后再去处理宏任务。

什么是promise?为什么使用promise?

先来谈谈普通回调实现异步所带来的弊病,也就是回调地狱(回调黑洞)问题。

比方说,处理Ajax请求,回调函数形式为:

$.get("http://localhost/a.json",function(data){
    console.log(data);
    // Do something... 1
    $.get("http://localhost/b.json",function(data){
        console.log(data);
        // Do something... 3
        $.get("http://localhost/c.json",function(data){
            console.log(data);
            // Do something... 5
        })
        // Do something else... 4
    })
    // Do something else... 2
})

可以看到,普通的回调函数主要有以下缺点:

  1. 代码书写是不自然的,代码从上而下书写的顺序和执行的顺序不一致。阅读代码需要反复上下切换,十分费劲。
  2. 过多的嵌套、缩进使得代码不优雅,不简洁。
  3. 回调信任问题,在使用第三方代码时,调用回调函数的时机是不能完全被掌握的(过早、晚?,调用次数?,回调内容?)。

而我们使用promise后语法会简洁明了很多:

axios.get("http://localhost/a.json")
    .then(data=>{
        console.log(data);
        return axios.get("http://localhost/b.json")
    })
    .then(data=>{
        console.log(data);
        return axios.get("http://localhost/c.json")
    })
    .then(data=>{
        console.log(data);
    })

当我们实现使用了Async 和 Await语法糖后,代码还会更加简洁:

(async ()=>{
    let data;
    data = await axios.get("http://localhost/a.json");
    console.log(data);
    data = await axios.get("http://localhost/b.json");
    console.log(data);
    data = await axios.get("http://localhost/c.json");
    console.log(data);
})()

什么是Promise?

Promise是一种处理javascript异步的一种方式,可以有效解决回调地狱问题。

Promise实例接收一个executor函数,其又接收两个参数,一个为 onfulfilled,另一个为onrejected

Promise实例都有一个than()方法,它接受事件完成后的回调值。

举一个例子,用Promise实现一个sleep函数:

function sleep(time){
    return new Promise((resolve, reject)=>{
        setTimeout(resolve, time)
    })
}

sleep(100).then(()=>{
    console.log("A")
    return sleep(100);
}).then(()=>{
    console.log("B")
    return sleep(100);
}).then(()=>{
    console.log("C")
})


// Async Await写法:
(async ()=>{
    await sleep(100);
    console.log("A");
    await sleep(100);
    console.log("B");
    await sleep(100);
    console.log("C");
})()

手写一个promise

简易版的Promise:

class Promise {

    constructor(executor) {
        this.status = "pending";
        this.value = null;
        this.reason = null;

        const onfulfilled = value => {
            setTimeout(() => {  // 添加到异步队列中
                if (this.status === "pending") { // 状态只能转换一次
                    this.value = value;
                    this.status = "fulfilled";
                    this.onFulfilledFunc(this.value);
                }
            }, 0)

        }

        const onrejected = reason => {
            setTimeout(() => {  // 添加到异步队列中
                if (this.status === "pending") { // 状态只能转换一次
                    this.reason = reason;
                    this.status = "rejected";
                    this.onRejectedFunc(this.reason);
                }
            }, 0)
        }

        executor(onfulfilled, onrejected);

    }

    then(onfulfilled, onrejected) {
        if (this.status === "fulfilled") {
            onfulfilled(this.value);
        } else if (this.status === "rejected") {
            onrejected(this.reason);
        } else if (this.status === "pending") {
            this.onFulfilledFunc = onfulfilled;     // then是同步执行的,所以需要把回调先存起来,当状态变为fulfilled的时候再去执行。
            this.onRejectedFunc = onrejected;
        }
    }

}

let promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("data");
    }, 1000)
})


promise.then(resolve => {
    console.log(resolve);
}, reject => {
    console.log(reject)
})

这里其实还有一些细节没有处理,这里列举一下:

  1. Promise实例状态更变之前如果添加多个then方法then。解决方法是将所有then方法中的onFulfilledFunc存到一个数组中,在promise决议时候依次执行。
  2. 如果Promise构造函数中出现错误,应该会自动将状态变为rejected。解决方法是使用try...catch...进行包裹。
  3. promise应该实现链式调用。

改进后的代码:

class Promise {

    constructor(executor) {
        this.status = "pending";
        this.value = null;
        this.reason = null;
        this.onFulfilledFuncArr = []; // 这里是数组是因为可以给promise实例添加多个then
        this.onRejectedFuncArr = [];

        const onfulfilled = value => {
            if (value instanceof Promise) {
                return value.then(onfulfilled, onrejected);
            }

            setTimeout(() => {
                if (this.status === "pending") { // 状态只能转换一次
                    this.value = value;
                    this.status = "fulfilled";

                    this.onFulfilledFuncArr.forEach(callback => { // 依次执行then fulfilled回调
                        callback(this.value);
                    });
                }
            }, 0)

        }

        const onrejected = reason => {
            setTimeout(() => {
                if (this.status === "pending") { // 状态只能转换一次
                    this.reason = reason;
                    this.status = "rejected";

                    this.onRejectedFuncArr.forEach(callback => {
                        callback(this.reason);
                    });
                }
            }, 0)
        }

        try {
            executor(onfulfilled, onrejected);
        } catch (e) {
            reject(e); // 若在构造函数内出错,就自动变为rejected
        }

    }

    then(onfulfilled, onrejected) {

        if (this.status === "fulfilled") {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    try {
                        let result = onfulfilled(this.value);
                        resolve(result);
                    } catch (err) {
                        reject(err);
                    }
                })
            })

        } else if (this.status === "rejected") {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    try {
                        let result = onrejected(this.reason);
                        resolve(result);
                    } catch (err) {
                        reject(err)
                    }
                })
            })

        } else if (this.status === "pending") {
            return new Promise((resolve, reject) => {
                this.onFulfilledFuncArr.push(() => {
                    try {
                        let result = onfulfilled(this.value);
                        resolve(result);
                    } catch (err) {
                        reject(err);
                    }
                });

                this.onRejectedFuncArr.push(() => {
                    try {
                        let error = onrejected(this.reason);
                        resolve(error);
                    } catch (err) {
                        reject(err);
                    }
                });
            })

        }

    }

}

当然,这段代码仍然还有改进的空间,例如promise穿透和promise返回值类型问题,这里就不再多做讨论了。

接着我们实现静态方法,分别是Promise.resolve(),Promise.reject(),Promise.all(),Promise.race()

Promise.resolve = function(data) {
    return new Promise((resolve, reject) => {
        resolve(data);
    })
}


Promise.reject = function(err) {
    return new Promise((resolve, reject) => {
        reject(err);
    })
}


Promise.all = function(promiseArr) {
    if (!Array.isArray(promiseArr)) {
        throw new TypeError("The argument should be an array.")
    }


    return new Promise((resolve, reject) => {
        let resultArr = [];
        let length = promiseArr.length;
        promiseArr.forEach(promise => {
            promise.then(data => {
                resultArr.push(data);
                if (resultArr.length === length) {
                    resolve(resultArr);
                }
            }, reject);
        })
    });
}


Promise.race = function(promiseArr) {
    if (!Array.isArray(promiseArr)) {
        throw new TypeError("The argument should be an array.")
    }

    return new Promise((resolve, reject) => {
        promiseArr.forEach(promise => {
            promise.then(resolve, reject);
        })
    })
}

Reference

  • 前端开发核心知识进阶 - 候策
Promise