第六章 :Promise & Async/await & Generators—上-灵析社区

懒人学前端

一、如何模拟实现 Promise?

为什么需要 Promise?

为什么我们需要 Promise, 最直接的答案就是它更直观、好用。Promise 出现之前,我们使用事件和回调。我们创建并加载一个脚本文件, 监听 onload 事件, 执行对应的回调函数。如下所示:

const loadScript = (src, callback) => {
	// 创建一个 <script> 标签, 并将其添加到页面
	let script = document.createElement("script");
	script.src = src;

	script.onload = () => callback(null, script);
	script.onerror = () => callback(new Error(`Script load error for ${src}`));
	document.head.append(script);
};

loadScript(
	"https://unpkg.com/vue@3/dist/vue.global.js",
	function (error, script) {
		if (error) {
			console.log(error);
		} else {
			console.log("loading Vue"); // "loading Vue"
		}
	}
);

此处只加载一个脚本文件,回调看起来还比较清晰,但是如果我们想加载多个脚本文件呢?如下所示:

loadScript(
	"https://unpkg.com/vue@3/dist/vue.global.js",
	function (error, script) {
		console.log("loading Vue"); // "loading Vue"

		loadScript(
			"https://unpkg.com/react@18/umd/react.development.js",
			function (error, script) {
				console.log("loading React"); // "loading React"
			}
		);
	}
);

在上面的代码中:我们加载 Vue, 如果没有发生错误,我们加载 React......

随着调用嵌套的增加,代码层次变得更深,维护难度也随之增加,这就被称为“回调地狱”。此时 Promise 闪亮登场了,它可以很好地解决回调地狱问题,且代码简单易读。

如果改成 Promise:

const loadScript = (src) => {
	return new Promise(function (resolve, reject) {
		let script = document.createElement("script");
		script.src = src;

		script.onload = () => resolve(script);
		script.onerror = () => reject(new Error(`Script load error for ${src}`));

		document.head.append(script);
	});
};

loadScript("https://unpkg.com/vue@3/dist/vue.global.js")
	.then((script) => {
		console.log("loading Vue"); // "loading Vue"
		return loadScript("https://unpkg.com/react@18/umd/react.development.js");
	})
	.then((script) => console.log("loading React")) // "loading React"
	.catch((error) => console.log(error));

Promise 介绍

Promise 对象用于表示一个异步操作的最终完成(或失败)及其结果值。— MDN

它的构造器语法如下:

let promise = new Promise(function(resolve, reject) {
    // executor 
})

executor 会自动运行并尝试执行。尝试结束后,如果成功则调用 resolve,如果出现 error 则调用 reject

所以,executor 最终将 Promise 移至以下状态之一:

由图可知,new Promise 构造器返回的 pending 对象具有以下内部属性:

  • state:初始值是 pendingresolve 调用后变为 fulfilled, reject 调用之后变为 rejected
  • result:初始值是 undefinedresolve(value) 调用后变为 value reject(error) 调用时变为 error

Promise 支持链式调用,.then.catch .finally 方法均支持链式调用。其中,.then 方法接收两个参数,第一个参数处理已决议状态(fulfilled)的回调函数,第二个参数则处理已经拒绝(rejected)的回调函数。每一个 .then() 方法返回的是一个新生成的 Promise 对象,这个对象可被用作链式调用。如:

const promise = doSomething();
const promise2 = promise.then(successCallback, failureCallback);

模拟实现 Promise

第一版:简易版 Promise

我们的第一版 Promise 具有以下功能:

  • 创建一个 Promise
  • resolve reject 一个 Promise,仅可敲定一次
  • .then() 方法接收回调函数,满足无论 Promise 的状态是否已经敲定,都需要单独处理回调函数
  • .then() 方法暂不支持链式调用,我们将在第二版实现这个功能

MyPromise 是一个类,它有下面三个原型方法:

  • MyPromise.prototype.resolve(value)
  • MyPromise.prototype.reject(reason)
  • MyPromise.prototype.then(onFulfilled, onRejected)

