21
头图
作者:JowayYoung
仓库:GithubCodePen
博客:官网掘金思否知乎
公众号:IQ前端
特别声明:原创不易,未经授权不得转载或抄袭,如需转载可联系笔者授权

本文由笔者师妹LazyCurry创作,收录于笔者技术文章专栏下

前言

我们都知道,JS是单线程的,只有前一个任务结束,才能执行下一个任务。显然在浏览器上,这样执行会堵塞浏览器对DOM的渲染。所以,JS中会有很多异步操作,那JS是如何实现异步操作呢?这就要想到Promise对象了,文本先来认识Promise,再手写代码实现Promise。

认识Promise

Promise是JS解决异步编程的方法之一,其英文意思是承诺。在程序中可理解为等一段时间就会执行,等一段时间就是JS中的异步。异步是指需要比较长的时间才能执行完成的任务,例如网络请求,读取文件等。Promise是一个实例对象,可从中获取异步处理的结果。

Promise有3种状态,分别是pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作可改变Promise的状态,其他操作都无法改变。并且状态改变后就不会再变,只能是从pendingfulfiledpendingrejected,这也是Promise一个比较鲜明的特点。

使用Promise

上述已说到,Promise是一个对象,那么它肯定是由其构造函数来创建。其构造函数接受一个函数作为参数,其函数的参数有2个,分别是resolverejectresolve将状态从pending变为fulfiled,成功时调用。reject将状态从pending变为rejected,失败时调用。

function RunPromise(num, time) {
    return new Promise((resolve, reject) => {
        console.log("开始执行");
        if (num % 2 === 0) {
            setTimeout(() => {
                resolve(`偶数时调用resolve,此时num为${num}`);
            }, time);
        } else {
            setTimeout(() => {
                reject(new Error(`奇数时调用rejected,此时num为${num}`));
            }, time);
        }
    });
}

Promise对象上有then()catch()方法。then()接收2个参数,第一个对应resolve的回调,第二个对应reject的回调。catch()then()的第二个参数一样,用来接受reject的回调,但是还有一个作用,如果在then()中执行resolve回调时抛出异常,这个异常可能是代码定义抛出,也可能是代码错误,而这个异常会在catch()被捕获到。

RunPromise(22, 2000)
    .then(res => {
        console.log("then的第一个参数执行");
        console.log(res);
        console.log(newres);
    }, error => {
        console.log("then的第二个参数执行");
        console.log(error);
    })
    .catch(error => {
        console.log("error");
        console.log(error);
    });

// 输出结果如下:
// 开始执行
// then的第一个参数执行
// 偶数时调用resolve,此时num为22
// error
// ReferenceError: newres is not defined

上面例子中,RunPromise()调用resolvethen()的第一个参数对应回调,状态从pending改成fulfilled,且状态不会再改变。在then()中,newres这个变量尚未定义,因此程序出错,其异常在catch()被捕获。一般来说,then()使用第一个参数即可,因为catch()then()的第二个参数一样,还能捕获到异常。

实现Promise

Promise大致已了解清楚,也知道如何使用。为了了解Promise是如何实现的,我们手写实现一个简单的Promise方法,简单地实现then()异步处理链式调用。用最简单的思考方法,函数是为了实现什么功能,给对应函数赋予相应的实现代码即可。以下代码均使用ES6进行书写。

定义Promise构造函数

创建Promise对象使用new Promise((resolve, reject) => {}),可知道Promise构造函数的参数是一个函数,我们将其定义为implement,函数带有2个参数:resolvereject,而这2个参数又可执行,所以也是一个函数。

声明完成后,需要解决状态。上述已说过,Promise有3种状态,这里不再细说,直接上代码。

// ES6声明构造函数
class MyPromise {
    constructor(implement) {
        this.status = "pending"; // 初始化状态为pending
        this.res = null; // 成功时的值
        this.error = null; // 失败时的值
        const resolve = res => {
            // resolve的作用只是将状态从pending转为fulfilled,并将成功时的值存在this.res
            if (this.status === "pending") {
                this.status = "fulfilled";
                this.res = res;
            }
        };
        const reject = error => {
            // reject的作用只是将状态从pending转为rejected,并将失败时的值存在this.error
            if (this.status === "pending") {
                this.status = "rejected";
                this.error = error;
            }
        };
        // 程序报错时会执行reject,所以在这里加上错误捕获,直接执行reject
        try {
            implement(resolve, reject);
        } catch (err) {
            reject(err);
        }
    }
}
then函数

我们在使用Promise时,都知道then()有2个参数,分别是状态为fulfilledrejected时的回调函数,我们在这里将2个函数定义为onFulfilledonRejected

class MyPromise {
    constructor(implement) { ... }
    then(onFulfilled, onRejected) {
        // 当状态为fulfilled时,调用onFulfilled并传入成功时的值
        if (this.status === "fulfilled") {
            onFulfilled(this.res);
        }
        // 当状态为rejected时,调用onRejected并传入失败时的值
        if (this.status === "rejected") {
            onRejected(this.error);
        }
    }
}
异步处理

到这里已实现了基本的代码,但是异步时会出现问题。例如,本文一开始举例使用Promise时,resolvesetTimeout()中使用,这时候在then()里,状态还是pending,那就没办法调用到onFulfilled。所以我们先将处理函数(onFulfilledonRejected)保存起来,等到then()被调用时再使用这些处理函数。

因为Promise可定义多个then(),所以这些处理函数用数组进行存储。实现思路:

  • then()增加状态为pending的判断,在此时存储处理函数
  • resolvereject时循环调用处理函数
