前端进阶:手写Promise

557 阅读8分钟

2020.3正式进军前端,目标高级前端工程师,经验尚浅,文章内容如若有误,欢迎指正。

Promise对象是一个可以管理同步 / 异步操作运行结果的状态和数据的容器。它的核心逻辑就是外部操作合理地把它的运行结果状态和数据交由容器存储并管理,而后外部再从容器中取出该外部操作的状态和数据进行下一步操作。

本文接下来从以下手写Promise的五个关键认识开始探讨,最终完成手写Promise:

  • Promise容器的基本模样(容器的属性和行为)
  • Promise容器的构造方式(容器托管excutor函数的运行结果状态和数据)
  • 往Promise容器中写入数据(resolve、reject函数负责)
  • 从Promise容器中读取数据(then方法负责)
  • 实现then函数的链式调用

一:Promise容器的基本模样(容器的属性和行为)

1.容器管理的数据(属性)

  • 状态state:pending、resolved / fulfilled、rejected
  • 结果value:容器resolved / fulfilled状态下的数据
  • 拒因reason:容器rejected状态下的数据
  • onResolvedTodoList:pending状态时添加,容器resolved / fulfilled状态后的回调行为数组
  • onRejectedTodoList:pending状态时添加,容器rejected状态后的回调行为数组

2.容器数据的读写(行为)

  • 写操作:容器内部定义一个可以设置内部状态和数据的resolve和reject方法,以供外部为容器中写入数据(value / reason)。实际上,Promise容器对写操作有约束,它只允许初始化写操作,不允许更改写操作。
  • 读操作:容器内部定义一个可以读取内部状态和数据的then方法,以供外部读取容器中的状态和数据后触发相应的回调行为。

3.代码实现

class Container {
    state = undefined;
    value = undefined;
    reason = undefined;
    
