promise

什么是promise

如果说到JavaScript的异步处理,我想大多数人都会想到利用回调函数:

 
    
1
2
3
4
5
6
7
 
    
//示例:使用了回调函数的异步处理
http.get( '/v1/get', function(error, data) {
if (error) {
//错误时的处理
}
//成功时的处理
})

像上面这样基于回调函数的异步处理如果统一参数使用规则的话,写法也会很明了。但是,这也仅是编码规约而已,即使采用不同的写法也不会报错。

Promise则是把类似的异步处理对象和处理规则进行规范化,并按照采用统一的接口来编写,而采取规定方法之外的写法都会报错。下面看一个例子:

 
    
1
2
3
4
5
6
 
    
var promise = http.get( '/v1/get');
promise.then( function(result) {
//成功时的处理
}).catch( function(error) {
//错误时的处理
})

可以看到,这里在使用promise进行一步处理的时候,我们必须按照接口规定的方法编写处理代码。也就是说,除promise对象规定的方法(这里的 then 或 catch)以外的方法都是不可以使用的, 而不会像回调函数方式那样可以自己自由的定义回调函数的参数,而必须严格遵守固定、统一的编程方式来编写代码。这样,基于Promise的统一接口的做法, 就可以形成基于接口的各种各样的异步处理模式。

但这并不是使用promise的足够理由,promise为异步操作提供了统一的接口,能让代码不至于陷入回调嵌套的死路中,它的强大之处在于它的链式调用(文章后面会有提及)。

基本用法

promise的语法:

 
    
1
2
3
4
 
    
new Promise( function(resolve, reject) {
//待处理的异步逻辑
//处理结束后,调用resolve或reject方法
})

新建一个promise很简单,只需要new一个promise对象即可。所以promise本质上就是一个函数,它接受一个函数作为参数,并且会返回promise对象,这就给链式调用提供了基础。

其实Promise函数的使命,就是构建出它的实例,并且负责帮我们管理这些实例。而这些实例有以下三种状态:

  1. pending: 初始状态,位履行或拒绝
  2. fulfilled: 意味着操作成功完成
  3. rejected: 意味着操作失败

pending 状态的 Promise对象可能以 fulfilled状态返回了一个值,也可能被某种理由(异常信息)拒绝(reject)了。当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers)就会被调用,then方法分别指定了resolve方法和reject方法的回调函数

一图胜千言:

alt promise图解

简单的示例:

 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
 
    