class MyPromise {
    constructor(implement) {
        this.status = "pending";
        this.res = null;
        this.error = null;
        this.resolveCallbacks = []; // 成功时回调的处理函数
        this.rejectCallbacks = []; // 失败时回调的处理函数
        const resolve = res => {
            if (this.status === "pending") {
                this.status = "fulfilled";
                this.res = res;
                this.resolveCallbacks.forEach(fn => fn()); // 循环执行成功处理函数
            }
        };
        const reject = error => {
            if (this.status === "pending") {
                this.status = "rejected";
                this.error = error;
                this.rejectCallbacks.forEach(fn => fn()); // 循环执行失败处理函数
            }
        };
        try {
            implement(resolve, reject);
        } catch (err) {
            reject(err);
        }
    }
    then(onFulfilled, onRejected) {
        if (this.status === "fulfilled") {
            onFulfilled(this.res);
        }
        if (this.status === "rejected") {
            onRejected(this.error);
        }
        // 当状态为pending时,说明这时还没有调用到resolve或reject
        // 在这里把成功函数和失败函数存至相应的数组中,不做执行操作只做存储操作
        if (this.status === "pending") {
            this.resolveCallbacks.push(() => onFulfilled(this.res));
            this.rejectCallbacks.push(() => onRejected(this.error));
        }
    }
}

测试一下异步功能,打印结果中,'执行resolve'是等待了2秒后打印出来的

new MyPromise((resolve, reject) => {
    console.log("开始执行");
    setTimeout(() => {
        resolve("执行resolve");
    }, 2000);
}).then(res => console.log(res));

// 输出结果如下:
// 开始执行
// 执行resolve
链式调用

到这里就已实现异步操作啦!吼吼~但是,我们都知道,Promise能定义多个then,就例如new Promise().then().then(),这种就是链式调用。当然我们也要实现这个功能。

链式调用是指Promise在状态是fulfilled后,又开始执行下一个Promise。要实现这个功能,我们只需要在then()里返回Promise就好了,说起来好像是挺简单的。

then()的实现思路:

  • then()中需要返回Promise对象,我们将其命名为nextPromise
  • 仍然需要判断状态,执行相应处理
  • onFulfilledonRejected是异步调用,用setTimeout(0)解决
  • 需要对onFulfilledonRejected类型做判断,并做相应返回
class MyPromise {
    constructor(implement) { ... }
    then(onFulfilled, onRejected) {
        // 如果onRejected不是函数,就直接抛出错误
        onFulfilled = typeof onFulfilled === "function" ? onFulfilled : res => res;
        onRejected = typeof onRejected === "function" ? onRejected : err => { throw err; };
        const nextPromise = new MyPromise((resolve, reject) => {
            if (this.status === "fulfilled") {
                // 解决异步问题
                setTimeout(() => {
                    const x = onFulfilled(this.res);
                    RecursionPromise(nextPromise, x, resolve, reject);
                }, 0);
            }
            if (this.status === "rejected") {
                setTimeout(() => {
                    const x = onRejected(this.error);
                    RecursionPromise(nextPromise, x, resolve, reject);
                }, 0);
            }
            if (this.status === "pending") {
                this.resolveCallbacks.push(() => {
                    setTimeout(() => {
                        const x = onFulfilled(this.res);
                        RecursionPromise(nextPromise, x, resolve, reject);
                    }, 0);
                });
                this.rejectCallbacks.push(() => {
                    setTimeout(() => {
                        const x = onRejected(this.error);
                        RecursionPromise(nextPromise, x, resolve, reject);
                    }, 0);
                });
            }
        });
        return nextPromise;
    }
}

RecursionPromise()用来判断then()的返回值,以决定then()向下传递的状态走resolve还是reject,实现思路:

  • nextPromisex不能相等,否则会一直调用自己
  • 判断x的类型,如果不是函数或对象,直接resolve(x)
  • 判断x是否拥有then(),并且如果then()是一个函数,那么就可执行xthen(),并且带有成功与失败的回调
  • flag的作用是执行xthen()时成功与失败只能调用一次
  • 执行xthen(),成功时继续递归解析
  • 如果then()不是一个函数,直接resolve(x)
function RecursionPromise(nextPromise, x, resolve, reject) {
    if (nextPromise === x) return false;
    let flag;
    if (x !== null && (typeof x === "object" || typeof x === "function")) {
        try {
            let then = x.then;
            if (typeof then === "function") {
                then.call(x, y => {
                    if (flag) return false;
                    flag = true;
                    // 这里说明Promise对象resolve之后的结果仍然是Promise,那么继续递归解析
                    RecursionPromise(nextPromise, y, resolve, reject);
                }, error => {
                    if (flag) return false;
                    flag = true;
                    reject(error);
                });
            } else {
                resolve(x);
            }
        } catch (e) {
            if (flag) return false;
            flag = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

总结

具有异步处理链式调用的Promise已实现啦!还有一些方法在这里就不一一实现了。毕竟实现一个完整的Promise不是一篇文章就能讲完的,有兴趣的同学可自行参照Promise的功能进行解构重写,若有写得不正确的地方请各位大佬指出。公众号后台回复promise可获取本文的源码,如果是转载的文章,可关注IQ前端再回复promise即可。

写这篇文章的目的是为了给各位同学提供一个函数解构的思路,学会去分析一个函数的功能,从而解构出每一个步骤是如何执行和实现的,祝大家学习愉快,下次再见~

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更多高质量文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复资料免费领取学习资料
  • 关注后回复进群拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送


JowayYoung
5.1k 声望6.7k 粉丝