希望可以如下使用我们实现的 Promise,也可作为我们稍后的测试代码:


// .resolve() 先于 .then() 执行, 无论它们代码位置的先后
// .resolve() 在 .then() 前
const tp1 = new MyPromise();
tp1.resolve("abc");
tp1.then((value) => {
	console.log(value === "abc"); // true
});
// .then() 在 .resolve() 前
const tp2 = new MyPromise();
tp2.then((value) => {
	console.log(value === "def"); // true
});
tp2.resolve("def");

class MyPromise {
	// 处理 .then 中状态是 pending 的所有任务,加入任务数组中
	_fulfillmentTasks = [];
	_rejectionTasks = [];
	_preomiseResult = undefined;
	_promiseState = "pending";

	then(onFulfilled, onRejected) {
		const fulfillmentTask = () => {
			if (typeof onFulfilled === "function") {
				onFulfilled(this._preomiseResult);
			}
		};
		const rejectionTask = () => {
			if (typeof onRejected === "function") {
				onRejected(this._preomiseResult);
			}
		};
		switch (this._promiseState) {
			case "pending":
				this._fulfillmentTasks.push(fulfillmentTask);
				this._rejectionTasks.push(rejectionTask);
			case "fulfilled":
				addToTaskQueue(fulfillmentTask);
				break;
			case "rejected":
				addToTaskQueue(rejectionTask);
			default:
				throw new Error();
		}
	}

	resolve(value) {
		if (this._promiseState !== "pending") return this;
		this._promiseState = "fulfilled";
		this._preomiseResult = value;
		this._clearAndEnqueueTask(this._fulfillmentTasks);
		return this; // 可以链式调用
	}

	reject(error) {
		if (this._promiseState !== "pending") return this;
		this._promiseState = "rejected";
		thsi._promiseResult = error;
		this._clearAndEnqueueTask(this._rejectionTasks);
		return this; // 可以链式调用
	}

	_clearAndEnqueueTask(tasks) {
		this._fulfillmentTasks = undefined;
		this._rejectionTasks = undefined;
		tasks.map(addToTaskQueue);
	}
}

function addToTaskQueue(task) {
	setTimeout(task, 0);
}

resolve()

resolve() 方法的工作原理如下:

  • 如果 Promise 已经敲定,即 Promise 已经兑现(fulfilled)或已拒绝(rejected),什么都不做。因为一个 Promise 仅可以被敲定一次。
  • 改变 Promise 的状态: 由 pending 变为 fulfilled;并将结果缓存到 this._promiseResult 中。
  • 所有的回调函数都加入队列中,用 setTimeout() 模拟异步执行。

reject()

原理同 resolve(),只不过它处理的是 Promise 的 rejected 情况。

then()

then() 方法需要处理下面两种情况:

  • 如果 Promise 是 pending 状态,需要把它封装成待处理的函数: onFulfilled onRejected,添加到对应的任务数组中(_fulfillmentTasks_rejectionTasks),它们将在 Promise 已经敲定后再执行。
  • 如果 Promise 已经敲定,onFulfilledonRejcted 可以直接触发。

Promise 必须始终是异步敲定,这就是我们为什么不直接执行 task,而是添加到事件循环(event loop)的任务队列中。真正的 Promise 不是使用 setTimeout 模拟的,它使用的是微任务(microtasks)。

第二版:实现链式调用

我们将实现链式调用,先看一段代码:

new MyPromise()
	.resolve("result1")
	.then((x) => {
		console.log(x === "result1"); // true
		return "result2";
	})
	.then((x) => {
		console.log(x === "result2"); // true
		return new Error("error1");
	})
	.then((x) => {
		console.log(x.message === "error1"); // true
	});

在上面的例子中:

  • 第一个 .then() 方法在传递了一个箭头函数,返回一个值 result2
  • 第二个 .then() 方法在中接收了 result2

.then() 方法如何实现链式调用呢?

  • .then() 返回一个 Promise,无论前一个 Promise 返回的是 onFullfilled 还是 onRejected,它都接收其状态。
  • 如果 onFulfilled onRejected 都缺失,都通过 .then() 方法返回给该 Promise。

