手写Promise--7k字从0开始看完必会
概述
现如今,Promise
可谓大行其道。如果不太理解Promise
的相关特性,你很难阅读别人的代码,也无法优雅的进行异步编程。
Promise的历史渊源
其实Promise
的概念很早就诞生了,甚至比javascript
年纪还大。在javascript
中,最早的Promise
机制出现在JQuery
中,但是还没形成相关规范。后来CommonJS
制定的Promise/A
规范日渐流行。到了2012年,Promise/A+
组织制定了同名的Promise
规范:Promise/A+
规范。ECMAScript
规范中关于Promise
的内容遵循的就是Promise/A+
规范。所以,我们就按照Promise/A+
规范,来实现我们自定义的Promise
。如果我们能通过Promise/A+
规范提供的测试用例,则说明,我们自定义的Promise
也成为了Promise/A+
规范的一种实现。
什么是Promise/A+
规范
Promise/A+
规范
所谓规范,实际上是一个指南,指导你如何实现。所以说他并不是具体的实现代码。这个规范其实不算长,所以值得大家一读,可以让我们对ECMAScript
中的Promise
有更深层次的理解。
Promise主要解决了什么问题
以往的异步编程,为了获取到异步任务的结果,我们通常是使用回调函数的形式。当异步任务有了结果,就调用这个函数将异步任务的结果交给回调函数进行处理。但是如果异步任务嵌套过多,就会造成回调地狱的情况。非常不利于代码的阅读和维护。Promise
就是用来解决这个问题的,通过Promise
的链式调用,我们可以优雅的进行异步编程。将嵌套过多的回调函数扁平化,链路化。
手写Promise
手写Promise
至少需要我们对Promise
展现的特性有足够多的了解,只有充分的了解,才能更好的实现它。经历这个过程,你至少能有以下收获:
- 为什么
Promise
可以进行链式调用。 - 为什么
Promise
可以异常穿透。 - 为什么传递给
then
的参数会是异步任务。 Promise.all
,Promise.race
是如何实现的。 .....
接下来,让我们来严格按照Promise/A+
规范来实现我们自己的Promise
。
实现一个最基本的Promise
类
我们知道,Promise
是一个类,用来创建一个Promise
实例。我们需要传递一个执行器函数。
/*
* @Description: implement Promise by myself.
*/
enum PromiseStateType {
pending = "pending",
fulfilled = "fulfilled",
rejected = "rejected",
}
//@ts-ignore
class Promise {
//Promise实例的状态。对应ECMAScript中的Promise实例的内部属性:[[PromiseState]]
private state: PromiseStateType = PromiseStateType.pending;
//履行Promise时的value。
private value: any = undefined;
//履行Promise时的reason。
private reason: any = undefined;
//二者共同组成了ECMAScript中的Promise实例的内部属性:[[PromiseResult]]
constructor(
executor: (
resolve: (value: unknown) => void,
reject: (reason: unknown) => void
) => void
) {
let resolve: (value: unknown) => void = (value: any) => {
//调用resolve后,将Promise的状态改为fulfilled,Promise的结果记为value.
if (this.state === PromiseStateType.pending) {
this.state = PromiseStateType.fulfilled;
this.value = value;
}
};
let reject: (reason: unknown) => void = (reason: any) => {
//调用resolve后,将Promise的状态改为rejected,Promise的拒绝原因记为reason.
if (this.state === PromiseStateType.pending) {
this.state = PromiseStateType.rejected;
this.reason = reason;
}
};
if (typeof executor === "function") {
try {
//执行器函数是同步执行的。
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
}
}
看懂这里,你至少需掌握了以下的知识点:(规范中均有提及)
Promise
实例存在三种状态:pending
(初始态),fulfilled
和rejected
。一个实例只能进行一次状态变更:要么是从pending
到fulfilled
,要么是从pending
到rejected
.value
代表履行Promise
(承诺或期约)的结果,当状态从pending
到fulfilled
时进行填充。
reason
代表拒绝Promise
的原因,当状态从pending
到rejected
时进行答复。二者其实就构成了ECMAScript
中Promise
实例中的内部属性[[PromiseResult]]
的值。
- 创建实例的时候,需要传递一个函数,我们称为执行器函数。这个函数存在两个形式参数,并且这两个形式参数也是函数,形式参数对应的实际参数是
Promise
内部提供的,交由你去调用,这两个函数一个叫resolve
,用来将Promise
的状态转为fulfilled
,并填充结果value
。一个叫reject
,用来将Promise
的状态转为rejected
,并答复原因reject
。 - 并且执行器函数
executor
的调用是在constructor
中进行的。所以执行器函数是同步执行的。 - 当执行器函数执行过程中,主动抛出了错误,这个时候应该返回一个失败的
Promise
实例,并且reason
应该为抛出的错误err
。所以,我这里使用了一个try...catch
的结构进行包裹,进行错误的捕获工作。
测试一下
let p1 = new Promise(resolve => {
resolve(1);
});
let p2 = new Promise((resolve, reject) => {
reject(1);
});
let p3 = new Promise((resolve, reject) => {});
目前来讲,我们已经完成了我们第一个小目标,完成了
Promise
类的大致流程的搭建。
实现基本态then
方法
then
方法可以说是Promise
链式调用的灵魂,其他方法如catch
,finally
都是then
方法的变种,所以关于它的实现至关重要。
then(onFulfilled?: (value?: any) => any, onRejected?: (reason?: any) => any) {
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : (value) => value;
onRejected =
typeof onRejected === "function"
? onRejected
: (reason) => {
throw reason;
};
if (this.state === PromiseStateType.fulfilled) {
simulateMicroTask(() => {
onFulfilled!(this.value);
});
} else if (this.state === PromiseStateType.rejected) {
simulateMicroTask(() => {
onRejected!(this.reason);
});
} else {
this.onFulfilledCallbacks.push(() => {
onFulfilled!(this.value);
});
this.onRejectedCallbacks.push(() => {
onRejected!(this.reason);
});
}
}
我们知道,then
方法可以接收两个参数,都是函数:
- 一个是
fulfilled
状态下的回调函数onFulfilled
,它接收fulfilled
状态下的结果,也就是我们前面提到的value
。 - 一个是
rejected
状态下的回调函数onRejected
, 它接收rejected
状态下的原因,也就是我们前面提到的reason
。
注意千万不要和resolve
,reject
这两个函数弄混了。他们四个函数长得确实有点像。因为存在一定的关联性。但是onFulfilled
和onRejected
是用于是状态变更下的回调。而resolve
,reject
是用于变更状态。
有了这点知识,我们再来回顾上面的代码,首先,我们对传入的参数进行了一个预处理:
//当不是函数时,初始化为(value) => value
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : (value) => value;
//当不是函数时,初始化为(reason) => { throw reason; }
onRejected =
typeof onRejected === "function"
? onRejected
: (reason) => {
throw reason;
};
可别小看这两行初始化的代码:对onFulfilled
的初始化是当then
方法无实参传入,或者实参不是函数情况的处理。对onRejected
的初始化是Promise能进行异常穿透的根本原因!对于这两句话的理解可能还得结合后面的处理。但是我们可以先举两个小🌰:
//在浏览器中,这样也会返回一个fulfilled状态的Promise实例。
Promise.resolve(1).then();
//Promise的链式调用中,我们往往只需要在最后面进行异常的捕获(catch)就行了。这也就是我们常说的异常穿透机制。
//这个就是因为我们对onRejected的处理。
Promise.reject(1).then(() => {}).then(() => {}).then(() => {}).catch(() => {})
下面的一系列if/else
的逻辑无非在表达三件事:
- 当我们调用
then
方法时,Promise
实例已经是fulfill
状态的时候,我们是不是直接调用onFulfill
函数就行了。 - 当我们调用
then
方法时,Promise
实例已经是reject
状态的时候,我们是不是直接调用onReject
函数就行了。 - 当我们调用
pending
时,Promise
实例仍是pending
的时候,我们是不是应该把回调函数都存起来在状态变更的时候,再进行调用呢!
上面三句话,可以说是Promise
的精髓之一了。这是一定要弄懂的。但是我们还有着些许的疑问:
疑问一:simulateMicroTask是什么
在调用onFulfilled
和onRejected
的时候,我们使用这个simulateMicroTask
函数进行了包裹。这是为了符合规范中,onFulfilled
和onRejected
属于异步任务的要求。当然规范其实并未限制异步任务是宏任务还是微任务。我们可以使用setTimeout
这类宏任务来实现它,也能使用MutationObserver
这类微任务来实现它。在这里,我采用的是在浏览器环境,使用MutationObserver
来实现,因为**ECMAScript
中的Promise
的回调函数就是表现为微任务的。在node
环境是用的是process.nextTick
。
const isBrowser = typeof window !== "undefined" ? true : false;
function simulateMicroTask(callback: () => void): void {
//浏览器环境 使用MutationObserver模拟微任务
if (isBrowser) {
let counter = 1;
const textNode = document.createTextNode(String(counter));
const mutationInstance = new MutationObserver(callback);
mutationInstance.observe(textNode, {
characterData: true,
});
textNode.data = String(counter + 1);
}
//node环境 使用 process.nextTick模拟微任务
else {
//@ts-ignore
process.nextTick(() => {
callback();
});
}
}
疑问二:pending状态时,回调函数保存到哪里
pending
状态时,我们需要保存回调函数。那我们应该保存在哪里呢?答:保存在实例对象上。所以我们需要在上面的构造函数中新增两行代码:
疑问三,保存的回调函数数组的执行时机在何时
状态变更之时,便是我们回调函数执行之日。那我们会在哪里改变状态呢?答:resolve
和reject
中。所以我们需要重新修改下resolve
和reject
的代码:至此,我们已经实现了基本态的
Promise
。
实现进化态then
方法
进化态then
方法,主要目标是实现Promise
的链式调用。首先,小问一个问题:为什么Promise
可以进行链式调用?原因很简单,就是因为Promise.then
方法本身会返回一个Promise
实例,这个Promise
实例的状态和结果跟回调函数onResolve
和onRejected
的执行情况息息相关。根据我们上面所描述的,我们重新设计一下then
方法:这个地方就是定义
then
方法会返回一个Promise
实例。实例的结果和状态取决于回调函数的执行情况。图中有四个回调函数的调用都被try...catch
结构所包裹。这是因为规范规定:如果回调函数调用过程中,抛出错误,就应该返回失败的**Promise**
,失败原因为抛出的错误。当回调函数正常执行时,应该以回调函数的返回值作为
resolve
函数的实参,也就是作为Promise
实例的结果value
。举个🌰:
new Promise((resolve, reject) => {
resolve(1);
}).then(res => {
return 2;
});
//这个链式调用的最终结果是返回一个成功的Promise实例,并且成功的结果是2.
实现究极形态then方法(实则优化resolve
方法)
首先说,这段逻辑属于Promise中很绕的一部分,所以很多东西只可意会不可言传。建议先多看规范。
现在我们可以已经可以实现Promise
的链式调用了。但是还存在一个严重的问题:
new Promise((resolve, reject) => {
resolve(1);
}).then(() => {
return new Promise((resolve, reject) => {
resolve(1);
});
})
当我们的回调函数onReject
和onFulfilled
返回一个Promise
实例的时候:现在我们的Promise
实现方案会将这个Promise
作为then
方法返回的Promise
实例的value
值。但是实际上,规范中其实对这种情况有所描述:
这个地方我有必要说明一下,这个地方我没有按照规范的逻辑逻辑走(规范并没有说直接对回调函数的执行结果调用resolve
方法,这是我自己的思路。)。首先说规范的思路:
- 拿到回调函数
onReject
和onFulfilled
的返回值x
. - 拿到
x
的值,执行这个方法:**[[Resolve]](promise2, x)**
。这个方法是一个伪方法。也就是说他没有具体的实现代码,只提供思路。 - 这个伪方法其实很简单,就是对回调函数的返回值进行分类讨论:
:::info
- 情况一,
promise
和x
指向同一个对象的时候,报错,promise
就是then
方法的返回值。x
就是回调函数的返回结果。两者一致时报错。这就是一种套娃的情况。我先表示一下什么情况下会出现这种情况: :::
let x = new Promise((resolve, reject) => {
resolve(1);
}).then(() => x);
:::info
-
情况二,当
x
是一个Promise
对象的时候,应该以这个Promise
对象的结果和状态作为最终then
方法返回的Promise
实例的结果和状态。 ::: :::info -
情况三,就比较饶了。当
return
的结果是一个thenable
结构的时候。应该将resolvePromise
方法和rejectPromise
方法传递给它:- 如果
rejectPromise
方法被调用,则then
方法返回一个失败的Promise
方法。 - 如果
resolvePromise
方法被调用,则对拿到的值又走一遍[[Resolve]](promise2, x)
的逻辑。实际上是形成了一种递归调用。(这地方很绕很绕,一开始是懵的,是很正常的,因为我的表达能力有限。) - 如果调用过程,报错,则
then
方法返回失败的Promise
方法。 ::: thenable的含义这里出现了一个概念:thenable
。所谓thenable
的含义就是对象或者函数如果实现了then
方法这个接口,就属于thenable
。可见,Promise
对象一定是thenable
。 :::info
- 如果
-
情况四,如果
x
不属于上面的情况,则then
方法以x
为结果,返回成功的Promise
实例。 :::
:::info
上面的逻辑,可以说很绕很绕。但是我觉得上面只在做一件事儿:当返回值是thenable
的时候(Promise
也是thenable
),对其做解包。(请理解我这句话。)我们要不断地取出thenable
结构中的then
方法的第一个参数(这个参数是个函数),接收到的值,直到接收到的值不是一个thenable
结构,则说明这个值是then
方法返回的Promise
实例的状态才会真正确定,并且Promise
的结果正好该值。
举个🌰:
new Promise((resolve, reject) => {
resolve(1);
}).then(() => {
return {
then: (a) => {
a({
then: (a) => {
a(1);
}
})
}
}
})
这个then
方法返回的Promise
实例的最终结果value
应该是1
,看懂这个应该会对你理解上面描述的这个[[Resolve]](promise2, x)
方法会有所帮助。
总而言之,[[Resolve]](promise2, x)
在进行一个解包的操作。我现在按照它的思路来实现一下这个伪方法(我见网上也有人是这样写的):
then(onFulfilled?: (value?: any) => any, onRejected?: (reason?: any) => any) {
......
let promise2 = new Promise((resolve, reject) => {
if (this.state === PromiseStateType.fulfilled) {
simulateMicroTask(() => {
try {
let x = (onFulfilled!(this.value));
promiseResolutionProcedure(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
}
});
......
}
//这个函数就是对那个[[Resolve]](promise2, x)伪方法的具体实现。网上别人的思路。
function promiseResolutionProcedure (promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
let called;
if ((typeof x === 'object' && x != null) || typeof x === 'function') {
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, r => {
if (called) return;
called = true;
reject(r);
});
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e)
}
} else {
resolve(x)
}
}
但是,我们来思考一个问题,解包(自创的名词,规范中没有这个概念。)这个动作在这里做真的好吗。因为使用Promise
的同学应该都知道,resolve
函数也表现了解包这种行为:
new Promise(resolve => {
resolve(new Promise(resolve => resolve(1)))
})
那我们为什么不把解包这个动作放到
resolve
函数中呢?所以我这样去实现它:
//废弃
// try {
// let x = (onFulfilled!(this.value));
// promiseResolutionProcedure(promise2, x, resolve, reject);
// } catch (err) {
// reject(err);
// }
//新方案
try {
resolve(onFulfilled!(this.value));
} catch (err) {
reject(err);
}
那现在我们需要在resolve
中实现解包,所以我们不得不重新改造一下resolve
函数的实现:
:::info
- 这里,我十分巧妙的使用了
**this**
来处理了情况一,因为**constructor**
中的**this**
就是指向的构造出来的**Promise**
实例。 - 第二点,同样也巧妙,我这里并没有对
**Promise**
和其他**thenable**
结构进行区分,因为我认为所有**thenable**
的解包逻辑是一样的,根本不存在任何区别。我的处理逻辑很简单,也是通过递归。当我发现你是一个**thenable**
结构的时候,我就会一直派**resolve**
函数去拿到你真正的结果。只有你不是**thenable**
结构的时候,**resolve**
才能发挥其真正的作用,那就是改变**Promise**
实例的状态,填充结果**value**
。请务必注意我**return**
语句的使用。 - 为什么
then
方法要进行try...catch
?如果value
是Promise
的时候,是不可能会有同步的错误被抛出从而被try...catch...
捕获到的。但是我们得考虑自定义的then
方法的情况:
:::
new Promise(resolve => {
resolve({
then() {
throw 1;
}
})
})
:::info
- 为什么要使用
call
调用then
方法,而不是直接使用value.then
。这二者调用时,this
指向是一样的,为什么要脱裤子放屁,多此一举呢。
事实上,每次属性的访问都是有可能造成副作用的。比如当他是一个访问器属性的时候,这个时候,必然会执行一次get
。如果使用value.then
就会又造成一次get
方法的调用。
:::
let obj = {};
let number = 0;
Object.defineProperty(obj, 'then', {
get() {
//这就是副作用!!!
number++;
return function() {
throw 1;
}
}
})
new Promise((resolve, reject) => {
then() {
throw 1;
}
})
完善resolve/reject方法
我们搭建的Promise
方法其实已经完成了80%
的工作,但是它还存在一个严重的缺陷:那就是规范中提到的这句话:说人话就是,如果我们的
resolve
方法被调用过之后,之后所有的reject
和resolve
方法的调用将会被忽略。可能有同学会想,我们好像已经实现了这个效果:
new Promise((resolve, reject) => {
resolve(1);
resolve(2);
resolve(3);
reject(4);
})
这个结果最终会是fulfilled
状态的Promise
实例,并且是结果value
是1
。
:::info
但是,我们思考一个问题,它是怎么实现忽略后续执行的resolve/reject
方法的。其实后续的resolve/reject
方法还是按部就班的被执行了,只是第一次执行的resolve
方法改变了Promise
实例的状态,导致后续的执行并没有产生实际的效果。总而言之,它是通过状态的改变,来变相规避了重复调用的问题。
但是,考虑一种情景,如果状态的改变不是同步执行的呢?这不就会出问题了吗?举个🌰:
new Promise((resolve, reject) => {
resolve( new Promise((res) => {
res(1);
}));
reject(2);
})
- 当我们
resolve
一个Promise
的时候,我们会调用它的then
方法,然后将resolve
和reject
传递给then
方法。但是then
调用回调函数是异步的。所以状态的改变不会立即到来。 - 这时同步代码继续执行,
reject(2)
,这时候Promise
的状态被这行代码修改掉了。即使前面的异步任务执行也无力回天了,因为状态已经被改变了。
最后的表现效果为reject
方法发挥了作用。而前面的resolve
方法被覆盖了。这显然不是规范想要达到的效果。那我们应该重新思考一下了:
明确目标:我们要达到的目标是resolve
/reject
方法只要有一个被调用,其他调用resolve
/reject
都会被忽略。
我这里想到的思路是通过闭包来达到我们的目的:
function onceCall(
resolve: (value: unknown) => void,
reject: (reason: unknown) => void
): {
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
} {
let called = false;
return {
resolve(vlaue) {
if (!called) {
called = true;
resolve(vlaue);
}
},
reject(reason) {
if (!called) {
called = true;
reject(reason);
}
},
};
}
这里就是使用called
作为标志,只要暴露出来的resolve
,reject
方法任意一个被调用。就会导致标志置为true
,从而免疫一切后续调用。所以我们现在使用的resolve
和reject
要被onceCall
进行一次包裹,所以我们对原来的resolve
和reject
进行一次重命名:将
resolve
重命名为realResolve
,将reject
重命名为realReject
。然后对传入executor
函数中的resolve
和reject
先进行一次onceCall
包裹:此时,我们代码中用到的
resolve
和reject
都是被包裹后的。只有realResolve
和realReject
才是原始的。图中还对realResolve
方法进行了一些调整:
reject
改为realReject
这是因为
realResolve
被调用,则说明resolve
方法一定被调用了。那么called
已经为true
了。所以这时候调用reject
肯定是不起作用的,所以得调用realReject
。
- 又增加了一个
try...catch...
的结构,这个结构是为了捕获访问value.then
时,可能会抛出的异常,举个🌰:
let value = {};
Object.defineProperty(value, 'then', {
get() {
throw 1;
}
})
- 对传递给
then
方法的resolve
和reject
也进行了一次onceCall
的包裹。
其实对于Promise
实例来说,传递的resolve
和reject
不可能被重复调用。那onceCall
包裹的意义在哪里呢?这是因为要考虑thenable
结构的情况:
var thenableInstance = {
then(resolve, reject) {
resolve();
resolve();
reject();
}
}
既然会出现多次调用的情况,那么也同样面临之前描述的那种问题,所以仍然需要进行一次**onceCall**
的包裹。
验证Promise的规范性
我们前文说过,我们的自定义的Promise
会遵循Promise/A+
组织推出的Promise/A+
规范。为了验证大家的Promise
是否合乎规范。Promise/A+
组织也有相应的NPM
包来进行校验:promises-aplus-tests
。该npm
包中有872条测试用例,完全通过则证明合乎规范。
:::info
- 下载
npm
包,npm install promises-aplus-tests
- 改造我们的手写实现文件,文件尾部添加如下代码:
//@ts-ignore
Promise.defer = Promise.deferred = function () {
let dfd = {};
//@ts-ignore
dfd.promise = new Promise((resolve, reject) => {
//@ts-ignore
dfd.resolve = resolve;
//@ts-ignore
dfd.reject = reject;
});
return dfd;
};
//@ts-ignore
module.exports = Promise;
- 执行命令:
npx promises-aplus-tests <你的手写文件路径>
即可完成测试。
通过这个测试包,我们可以有效的排查我们未考虑到的情况,在整个代码的形成过程中,可见我其实考虑很多边界情况。这也是借助这个库帮助我完成的,不断优化代码逻辑。没有什么事情是一蹴而就的。
我已自测:
如果想参考我的代码,请点这里!代码提交的历史是和文章思路是保持一致的。不过我使用了typescript
,所以我经过了一次编译,关注promise/index.js
文件即可。
完善后续API
Promise.prototype.catch
catch(onRejected?: (reason?: any) => any) {
return this.then(undefined, onRejected);
}
Promise.protype.finally
finally(callback: () => void) {
this.then(
(value) => {
return new Promise((resolve) => {
resolve(callback());
}).then(() => value);
},
(reason) => {
return new Promise((resolve) => {
resolve(callback());
}).then(() => {
throw reason;
});
}
);
}
:::info
相对来讲,finally
的逻辑会有点绕:
- 当成功的
Promise
调用finally
方法时:- 如果
finally
方法的回调函数不抛出错误或返回错误的Promise
实例,则返回和当前成功Promise
状态和结果一致的Promise
。 - 如果
finally
方法的回调函数抛出错误或返回错误的Promise
实例,则返回状态为失败,结果为回调函数抛出的错误内容的Promise
。
- 如果
- 当失败的
Promise
调用finally
方法时:
Promise.resolve
static resolve(value: unknown) {
return new Promise((resolve, reject) => {
resolve(value);
});
}
Promise.reject
static reject(reason: unknown) {
return new Promise((resolve, reject) => {
reject(reason);
});
}
Promise.all
static all(list: any[]) {
return new Promise((resolve, reject) => {
//暂存成功数组元素的结果,务必和数组元素是一一对应关系
const results: any[] = [];
//记录成功状态的数量。作为标志。
let fulfilledCount: number = 0;
function fulfillPromise(value: unknown, index: number) {
results[index] = value;
if (++fulfilledCount === list.length) {
resolve(results);
}
}
for (let index = 0; index < list.length; index++) {
Promise.resolve(list[index]).then((value) => {
fulfillPromise(value, index);
}, reject);
}
});
}
主要思路就是每次数组元素成功的时候,进行判断是不是所有元素都成功了,是的话就返回成功的Promise
。
Promise.race
static race(list: any[]) {
return new Promise((resolve, reject) => {
for (let index = 0; index < list.length; index++) {
Promise.resolve(list[index]).then(resolve, reject);
}
});
}
写在最后
行文至此,身心俱疲。如有纰漏,不吝赐教。(后面的API硬敲的,没测hh~卒...)
转载自:https://juejin.cn/post/7133236163451551757