JS异步编程:Promise、Async/Await、setTimeout与事件循环机制

异步编程

在 JavaScript 这种单线程事件循环模型中,同步操作与异步操作是代码所要依赖的核心机制。

异步行为是为了优化因计算量大而时间长的操作。在等待其他操作完成时,即使运行其他指令,系统也能保持稳定。只要你不想为等待某个异步操作而阻塞线程执行,那么任何时候都可以使用。

以往的异步编程(setTimeout)

function double(value) { 
 setTimeout(() => setTimeout(console.log, 0, value * 2), 1000); 
} 
double(3); 
// 6(大约 1000 毫秒之后)

对这个例子而言,1000 毫秒之后,JavaScript 运行时会把回调函数推到自己的消息队列上去等待执行。推到队列之后,回调什么时候出列被执行对 JavaScript 代码就完全不可见了。double()函数在 setTimeout 成功调度异步操作之后会立即退出。

1.处理异步返回值

假设 setTimeout 操作会返回一个有用的值。有什么好办法把这个值传给需要它的地方?

=》给异步操作提供一个回调,这个回调中包含要使用异步返回值的代码(作为回调的参数)。

function double(value, callback) { 
 setTimeout(() => callback(value * 2), 1000); 
} 
double(3, (x) => console.log(`I was given: ${x}`)); 
// I was given: 6(大约 1000 毫秒之后)

这里的 setTimeout 调用告诉 JavaScript 运行时在 1000 毫秒之后把一个函数推到消息队列上。这个函数会由运行时负责异步调度执行。而位于函数闭包中的回调及其参数在异步执行时仍然是可用的。

2.失败处理

成功回调和失败回调:

function double(value, success, failure) { 
 setTimeout(() => { 
 try { 
 if (typeof value !== 'number') { 
 throw 'Must provide number as first argument'; 
 } 
 success(2 * value); 
 } catch (e) { 
 failure(e); 
 } 
 }, 1000); 
} 
const successCallback = (x) => console.log(`Success: ${x}`); 
const failureCallback = (e) => console.log(`Failure: ${e}`); 
double(3, successCallback, failureCallback); 
double('b', successCallback, failureCallback); 
// Success: 6(大约 1000 毫秒之后)
// Failure: Must provide number as first argument(大约 1000 毫秒之后)

这种模式已经不可取了,因为必须在初始化异步操作时定义回调。异步函数的返回值只在短时间内存在,只有预备好将这个短时间内存在的值作为参数的回调才能接收到它。

3.嵌套异步回调

如果异步返值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂。在实际的代码中,这
就要求嵌套回调:

function double(value, success, failure) { 
 setTimeout(() => { 
 try { 
 if (typeof value !== 'number') { 
 throw 'Must provide number as first argument'; 
 } 
 success(2 * value); 
 } catch (e) { 
 failure(e); 
 } 
 }, 1000);
  } 
const successCallback = (x) => { 
 double(x, (y) => console.log(`Success: ${y}`)); 
}; 
const failureCallback = (e) => console.log(`Failure: ${e}`); 
double(3, successCallback, failureCallback); 
// Success: 12(大约 1000 毫秒之后)

显然,随着代码越来越复杂,回调策略是不具有扩展性的。“回调地狱”这个称呼可谓名至实归。嵌套回调的代码维护起来就是噩梦。

Promise

成为了主导性的异步编程机制。

Promise 基础

ECMAScript 6 新增的引用类型 Promise,可以通过 new 操作符来实例化。

当通过new创建Promise实例时,需要传入一个回调函数,我们称之为executor

  • 这个回调函数会被立刻执行,并传入两个回调参数resolvereject
  • 当调用resolve回调函数时,会执行 Promise 对象的then方法传入的回调
  • 当调用reject回调函数时,会执行 Promise 对象的catch方法传入的回调
  1. Promise状态机

promise 是一个有状态的对象,有以下3种:

 待定(pending):最初始状态代表尚未开始或者正在执行中
 兑现(fulfilled,有时候也称为“解决”,resolved):代表成功
 拒绝(rejected):代表失败