var promise = new Promise( function(resolve, reject) {
if ( /* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then( function(value) {
// 如果调用了resolve方法,执行此函数
}, function(value) {
// 如果调用了reject方法,执行此函数
});

上述代码很清晰的展示了promise对象运行的机制。下面再看一个示例:

 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 
    
var getJSON = function(url) {
var promise = new Promise( function(resolve, reject){
var client = new XMLHttpRequest();
client.open( "GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader( "Accept", "application/json");
client.send();
function handler() {
if ( this.status === 200) {
resolve( this.response);
} else {
reject( new Error( this.statusText));
}
};
});
return promise;
};
getJSON( "/posts.json").then( function(json) {
console.log( 'Contents: ' + json);
}, function(error) {
console.error( '出错了', error);
});

上面代码中,resolve方法和reject方法调用时,都带有参数。它们的参数会被传递给回调函数。reject方法的参数通常是Error对象的实例,而resolve方法的参数除了正常的值以外,还可能是另一个Promise实例,比如像下面这样。

 
    
1
2
3
4
5
6
7
8
 
    
var p1 = new Promise( function(resolve, reject){
// ... some code
});
var p2 = new Promise( function(resolve, reject){
// ... some code
resolve(p1);
})

上面代码中,p1和p2都是Promise的实例,但是p2的resolve方法将p1作为参数,这时p1的状态就会传递给p2。如果调用的时候,p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是fulfilled或者rejected,那么p2的回调函数将会立刻执行。

promise的链式操作

正如前面提到的,Promise.prototype.then方法返回的是一个新的Promise对象,因此可以采用链式写法。

 
    
1
2
3
4
5
 
    
getJSON( "/visa.json").then( function(json) {
return json.name;
}).then( function(name) {
// proceed
});

上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

如果前一个回调函数返回的是Promise对象,这时后一个回调函数就会等待该Promise对象有了运行结果,才会进一步调用。

 
    
1
2
3
4
5
 
    
getJSON( "/visa/get.json").then( function(post) {
return getJSON(post.jobURL);
}).then( function(jobs) {
// 对jobs进行处理
});

这种设计使得嵌套的异步操作,可以被很容易得改写,从回调函数的“横向发展”改为“向下发展”。

promise捕获错误

Promise.prototype.catch方法是Promise.prototype.then(null, rejection)的别名,用于指定发生错误时的回调函数。

 
    
1
2
3
4
5
6
 
    
getJSON( "/visa.json").then( function(result) {
// some code
}).catch( function(error) {
// 处理前一个回调函数运行时发生的错误
console.log( '出错啦!', error);
});

Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

 
    
1
2
3
4
5
6
7
 
    
getJSON( "/visa.json").then( function(json) {
return json.name;
}).then( function(name) {
// proceed
}).catch( function(error) {
//处理前面任一个then函数抛出的错误
});

其他常用的promise方法

Promise.all方法,Promise.race方法

Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。

 
    
1
 
    
var p = Promise.all([p1,p2,p3]);

上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是Promise对象的实例。(Promise.all方法的参数不一定是数组,但是必须具有iterator接口,且返回的每个成员都是Promise实例。)

p的状态由p1、p2、p3决定,分成两种情况。

  1. 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

  2. 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

下面是一个具体的例子。

 
    
1
2
3
4
5
6
7
8
9
10
 
    
// 生成一个Promise对象的数组
var promises = [ 2, 3, 5, 7, 11, 13].map( function(id){
return getJSON( "/get/addr" + id + ".json");
});
Promise.all(promises).then( function(posts) {
// ...
}).catch( function(reason){
// ...
});

Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。

 
    
1
 
    
var p = Promise.race([p1,p2,p3]);

上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值。

如果Promise.all方法和Promise.race方法的参数,不是Promise实例,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。

Promise.resolve方法,Promise.reject方法

有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。

 
    
1
 
    
var jsPromise = Promise.resolve($.ajax( '/whatever.json'));

上面代码将jQuery生成deferred对象,转为一个新的ES6的Promise对象。

如果Promise.resolve方法的参数,不是具有then方法的对象(又称thenable对象),则返回一个新的Promise对象,且它的状态为fulfilled。

 
    
1
2
3
4
5
6
 
    
var p = Promise.resolve( 'Hello');
p.then( function (s){
console.log(s)
});
// Hello

上面代码生成一个新的Promise对象的实例p,它的状态为fulfilled,所以回调函数会立即执行,Promise.resolve方法的参数就是回调函数的参数。

如果Promise.resolve方法的参数是一个Promise对象的实例,则会被原封不动地返回。

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。

 
    
1
2
3
4
5
6
 
    
var p = Promise.reject( '出错啦');
p.then( null, function (error){
console.log(error)
});
// 出错了

上面代码生成一个Promise对象的实例p,状态为rejected,回调函数会立即执行。

题外话:async函数

async函数是es7提案出来的语法,并不属于es6,但是已经有一些平台和编辑器支持这种函数了,所以这里也做一下了解。

async函数是用来取代回调函数的另一种方法。

只要函数名之前加上async关键字,就表明该函数内部有异步操作。该异步操作应该返回一个Promise对象,前面用await关键字注明。当函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。

 
    
1
2
3
4
 
    
async function getStockPrice(symbol, currency) {
let price = await getStockPrice(symbol);
return convert(price, currency);
}

上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数将返回一个Promise对象。调用该函数时,当遇到await关键字,立即返回它后面的表达式(getStockPrice函数)产生的Promise对象,不再执行函数体内后面的语句。等到getStockPrice完成,再自动回到函数体内,执行剩下的语句。

下面是一个更一般性的例子。

 
    
1
2
3
4
5
6
7
8
9
10
 
    
function timeout(ms) {
return new Promise( (resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncValue(value) {
await timeout( 50);
return value;
}

上面代码中,asyncValue函数前面有async关键字,表明函数体内有异步操作。执行的时候,遇到await语句就会先返回,等到timeout函数执行完毕,再返回value。

个人觉得async函数将异步发挥到了极致,代码看上去更加简洁,更加舒服了,而且其流程也很好理解。

总结

promise作为异步操作的规则,确实给开发带来了不少便利,至少不用像回调那样,出现函数里面套函数这种无限的嵌套的情况,promise让你的代码变得更加的优雅了。当然如果之后async函数变得更加普及,那么就更好了。

下面来看看下面这道题目,大家可以思考下结果是多少?

 
    
1
2
3
4
5
6
7
8
9
10
11
12
 
    
console.log( 1);
new Promise( function (resolve, reject){
reject( true);
window.setTimeout( function (){
resolve( false);
}, 0);
}).then( function(){
console.log( 2);
}, function(){
console.log( 3);
});
console.log( 4);

答案我就不贴出来了,给大家思考的空间,实在不知道结果的也可以直接复制这段代码到浏览器控制台执行下,也能很快的出结果。

展望

很好奇promise内部的实现机理,接下来我会深入研究下promise实现原理,有成果了给大家分享哦~


前言

前一阵子记录了promise的一些常规用法,这篇文章再深入一个层次,来分析分析promise的这种规则机制是如何实现的。ps:本文适合已经对promise的用法有所了解的人阅读,如果对其用法还不是太了解,可以移步我的上一篇博文

本文的promise源码是按照Promise/A+规范来编写的(不想看英文版的移步Promise/A+规范中文翻译

引子

为了让大家更容易理解,我们从一个场景开始讲解,让大家一步一步跟着思路思考,相信你一定会更容易看懂。

考虑下面一种获取用户id的请求处理

 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
 
    
//例1
function getUserId() {
return new Promise( function(resolve) {
//异步请求
http.get(url, function(results) {
resolve(results.id)
})
})
}
getUserId().then( function(id) {
//一些处理
})

getUserId方法返回一个promise,可以通过它的then方法注册(注意注册这个词)在promise异步操作成功时执行的回调。这种执行方式,使得异步调用变得十分顺手。

原理剖析

那么类似这种功能的Promise怎么实现呢?其实按照上面一句话,实现一个最基础的雏形还是很easy的。

极简promise雏形

 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
    
function Promise(fn) {
var value = null,
callbacks = []; //callbacks为数组,因为可能同时有很多个回调
this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
};
function resolve(value) {
callbacks.forEach( function (callback) {
callback(value);
});
}
fn(resolve);
}

上述代码很简单,大致的逻辑是这样的:

  1. 调用then方法,将想要在Promise异步操作成功时执行的回调放入callbacks队列,其实也就是注册回调函数,可以向观察者模式方向思考;
  2. 创建Promise实例时传入的函数会被赋予一个函数类型的参数,即resolve,它接收一个参数value,代表异步操作返回的结果,当一步操作执行成功后,用户会调用resolve方法,这时候其实真正执行的操作是将callbacks队列中的回调一一执行;

可以结合例1中的代码来看,首先new Promise时,传给promise的函数发送异步请求,接着调用promise对象的then属性,注册请求成功的回调函数,然后当异步请求发送成功时,调用resolve(results.id)方法, 该方法执行then方法注册的回调数组。

相信仔细的人应该可以看出来,then方法应该能够链式调用,但是上面的最基础简单的版本显然无法支持链式调用。想让then方法支持链式调用,其实也是很简单的:

 
    
1
2
3
4
 
    
this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
return this;
};

see?只要简单一句话就可以实现类似下面的链式调用:

 
    
1
2
3
4
5
6
 
    
// 例2
getUserId().then( function (id) {
// 一些处理
}).then( function (id) {
// 一些处理
});

加入延时机制

细心的同学应该发现,上述代码可能还存在一个问题:如果在then方法注册回调之前,resolve函数就执行了,怎么办?比如promise内部的函数是同步函数:

 
    
1
2
3
4
5
6
7
8
9
 
    
// 例3
function getUserId() {
return new Promise( function (resolve) {
resolve( 9876);
});
}
getUserId().then( function (id) {
// 一些处理
});

这显然是不允许的,Promises/A+规范明确要求回调需要通过异步方式执行,用以保证一致可靠的执行顺序。因此我们要加入一些处理,保证在resolve执行之前,then方法已经注册完所有的回调。我们可以这样改造下resolve函数:

 
    
1
2
3
4
5
6
7
 
    
function resolve(value) {
setTimeout( function() {
callbacks.forEach( function (callback) {
callback(value);
});
}, 0)
}

上述代码的思路也很简单,就是通过setTimeout机制,将resolve中执行回调的逻辑放置到JS任务队列末尾,以保证在resolve执行时,then方法的回调函数已经注册完成.

但是,这样好像还存在一个问题,可以细想一下:如果Promise异步操作已经成功,这时,在异步操作成功之前注册的回调都会执行,但是在Promise异步操作成功这之后调用的then注册的回调就再也不会执行了,这显然不是我们想要的。

加入状态

恩,为了解决上一节抛出的问题,我们必须加入状态机制,也就是大家熟知的pendingfulfilledrejected

Promises/A+规范中的2.1Promise States中明确规定了,pending可以转化为fulfilledrejected并且只能转化一次,也就是说如果pending转化到fulfilled状态,那么就不能再转化到rejected。并且fulfilledrejected状态只能由pending转化而来,两者之间不能互相转换。一图胜千言:

alt promise state

改进后的代码是这样的:

 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 
    
function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];
this.then = function (onFulfilled) {
if (state === 'pending') {
callbacks.push(onFulfilled);
return this;
}
onFulfilled(value);
return this;
};
function resolve(newValue) {
value = newValue;
state = 'fulfilled';
setTimeout( function () {
callbacks.forEach( function (callback) {
callback(value);
});
}, 0);
}
fn(resolve);
}

上述代码的思路是这样的:resolve执行时,会将状态设置为fulfilled,在此之后调用then添加的新回调,都会立即执行。

这里没有任何地方将state设为rejected,为了让大家聚焦在核心代码上,这个问题后面会有一小节专门加入。

链式Promise

那么这里问题又来了,如果用户再then函数里面注册的仍然是一个Promise,该如何解决?比如下面的例4

 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 
    
// 例4
getUserId()
.then(getUserJobById)
.then( function (job) {
// 对job的处理
});
function getUserJobById(id) {
return new Promise( function (resolve) {
http.get(baseUrl + id, function(job) {
resolve(job);
});
});
}

这种场景相信用过promise的人都知道会有很多,那么类似这种就是所谓的链式Promise

链式Promise是指在当前promise达到fulfilled状态后,即开始进行下一个promise(后邻promise)。那么我们如何衔接当前promise和后邻promise呢?(这是这里的难点)。

其实也不是辣么难,只要在then方法里面return一个promise就好啦。Promises/A+规范中的2.2.7就是这么说哒(微笑脸)~

下面来看看这段暗藏玄机的then方法和resolve方法改造代码:

 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
 
    
function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];
this.then = function (onFulfilled) {
return new Promise( function (resolve) {
handle({
onFulfilled: onFulfilled || null,
resolve: resolve
});
});
};
function handle(callback) {
if (state === 'pending') {
callbacks.push(callback);
return;
}
//如果then中没有传递任何东西
if(!callback.onFulfilled) {
callback.resolve(value);
return;
}
var ret = callback.onFulfilled(value);
callback.resolve(ret);
}
function resolve(newValue) {
if (newValue && ( typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if ( typeof then === 'function') {
then.call(newValue, resolve);
return;
}
}
state = 'fulfilled';
value = newValue;
setTimeout( function () {
callbacks.forEach( function (callback) {
handle(callback);
});
}, 0);
}
fn(resolve);
}

我们结合例4的代码,分析下上面的代码逻辑,为了方便阅读,我把例4的代码贴在这里:

 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 
    
// 例4
getUserId()
.then(getUserJobById)
.then( function (job) {
// 对job的处理
});
function getUserJobById(id) {
return new Promise( function (resolve) {
http.get(baseUrl + id, function(job) {
resolve(job);
});
});
}

  1. then方法中,创建并返回了新的Promise实例,这是串行Promise的基础,并且支持链式调用。
  2. handle方法是promise内部的方法。then方法传入的形参onFulfilled以及创建新Promise实例时传入的resolve均被push到当前promisecallbacks队列中,这是衔接当前promise和后邻promise的关键所在(这里一定要好好的分析下handle的作用)。
  3. getUserId生成的promise(简称getUserId promise)异步操作成功,执行其内部方法resolve,传入的参数正是异步操作的结果id
  4. 调用handle方法处理callbacks队列中的回调:getUserJobById方法,生成新的promisegetUserJobById promise
  5. 执行之前由getUserId promisethen方法生成的新promise(称为bridge promise)的resolve方法,传入参数为getUserJobById promise。这种情况下,会将该resolve方法传入getUserJobById promisethen方法中,并直接返回。
  6. getUserJobById promise异步操作成功时,执行其callbacks中的回调:getUserId bridge promise中的resolve方法
  7. 最后执行getUserId bridge promise的后邻promisecallbacks中的回调。

更直白的可以看下面的图,一图胜千言(都是根据自己的理解画出来的,如有不对欢迎指正):

alt promise analysis

失败处理

在异步操作失败时,标记其状态为rejected,并执行注册的失败回调:

 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 
    
//例5
function getUserId() {
return new Promise( function(resolve) {
//异步请求
http.get(url, function(error, results) {
if (error) {
reject(error);
}
resolve(results.id)
})
})
}
getUserId().then( function(id) {
//一些处理
}, function(error) {
console.log(error)
})

有了之前处理fulfilled状态的经验,支持错误处理变得很容易,只需要在注册回调、处理状态变更上都要加入新的逻辑:

 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
 
    
function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];
this.then = function (onFulfilled, onRejected) {
return new Promise( function (resolve, reject) {
handle({
onFulfilled: onFulfilled || null,
onRejected: onRejected || null,
resolve: resolve,
reject: reject
});
});
};
function handle(callback) {
if (state === 'pending') {
callbacks.push(callback);
return;
}
var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
ret;
if (cb === null) {
cb = state === 'fulfilled' ? callback.resolve : callback.reject;
cb(value);
return;
}
ret = cb(value);
callback.resolve(ret);
}
function resolve(newValue) {
if (newValue && ( typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if ( typeof then === 'function') {
then.call(newValue, resolve, reject);
return;
}
}
state = 'fulfilled';
value = newValue;
execute();
}
function reject(reason) {
state = 'rejected';
value = reason;
execute();
}
function execute() {
setTimeout( function () {
callbacks.forEach( function (callback) {
handle(callback);
});
}, 0);
}
fn(resolve, reject);
}

上述代码增加了新的reject方法,供异步操作失败时调用,同时抽出了resolvereject共用的部分,形成execute方法。

错误冒泡是上述代码已经支持,且非常实用的一个特性。在handle中发现没有指定异步操作失败的回调时,会直接将bridge promise(then函数返回的promise,后同)设为rejected状态,如此达成执行后续失败回调的效果。这有利于简化串行Promise的失败处理成本,因为一组异步操作往往会对应一个实际功能,失败处理方法通常是一致的:

 
    
1
2
3
4
5
6
7
8
9
 
    
//例6
getUserId()
.then(getUserJobById)
.then( function (job) {
// 处理job
}, function (error) {
// getUserId或者getUerJobById时出现的错误
console.log(error);
});

异常处理

细心的同学会想到:如果在执行成功回调、失败回调时代码出错怎么办?对于这类异常,可以使用try-catch捕获错误,并将bridge promise设为rejected状态。handle方法改造如下:

 
    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 
    
function handle(callback) {
if (state === 'pending') {
callbacks.push(callback);
return;
}
var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
ret;
if (cb === null) {
cb = state === 'fulfilled' ? callback.resolve : callback.reject;
cb(value);
return;
}
try {
ret = cb(value);
callback.resolve(ret);
} catch (e) {
callback.reject(e);
}
}

如果在异步操作中,多次执行resolve或者reject会重复处理后续回调,可以通过内置一个标志位解决。

总结

刚开始看promise源码的时候总不能很好的理解then和resolve函数的运行机理,但是如果你静下心来,反过来根据执行promise时的逻辑来推演,就不难理解了。这里一定要注意的点是:promise里面的then函数仅仅是注册了后续需要执行的代码,真正的执行是在resolve方法里面执行的,理清了这层,再来分析源码会省力的多。

现在回顾下Promise的实现过程,其主要使用了设计模式中的观察者模式:

  1. 通过Promise.prototype.then和Promise.prototype.catch方法将观察者方法注册到被观察者Promise对象中,同时返回一个新的Promise对象,以便可以链式调用。
  2. 被观察者管理内部pending、fulfilled和rejected的状态转变,同时通过构造函数中传递的resolve和reject方法以主动触发状态转变和通知观察者。

参考文献

深入理解 Promise
JavaScript Promises … In Wicked Detail


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值