下面的图更为直观:

因此,我们将 .then() 方法改进如下:

then(onFulfilled, onRejcted) {
    const resultPromise = new MyPromise(); // new

    const fulfillmentTask = () => {
        if (typeof onFulfilled === "function") {
            const returned = onFulfilled(this._preomiseResult);
            resultPromise.resolve(returned);
        } else {
            resultPromise.resolve(this._promiseResult); // new
        }
    };

    const rejectionTask = () => {
        if (typeof onRejected === "function") {
            const returned = onRejected(this._promiseResult);
            resultPromise.resolve(returned); // new
        } else {
            resultPromise.reject(this._promiseResult);
        }
    };

    // ...
    return resultPromise; // new
}

.then() 返回一个新的 Promise,此外,我们需要关注:

  • fulfillmentTask 会有两种不同表现:

如果提供了 onFullfilled 函数,调用它并将其结果传递给 resultPromise resolve 方法。

如果没提供,使用当前 Promise 的敲定结果 this._promiseResult 传递给 resultPromise 的 resolve 方法。

  • rejectionTask 也有两种表现:

如果提供了 onRejected 函数,调用它并将其结果传递给 resultPromise resolve 方法。注意:resultPromise 并没有被拒绝,我们将在后文详细解释。

如果没提供,使用当前 Promise 的敲定结果 this._promiseResult 传递给 resultPromise resolve 方法。

第三版:处理 thenable

thenable 是一个对象,它具有方法 .then,被当作一个 Promise 对待。.then() 方法中还可以创建并返回一个 Promise,在这种情况下,其他的处理程序将等待它被敲定后再获得其结果。

asyncFunc1()
.then((result) => {
    console.log(result1 === "Result of asyncFun1()"); // true
    return asyncFunc2(); // A
})
.then((result2Promise) => {
    result2Promise
        .then((result2) => {  // B
            console.log(result2 === "Result of asyncFun2"); // true
        })
})

在 A 处,如果返回了一个 Promise,然后在第二个 .then() 中作为参数传入,并敲定了这个 Promise。官方的 Promise 可以直接敲定嵌套的 Promise,如下面的代码:

asyncFunc1()
.then((result) => {
    console.log(result1 === "Result of asyncFun1()"); // true
    return asyncFunc2(); // A
})
.then((result) => {
    // result2 是一个已兑现的结果,而非 Promise
    console.log(result2 === "Result of asyncFun2"); // true
})

我们将这样处理 then() 方法中返回的 thenable,如下图:

  • 如果第一个 Promise 处理的是一个 thenable 对象 x1,它锁定 x1 并且用 x1 敲定的结果作为敲定结果。
  • 如果第一个 Promise 处理的不是 thenable 对象,则像之前一样。

我们可以这样判断 thenable 对象:

function isThenable(value) {
	return (
		typeof value === "object" &&
		value !== null &&
		typeof value.then === "function"
	);
}

我们再来完善 .resolve() 方法:

resolve(value) {
    if (this._alreadyResolved) return this;
    this._alreadyResolved = true;

    if (isThenable(value)) {
        value.then(
            (result) => this._doFulfill(result),
            (error) => this._doReject(error);
        )
    } else {
        this._doFulfill(value);
    }

    return this; 
}

_doFulfill(value) { // [new]
    console.log(!isThenable(value)) // true
    this._promiseState = "fulfilled";
    this._promiseResult = value;
    this._clearAndEnqueueTasks(this._fulfillmentTasks);
}

this._alreadyResolved 是为了锁定 Promise 的状态的。返回一个已敲定状态的 Promise,那么 then() 返回的 Promise 的状态也会是已敲定。返回已拒绝状态同理。

第四版:处理错误

我们希望错误也能够得到正确处理:

new MyPromise()
  .resolve("a")
  .then((value) => {
    console.log(value === "a"); // true
    throw "b"; // 抛出一个错误
  })
  .catch((error) => {
    console.log(error === "b"); // true
  })