无论落定为哪种状态都是不可逆的。只要从待定转换为兑现或拒绝,promise的状态就不再改变。

重要的是,promise的状态是私有的,不能直接通过 JavaScript 检测到或修改。promise故意将异步行为封装起来,从而隔离外部的同步代码。

2.解决值、拒绝理由及用例

promise主要有两大用途。

(1)抽象地表示一个异步操作。

(2)promise封装的异步操作会实际生成某个值,而程序期待promise状态改变时可以访问这个值。

为了支持这两种用例,每个promise只要状态切换为resolved,就会有一个私有的内部值(value)。
每个promise只要状态切换为rejected,就会有一个私有的内部理由(reason)。

3.通过执行函数控制promise状态

由于promise的状态是私有的,所以只能在内部进行操作。内部操作在promise的执行器函数中完成。

执行器函数主要有两项职责:初始化promise的异步行为和控制状态的最终转换。

其中,控制promise状态的转换是通过调用它的两个函数参数实现的。

调用resolve()会把状态切换为兑现,

调用 reject()会把状态切换为拒绝。

另外,调用 reject()也会抛出错误(后面会讨论这个错误)。

let p1 = new Promise((resolve, reject) => resolve()); 
setTimeout(console.log, 0, p1); // Promise <resolved> 
let p2 = new Promise((resolve, reject) => reject()); 
setTimeout(console.log, 0, p2); // Promise <rejected>
// Uncaught error (in promise) 

在前面的例子中,并没有什么异步操作,因为在初始化promise时,执行器函数已经改变了每个promise的状态。这里的关键在于,执行器函数是同步执行的。这是因为执行器函数是promise的初始化程序

通过下面的例子可以看出上面代码的执行顺序:

new Promise(() => setTimeout(console.log, 0, 'executor')); 
setTimeout(console.log, 0, 'promise initialized'); 
// executor 
// promise initialized 

添加 setTimeout 可以推迟切换状态

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000)); 
// 在 console.log 打印promise实例的时候,还不会执行超时回调(即 resolve())
setTimeout(console.log, 0, p); // Promise <pending>

无论 resolve()和 reject()中的哪个被调用,状态转换都不可撤销了。于是继续修改状态会静默失败,如下所示:

let p = new Promise((resolve, reject) => { 
 resolve();
 reject(); // 没有效果
}); 
setTimeout(console.log, 0, p); // Promise <resolved>

为避免promise卡在待定状态,可以添加一个定时退出功能。比如,可以通过 setTimeout 设置一个10 秒钟后无论如何都会拒绝promise的回调:

let p = new Promise((resolve, reject) => { 
 setTimeout(reject, 10000); // 10 秒后调用 reject() 
 // 执行函数的逻辑
}); 
setTimeout(console.log, 0, p); // Promise <pending> 
setTimeout(console.log, 11000, p); // 11 秒后再检查状态
// (After 10 seconds) Uncaught error 
// (After 11 seconds) Promise <rejected> 

因为promise的状态只能改变一次,所以这里的超时拒绝逻辑中可以放心地设置让promise处于待定状态的最长时间。如果执行器中的代码在超时之前已经解决或拒绝,那么超时回调再尝试拒绝也会静默失败。

4.Promise.resolve()

Promise并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。通过调用Promise.resolve()静态方法,可以实例化一个解决的Promise。下面两个Promise实例实际上是一样的:

let p1 = new Promise((resolve, reject) => resolve()); 
let p2 = Promise.resolve(); 

a. 使用这个静态方法,实际上可以把任何值都转换为一个promise:

setTimeout(console.log, 0, Promise.resolve()); 
// Promise <resolved>: undefined 
setTimeout(console.log, 0, Promise.resolve(3));
// Promise <resolved>: 3 
// 多余的参数会忽略
setTimeout(console.log, 0, Promise.resolve(4, 5, 6)); 
// Promise <resolved>: 4

b. 如果传入的参数本身是一个promise,那它的行为就类似于一个空包装。因此,Promise.resolve()可以说是一个幂等方法,如下所示:这个幂等性会保留传入promise的状态