    constructor(excutor) { // 构造容器 }
    
    resolve = value => { // 写容器数据 }
    reject = reason => { // 写容器数据 }
    
    onResolvedTodoList = [];
    onRejectedTodoList = [];
    then(onResolved, onRejected) { // 读容器数据 }
}

Container.PENDING = 'pending';
Container.RESOLVED = 'resolved';
Container.REJECTED = 'rejected';

二:Promise容器实例的构造方式(容器托管excutor函数的运行结果状态和数据)

1.构造方法的参数函数excutor的定义

  • 哪里定义:容器外部定义
  • 函数职责:1是业务操作载体。2是把excutor运行后的状态和数据交给容器管理

容器外部对构造方法调用示例:

// 重点关注:构造参数excutor函数的定义,它定义了两个形参resolve和reject
const p1 = new Container((resolve, reject) => {
  // 内部逻辑决定什么时候初始化容器数据,初始化什么数据
  setTimeout(() => {
    resolve(0)
  })
})

2.构造方法的参数函数excutor的调用

  • 哪里调用:容器内部调用
  • 调用时机:构造容器实例时就调用

容器内部对构造方法的实现示例:

class Container {
    state = undefined;
    value = undefined;
    reason = undefined;
	
	// 重点关注:构造参数excutor函数的调用,它的调用时机以及实参传递。
    constructor(excutor) {
        try {
            excutor(this.resolve, this.reject);
            this.state = Container.PENDING;
        } catch (e) {
            this.reject(e)
        }
    }

    resolve = value => { // 写容器数据 }
    reject = reason => { // 写容器数据 }
    
    then(onResolved, onRejected) { // 读容器数据 }
}

Container.PENDING = 'pending';
Container.RESOLVED = 'resolved';
Container.REJECTED = 'rejected';

三:往Promise容器中写入数据(resolve、reject函数负责)

1.resolve、reject函数的定义

  • 哪里定义:容器内部定义
  • 函数职责:把excutor运行后的状态和数据交给容器管理
  • 不允许更新容器状态和数据:如果容器状态不是pending就视为无效通知,啥也不做。
  • 处理状态订阅的回调数组:onResolvedTodoList 和 onRejectedTodoList数组中存储着订阅某一状态的回调函数,在初始化好容器管理的状态和数据后,就可以根据容器状态,按插入顺序把回调函数从回调数组中逐个取出执行。
  • 定义在容器内部:定义在容器内部,方便访问并设置容器状态和数据。
  • 定义为箭头函数:定义为箭头函数,调用者就不能通过任何方式改变其内部的this指向,即使使用call、apply、bind函数。

容器内部对resolve和reject的实现示例:

class Container {
    constructor(excutor) {} 

    resolve = value => {
        if (this.state != Container.PENDING) return
        this.status = Container.RESOLVED;
        this.value = value;
        while (this.onResolvedTodoList.length) this.onResolvedTodoList.shift()() // 取出第一个
    }

    reject = reason => {
        if (this.state != Container.PENDING) return
        this.status = Container.REJECTED;
        this.reason = reason;
        while (this.onRejectedTodoList.length) this.onRejectedTodoList.shift()()
    }
    
    then(onResolved, onRejected) {}
}

2.resolve、reject函数的调用

  • 哪里调用:容器外部调用
  • 调用时机:支持同步、异步方式调用,异步调用时,表现为回调函数的调用。

容器外部对resolve和reject的调用示例:

const p1 = new Promise((resolve, reject) => {
  // 同步方式往容器中写入数据
  // resolve(0)
  // 异步方式往容器中写入数据
  setTimeout(() => {
    resolve(1)
  })
})

四:从Promise容器中读取数据(then方法负责)

1.then函数的定义

  • 哪里定义:容器内部定义
  • 函数职责:接收调用者传入的两个回调函数,根据容器存储的状态判断执行哪一个函数,同时把容器存储的数据作为输入执行这个函数。
  • 可选状态处理函数:允许调用者只针对某一个状态进行操作。
  • pending状态处理:pending状态时,容器没有拿到excutor运行结果的状态和数据,所以把两个回调函数分别放入回调函数数组onResolvedTodoList 和 onRejectedTodoList中,等待程序运行得到excutor执行结果的状态和数据并交付给容器管理之后再执行,也就是容器的resolve和reject方法调用之后。

容器内部对then函数的实现示例:

class Container {
    constructor(excutor) {}

    resolve = value => {}
    reject = reason => {}
    
    onResolvedTodoList = [];
    onRejectedTodoList = [];
    
    then(onResolved, onRejected) {
        onResolved = onResolved ? onResolved : value => value;
        onRejected = onRejected ? onRejected : reason => { throw reason };
        switch (this.state) {
            case Container.PENDING:
                this.onResolvedTodoList.push(() => {
                    try {
                        const value = onResolved(this.value);
                        resolve(value);
                    } catch (e) {
                        reject(e);
                    }
                });
                this.onRejectedTodoList.push(() => {
                    try {
                        const value = onRejected(this.reason);
                        reject(value);
                    } catch (e) {
                        reject(e);
                    }
                });
                break;
            case Container.RESOLVED:
                try {
                    const value = onResolved(this.value);
                    resolve(value);
                } catch (e) {
                    reject(e);
                }
                break;
            case Container.REJECTED:
                try {
                    const value = onRejected(this.reason);
                    resolve(value);
                } catch (e) {
                    reject(e);
                }
                break;
        }
    }
}

2.then函数的调用

  • 哪里调用:容器外部调用
  • 调用时机:容器实例构造之后。注意:在容器实例构造好之后,异步向容器中写入状态和数据之前的这个时间差内,容器内的状态和数据还没准备好。

容器外部对then函数的调用示例:

p1.then((value) => {
  console.log(value)
}, (reason) => {
  console.log(reason)
})

五:实现then函数的链式调用

  • then函数不支持链式调用的缺陷:then函数没有返回值,它虽然可以多次调用,但它只能根据容器状态和数据做一步操作,无法实现操作的流水线式处理,如此依然无法解决回调函数循环嵌套的问题。
  • then函数支持链式调用的优点:then函数根据回调函数的执行结果构造返回一个新的容器,这样外部就可以对then函数调用返回的容器状态和数据再做下一步操作。如此反复,就可以实现操作的流水线式处理解决回调函数嵌套问题

1.修改then函数的定义以支持链式调用

then方法返回一个容器对象,这个新容器对象管理的状态和数据由回调函数的执行结果来决定。
  • 如果回调函数运行返回一个值,则调用resolve方法初始化这个新容器的状态和数据。
  • 如果回调函数运行返回一个容器并且不是新容器本身,则调用这个回调函数返回的容器的then方法来读取其内部管理的状态和数据,而后调用新容器的resolve或者reject方法来初始化新容器的状态和数据。这个先读取后写入的具体通信过程,是通过把新容器的resolve或者reject方法作为回调函数返回容器的下一步操作(也就是分别作为then方法的参数onResolved和onRejected)来实现的。
  • 如果回调函数运行返回新容器本身,如果不做特殊处理,则会造成读取新容器的状态和数据作为新容器的状态和数据的现象,永远都不会结束,陷入无限循环。这个问题的解决方案,就是让回调函数的运行(新容器的excutor)作为异步任务放入任务队列中,这样做的好处在初始化这个新容器的状态和数据之前,就可以拿到这个新容器对象进行判断

容器内部对then函数支持链式调用的实现示例:

class Container {
    constructor(excutor) {}

    resolve = value => {}
    reject = reason => {}

    onResolvedTodoList = [];
    onRejectedTodoList = [];

    then(onResolved, onRejected) {
        onResolved = onResolved ? onResolved : value => value;
        onRejected = onRejected ? onRejected : reason => { throw reason };
        let containerBack = new Container((resolve, reject) => {
            switch (this.state) {
                case Container.PENDING:
                    this.onResolvedTodoList.push(() => {
                        setTimeout(() => {
                            try {
                                const value = onResolved(this.value);
                                resolveContainer(containerBack, value, resolve, reject);
                            } catch (e) {
                                reject(e);
                            }
                        })
                    });
                    this.onRejectedTodoList.push(() => {
                        setTimeout(function () {
                            try {
                                const value = onRejected(this.reason);
                                resolveContainer(containerBack, value, resolve, reject);
                            } catch (e) {
                                reject(e);
                            }
                        })
                    });
                    break;
                case Container.RESOLVED:
                    setTimeout(() => {
                        try {
                            const value = onResolved(this.value);
                            resolveContainer(containerBack, value, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    })
                    break;
                case Container.REJECTED:
                    setTimeout(function () {
                        try {
                            const value = onRejected(this.reason);
                            resolveContainer(containerBack, value, resolve, reject);
                        } catch (e) {
                            reject(e);
                        }
                    })
                    break;
            }
        });
        return containerBack
    }
}

function resolveContainer(containerBack, value, resolve, reject) {
    if (!(value instanceof Container)) {
      resolve(value)
    } else {
      if (value === containerBack) {
        reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
      } else {
        value.then(resolve, reject);
      }
    }
  }

2.then函数的链式调用

  • then链式调用的目的:在调用then方法之后,调用者可以根据回调函数的执行结果再做下一步操作,也就是监听回调函数运行结果的回调函数

容器外部对then函数的链式调用示例:

const p2 = p1.then((value) => {
  console.log(value)
}, (reason) => {
  console.log(reason)
})
p2.then((value) => {
  console.log('p2', value)
}, (reason) => {
  console.log('p2', reason)
}).then((value) => {
  console.log('p3', value)
}, (reason) => {
  console.log('p3', reason)
})

本文结束,观众老爷慢走,欢迎下次光临。