如果我们要处理错误,只需要在 .then() 方法中添加相关代码即可:

const fulfillmentTask = () => {
    if (typeof onFulfilled === "function") {
        this._runReactionSafely(resultPromise, onFulfilled); // [new]
    } else {
        // `onFulfilled` 没有传
        // => 我们需要传已敲定的结果
        resultPromise.resolve(this._promiseResult);
    }  
};

_runReactionSafely(resultPromise, reaction) { // [new]
    try {
        const returned = reaction(this._promiseResult);
        resultPromise.resolve(returned);
    } catch (e) {
        resultPromise.reject(e);
    }
}

错误处理过程如图所示:

总结

至此,我们模拟实现了 Promise,包含 .then().resolve() .reject() 方法的实现。

class MyPromise {
	_fulfillmentTasks = [];
	_rejectionTasks = [];
	_promiseResult = undefined;
	_promiseState = "pending";
	_alreadyResolved = false;

	then(onFulfilled, onRejected) {
		const resultPromise = new MyPromise();

		const fulfillmentTask = () => {
			if (typeof onFulfilled === "function") {
				this._runReactionSafely(resultPromise, onFulfilled);
			} else {
				resultPromise.resolve(this._promiseResult);
			}
		};

		const rejectionTask = () => {
			if (typeof onRejected === "function") {
				this._runReactionSafely(resultPromise, onRejected);
			} else {
				resultPromise.reject(this._promiseResult);
			}
		};

		switch (this._promiseState) {
			case "pending":
				this._fulfillmentTasks.push(fulfillmentTask);
				this._rejectionTasks.push(rejectionTask);
				break;
			case "fulfilled":
				addToTaskQueue(fulfillmentTask);
				break;
			case "rejected":
				addToTaskQueue(rejectionTask);
				break;
			default:
				throw new Error();
		}
		return resultPromise;
	}

	catch(onRejected) {
		return this.then(null, onRejected);
	}

	_runReactionSafely(resultPromise, reaction) {
		try {
			const returned = reaction(this._promiseResult);
			resultPromise.resolve(returned);
		} catch (e) {
			resultPromise.reject(e);
		}
	}

	resolve(value) {
		if (this._alreadyResolved) return this;
		this._alreadyResolved = true;

		if (isThenable(value)) {
			value.then(
				(result) => this._doFulfill(result),
				(error) => this._doReject(error)
			);
		} else {
			this._doFulfill(value);
		}

		return this;
	}

	_doFulfill(value) {
		this._promiseState = "fulfilled";
		this._promiseResult = value;
		this._clearAndEnqueueTasks(this._fulfillmentTasks);
	}

	reject(error) {
		if (this._alreadyResolved) return this;
		this._alreadyResolved = true;
		this._doReject(error);
		return this;
	}

	_doReject(error) {
		this._promiseState = "rejected";
		this._promiseResult = error;
		this._clearAndEnqueueTasks(this._rejectionTasks);
	}

	_clearAndEnqueueTasks(tasks) {
		this._fulfillmentTasks = undefined;
		this._rejectionTasks = undefined;
		tasks.map(addToTaskQueue);
	}
}

function isThenable(value) {
	return (
		typeof value === "object" &&
		value !== null &&
		typeof value.then === "function"
	);
}

function addToTaskQueue(task) {
	setTimeout(task, 0);
}

小练习

如何使用 Promise 封装 XMLHttpRequest?

答案:参考如下


function load(url) {
	return new Promise(function (resolve, reject) {
		const request = new XMLHttpRequest();
		request.onreadystatechange = function () {
			if (this.readyState === 4 && this.status == 200) {
				resolve(this.response);
			} else {
				reject(this.status);
			}
		};
		request.open("GET", url, true);
		request.send();
	});
}

// test
load("testUrl")
	.then((response) => {
		const result = JSON.parse(response);
		// ...
	})
	.catch((error) => {
		// ...
	});


阅读量:2021

点赞量:0

收藏量:0