let p = new Promise(() => {}); 
setTimeout(console.log, 0, p); // Promise <pending> 
setTimeout(console.log, 0, Promise.resolve(p)); // Promise <pending> 

c. 这个静态方法能够包装任何非promise值,包括错误对象,并将其转换为解决的promise。因此,也可能导致不符合预期的行为:

let p = Promise.resolve(new Error('foo')); 
setTimeout(console.log, 0, p); 
// Promise <resolved>: Error: foo 
5.Promise.reject()

Promise.reject()会实例化一个拒绝的promise并抛出一个异步错误(这个错误不能通过 try/catch 捕获,而只能通过拒绝处理程序捕获)。下面的两个promise实例实际上是一样的:

let p1 = new Promise((resolve, reject) => reject()); 
let p2 = Promise.reject(); 

这个拒绝的promise的理由就是传给 Promise.reject()的第一个参数。这个参数也会传给后续的拒绝处理程序

let p = Promise.reject(3); 
setTimeout(console.log, 0, p); // Promise <rejected>: 3 
p.then(null, (e) => setTimeout(console.log, 0, e)); // 3 

关键在于,Promise.reject()并没有照搬 Promise.resolve()的幂等逻辑。如果给它传一个promise对象,则这个promise会成为它返回的拒绝promise的理由:

setTimeout(console.log, 0, Promise.reject(Promise.resolve())); 
// Promise <rejected>: Promise <resolved>
6.同步/异步执行的二元性

Promise 的设计很大程度上会导致一种完全不同于 JavaScript 的计算模式。包含了两种模式下抛出错误的情形:

try { 
 throw new Error('foo'); 
} catch(e) { 
 console.log(e); // Error: foo 
} 
try { 
 Promise.reject(new Error('bar')); 
} catch(e) { 
 console.log(e); 
} 
// Uncaught (in promise) Error: bar

第一个 try/catch 抛出并捕获了错误,第二个 try/catch 抛出错误却没有捕获到。

这里的同步代码之所以没有捕获promise抛出的错误,是因为它没有通过异步模式捕获错误。从这里就可以看出promise真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介。

Promise的实例方法

promise实例的方法是连接外部同步代码与内部异步代码之间的桥梁。这些方法可以访问异步操作返回的数据,处理promise成功和失败的结果,连续对promise求值,或者添加只有promise进入终止状态时才会执行的代码。

1.实现 Thenable 接口

在 ECMAScript 暴露的异步结构中,任何对象都有一个 then()方法。这个方法被认为实现了Thenable 接口。

ECMAScript 的 Promise 类型实现了 Thenable 接口。

2.Promise.prototype.then()

Promise.prototype.then()是为promise实例添加处理程序的主要方法。这个 then()方法接收最多两个参数:onResolved 处理程序和 onRejected 处理程序。这两个参数都是可选的,如果提供的话,则会在promise分别进入“兑现”和“拒绝”状态时执行。

通过then方法可以对 Promise 中的resolve进行处理。then方法的返回值是一个 Promise 实例。

function onResolved(id) { 
 setTimeout(console.log, 0, id, 'resolved');
  } 
function onRejected(id) { 
 setTimeout(console.log, 0, id, 'rejected'); 
} 
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 3000)); 
let p2 = new Promise((resolve, reject) => setTimeout(reject, 3000)); 
p1.then(() => onResolved('p1'), 
 () => onRejected('p1')); 
p2.then(() => onResolved('p2'), 
 () => onRejected('p2')); 
//(3 秒后)
// p1 resolved 
// p2 rejected 
3.Promise.prototype.catch()

Promise.prototype.catch()方法用于给promise添加拒绝处理程序。这个方法只接收一个参数:onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype. then(null, onRejected)。

Promise.prototype.catch()返回一个新的promise实例:

let p1 = new Promise(() => {}); 
let p2 = p1.catch(); 
setTimeout(console.log, 0, p1); // Promise <pending> 
setTimeout(console.log, 0, p2); // Promise <pending> 
setTimeout(console.log, 0, p1 === p2); // false
4.Promise.prototype.finally()

Promise.prototype.finally()方法用于给promise添加 onFinally 处理程序,这个处理程序在promise转换为解决或拒绝状态时都会执行。这个方法可以避免 onResolved 和 onRejected 处理程序中出现冗余代码。但 onFinally 处理程序没有办法知道promise的状态是解决还是拒绝,所以这个方法主要用于添加清理代码。

let p1 = Promise.resolve(); 
let p2 = Promise.reject(); 
let onFinally = function() { 
 setTimeout(console.log, 0, 'Finally!') 
} 
p1.finally(onFinally); // Finally 
p2.finally(onFinally); // Finally 

Promise.prototype.finally()方法返回一个新的promise实例

let p1 = new Promise(() => {}); 
let p2 = p1.finally();
setTimeout(console.log, 0, p1); // Promise <pending> 
setTimeout(console.log, 0, p2); // Promise <pending> 
setTimeout(console.log, 0, p1 === p2); // false

这个新promise实例不同于 then()或 catch()方式返回的实例。因为 onFinally 被设计为一个状态 无关的方法,所以在大多数情况下它将表现为父promise的传递。对于已解决状态和被拒绝状态都是如此。

promise连锁与promise合成

promise连锁

每个promise实例的方 法(then()、catch()和 finally())都会返回一个新的promise对象,而这个新promise又有自己的实例方法。这样连缀方法调用就可以构成所谓的“promise连锁”。

可以让每 个后续promise都等待之前的promise,也就是串行化异步任务。

let p1 = new Promise((resolve, reject) => { 
 console.log('p1 executor'); 
 setTimeout(resolve, 1000); 
}); 
p1.then(() => new Promise((resolve, reject) => { 
 console.log('p2 executor'); 
 setTimeout(resolve, 1000); 
 })) 
 .then(() => new Promise((resolve, reject) => { 
 console.log('p3 executor'); 
 setTimeout(resolve, 1000); 
 })) 
 .then(() => new Promise((resolve, reject) => { 
 console.log('p4 executor'); 
 setTimeout(resolve, 1000); 
 })); 
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

把生成promise的代码提取到一个工厂函数中,就可以写成这样:

function delayedResolve(str) { 
 return new Promise((resolve, reject) => { 
 console.log(str); 
 setTimeout(resolve, 1000); 
 }); 
} 
delayedResolve('p1 executor') 
 .then(() => delayedResolve('p2 executor')) 
 .then(() => delayedResolve('p3 executor')) 
 .then(() => delayedResolve('p4 executor')) 
// p1 executor(1 秒后)
// p2 executor(2 秒后)
// p3 executor(3 秒后)
// p4 executor(4 秒后)

这种结构可以简 洁地将异步任务串行化,解决之前依赖回调的难题。

假如这种情况下不使用promise,那么前面的代码可能 就要这样写了:

function delayedExecute(str, callback = null) { 
 setTimeout(() => { 
 console.log(str); 
 callback && callback(); 
 }, 1000) 
} 
delayedExecute('p1 callback', () => { 
 delayedExecute('p2 callback', () => { 
 delayedExecute('p3 callback', () => { 
 delayedExecute('p4 callback'); 
 }); 
 }); 
}); 
// p1 callback(1 秒后)
// p2 callback(2 秒后)
// p3 callback(3 秒后)
// p4 callback(4 秒后

这不正是promise所要解决回调地狱问题吗!!!

promise图

因为一个promise可以有任意多个处理程序,所以promise连锁可以构建有向非循环图的结构。这样,每个 promise都是图中的一个节点,而使用实例方法添加的处理程序则是有向顶点。因为图中的每个节点都会等 待前一个节点落定,所以图的方向就是promise的解决或拒绝顺序。

// A 
// / \ 
// B C 
// /\ /\ 
// D E F G 
let A = new Promise((resolve, reject) => { 
 console.log('A'); 
 resolve(); 
}); 
let B = A.then(() => console.log('B')); 
let C = A.then(() => console.log('C')); 
B.then(() => console.log('D')); 
B.then(() => console.log('E')); 
C.then(() => console.log('F')); 
C.then(() => console.log('G')); 
// A 
// B 
// C 
// D 
// E 
// F 
// G 

注意,日志的输出语句是对二叉树的层序遍历。如前所述,promise的处理程序是按照它们添加的顺序 执行的。由于promise的处理程序是先添加到消息队列,然后才逐个执行,因此构成了层序遍历。

Promise.all()

Promise 类提供两个将多个promise实例组合成一个promise的静态方法:Promise.all()和 Promise.race()。 而合成后promise的行为取决于内部promise的行为。

Promise.all()静态方法创建的promise会在一组promise全部解决之后再解决。这个静态方法接收一个 可迭代对象,返回一个新promise。

如果所有promise都成功解决,则合成promise的解决值就是所有包含promise解决值的数组,按照迭代器顺序:

let p = Promise.all([ 
 Promise.resolve(3), 
 Promise.resolve(), 
 Promise.resolve(4) 
]); 
p.then((values) => setTimeout(console.log, 0, values)); // [3, undefined, 4] 

如果至少有一个包含的promise待定,则合成的promise也会待定。如果有一个包含的promise拒绝,则合成的 promise也会拒绝,则第一个拒绝的promise会将自己的理由作为合成promise的拒绝理由。

Promise.race()

Promise.race()静态方法返回一个包装promise,是一组集合中最先解决或拒绝的promise的镜像。这个方法接收一个可迭代对象,返回一个新promise。

Promise.race()不会对解决或拒绝的promise区别对待。无论是解决还是拒绝,只要是第一个落定的 promise,Promise.race()就会包装其解决值或拒绝理由并返回新promise:

// 解决先发生,超时后的拒绝被忽略
let p1 = Promise.race([ 
 Promise.resolve(3), 
 new Promise((resolve, reject) => setTimeout(reject, 1000)) 
]); 
setTimeout(console.log, 0, p1); // Promise <resolved>: 3 
// 拒绝先发生,超时后的解决被忽略
let p2 = Promise.race([ 
 Promise.reject(4), 
 new Promise((resolve, reject) => setTimeout(resolve, 1000)) 
]); 
setTimeout(console.log, 0, p2); // Promise <rejected>: 4 

异步函数 async/await

异步函数,也称为“async/await”(语法关键字),是 ES6 promise模式在 ECMAScript 函数中的应用。 async/await 是 ES8 规范新增的。这个特性从行为和语法上都增强了 JavaScript,让以同步方式写的代码能够异步执行

异步函数可以暂停执行,而不阻塞主线程。

1.async

async 关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上:

async function foo() {} 
let bar = async function() {}; 
let baz = async () => {}; 
class Qux { 
 async qux() {} 
} 

使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。

async function foo() { 
 console.log(1); 
} 
foo(); 
console.log(2); 
// 1 
// 2 

不过,异步函数如果使用 return 关键字返回了值(如果没有 return 则会返回 undefined),这个值会被 Promise.resolve()包装成一个promise对象。异步函数始终返回promise对象,在函数外部调用这个函数可以得到它返回的promise:

async function foo() { 
 console.log(1); 
 return 3; 
} 
// 给返回的promise添加一个解决处理程序
foo().then(console.log);
console.log(2); 
// 1 
// 2 
// 3 

当然,直接返回一个promise对象也是一样的:

async function foo() { 
 console.log(1); 
 return Promise.resolve(3); 
} 
// 给返回的promise添加一个解决处理程序
foo().then(console.log); 
console.log(2); 
// 1 
// 2 
// 3 

在异步函数中抛出错误会返回拒绝的promise:

async function foo() { 
 console.log(1); 
 throw 3; 
} 
// 给返回的promise添加一个拒绝处理程序
foo().catch(console.log);
console.log(2); 
// 1 
// 2 
// 3

不过,拒绝promise的错误不会被异步函数捕获:

async function foo() { 
 console.log(1); 
 Promise.reject(3); 
} 
// Attach a rejected handler to the returned promise 
foo().catch(console.log); 
console.log(2); 
// 1 
// 2 
// Uncaught (in promise): 3 

2.await

因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用 await 关键字可以暂停异步函数代码的执行,等待promise解决

await 关键字必须在异步函数中使用。

注意,await 关键字会暂停执行异步函数后面的代码,让出 JavaScript 运行时的执行线程。

// 1000 毫秒后异步打印"baz" 
async function baz() { 
 await new Promise((resolve, reject) => setTimeout(resolve, 1000)); 
 console.log('baz'); 
} 
baz(); 
// baz(1000 毫秒后)

如前面的例子所示,单独的 Promise.reject()不会被异步函数捕获,而会抛出未捕获错误。不 过,对拒绝的promise使用 await 则会释放(unwrap)错误值(将拒绝promise返回):

async function foo() { 
 console.log(1); 
 await Promise.reject(3); 
 console.log(4); // 这行代码不会执行
} 
// 给返回的promise添加一个拒绝处理程序
foo().catch(console.log); 
console.log(2); 
// 1 
// 2 
// 3

停止和恢复执行

JavaScript 运行时在碰 到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息 队列中推送一个任务,这个任务会恢复异步函数的执行。

async function foo() { 
 console.log(2); 
 await null; 
 console.log(4); 
} 
console.log(1); 
foo(); 
console.log(3);
// 1 
// 2 
// 3 
// 4

控制台中输出结果的顺序很好地解释了运行时的工作过程:

(1) 打印 1;

(2) 调用异步函数 foo();

(3)(在 foo()中)打印 2;

(4)(在 foo()中)await 关键字暂停执行,为立即可用的值 null 向消息队列中添加一个任务;

(5) foo()退出;

(6) 打印 3;

(7) 同步线程的代码执行完毕;

(8) JavaScript 运行时从消息队列中取出任务,恢复异步函数执行;

(9)(在 foo()中)恢复执行,await 取得 null 值(这里并没有使用);

(10)(在 foo()中)打印 4; (11) foo()返回。

事件循环

JS的运行机制就是事件循环!

核心思想是将所有的异步任务放入一个队列中,然后按照队列中的顺序依次执行,直到队列为空为止。主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

任务队列通常分为两种类型:宏任务队列和微任务队列。先进先出

调用栈也叫执行栈,是 JavaScript 运行时用于存储函数调用的数据结构(栈先进后出),它记录了当前执行的上下文和函数调用链

JS的执行顺序是什么?

  1. JS是从上到下一行一行执行。
  2. 如果某一行执行报错,则停止执行下面的代码。
  3. 先执行同步代码,再执行异步代码

事件循环的工作机制

1.先执行同步代码,所有同步代码都在主线程上执行,形成一个执行栈。

2.当遇到异步任务时,会将其挂起并添加到任务队列中,宏任务放入宏任务队列,微任务放进微任务队列。

3.当执行栈为空时,事件循环从任务队列中取出一个任务,加入到执行栈中执行。

4.重复上述步骤,直到任务队列为空。

微任务执行时机比宏任务要早,微任务在DOM渲染前触发,宏任务在DOM渲染后触发。

举例:

console.log(1) //同步代码先执行,调用栈执行后直接出栈
setTimeout(() => {
	console.log(4) //异步代码,宏任务放入宏任务队列中
})
Promise.resolve().then(() => {
    console.log(3) //异步代码,微任务放入微任务队列中
})
console.log(2) //同步代码先执行,调用栈执行后直接出栈
//1
//2
//3
//4

setTimeout、Promise、Async/Await的区别

🔶 延时器:setTimeout(回调函数,间隔时间):以毫秒为单位间隔指定时间后调用回调函数(仅1次)。

​ setTimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行。

🔶 promise.then里的回调函数会放到微任务队列里,等宏任务里面的同步代码执行完再执行。

🔶 async和await必须基于返回了pormise的函数,对于其它的函数没有任何作用。

​ async方法执行时,遇到await会立即await后代码,把代码放到微任务队列里,让出执行栈让同步代码先执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值