从简入深挖掘Promise源码(完结)-西安科技大学任佳洋

一、准备知识

1. 模拟并发

问题: 现在有两个异步任务,使用定时器模拟, 要求定时器执行之后得结果必须按照指定得顺序输出。

function before(count, cb) {
	let obj = {};
	return function(key, val){
		obj[key] = val;
		if(--count == 0) {
			cb(obj);
		}
	}
}

let out = before(2, function(obj) {
	let res = [],				// 保存结果
		keys = Object.keys(obj);// 得到键数组
	
	keys.forEach(val=>{
		res.push(obj[val]);
	})

	console.log(res.join(''));	// 转换为字符转
});


function setTimeFun(key, val, time) {
	setTimeout(function(){
		out(key, val);
	}, time);
}

setTimeFun('1', 'hello,', 2000);
setTimeFun('2', 'world', 1000);

分析:

  • 使用同步函数,使得异步任务并发执行,但是如果不加以处理,那么执行之后得到的结果不能保证。
  • out类似一个信号,当满足触发条件时,便会得到指定的结果。

2. 发布订阅

class Event {
	constructor() {
		this.eventArr = [];		// 用来保存所有需要执行的函数
	}

	// 收集任务
	on(fun) {
		this.eventArr.push(fun);
	}

	// 触发所有的任务
	emit() {
		this.eventArr.forEach(fun=>{fun()});
	}

	// 取消任务
	cancel(fun) {
		if(!fun) {
			this.eventArr = [];
		} else {
			for(let i = 0; i < this.eventArr.length; i++) {
				if(this.eventArr[i] === fun) {
					this.eventArr.splice(i, 1);
					i--;
				}
			}
		}
	}
}

function task1(){
	console.log('hello');
}
function task2() {
	console.log('world');
}

let event = new Event();
event.on(task1);
event.on(task2);
event.on(task1);
event.on(task2);
event.cancel();
event.emit();

分析:

  • 发布订阅模式的特点:
    1. 收集依赖和触发依赖之间没有联系, 两者相互独立。

3. 观察者模式

class Aim {
	constructor() {
		this._arr = [];			// 存放所有观察者
		this.state = 'open';	// 被观察者的状态
	}

	// 收集所有观察者
	collect(os) {				
		this._arr.push(os);		
	}

	// 悲观者的状态需要改变
	setState(newState) {
		this.state = newState;
		this._arr.forEach(os=>{	// 通知 观察者 被观察者 的状态发生了改变
			os.update(newState);
		})
	}
}

class ObServer {
	constructor(name) {
		this.name = name;
	}
	
	// 观察者的方法
	update(news) {	
		console.log(`${this.name}: ${news}`);
	}
}

let aim = new Aim();
let os1 = new ObServer('张三');
let os2 = new ObServer('李四');
aim.collect(os1);
aim.collect(os2);
aim.setState('close');
aim.setState('open');

分析:

  • 观察者模式的特点:
    1. 观察者是基于发布订阅模式。
    2. 相对于嘎布订阅模式来说,观察者和被观察者之间存在相互的联系,只有当被观者改变时,才会触发观察者的方法。

4. 发布订阅模式观察者模式之间的区别

首先引用一张图解:作者:snow_in
在这里插入图片描述

总结:

  • 观察者模式中,目标和观察者之间直接联系,观察者订阅目标,目标直接触发观察者方法。
  • 发布订阅模式中,发布者和订阅者之间有一个调度中心,即发布者和订阅者之间没有直接联系,两者通过调度中心进行管理,由于这个特性,发布订阅模式可以用来做异步任务之间的调度。

5. 使用发布订阅模式改写 1中 的模拟并发

class Event {
	constructor() {
		this.count = 0;		// 记录收集的任务数量
		this.eventArr = [];	// 存放任务
		this.data = [];		// 存放最终的数据
	}

	// 收集任务
	on(fun) {
		this.eventArr.push(fun);
		this.count++;				// 记录任务的数量
	}

	// 触发任务
	emit() {
		this.eventArr.forEach(fun=>{
			fun();
		})
	}

	// 收集任务的结果
	collectData(index, data) {
		this.data[index] = data;
		if(--this.count == 0) {
			this.handel();
		}
	}

	// 最终的处理结果
	handel() {
		console.log(this.data.join(''));
	}
}

let event = new Event();
event.on(function() {
	setTimeout(function() {
		event.collectData(1, 'hello, ');
	}, 3000);
})

event.on(function() {
	setTimeout(function() {
		event.collectData(2, 'word。');
	}, 2000);
})

event.on(function() {
	setTimeout(function() {
		event.collectData(3, 'I am web前端工程师');
	}, 1000);
})

event.emit();

分析:

  • 和 1 中的比方方法比较,我们发现有以下几个优点
    1. 实现了任务数量的自动记录。
    2. 减少了闭包的使用,节约性能。
    3. 代码更加利于维护。

二、手写promise

1. 简易版promise

promise类

// promise的三个状态
const PENDING = 'pending';	// 等待态
const RESOLVE = 'resolve';	// 成功
const REJECT = 'resject';	// 失败
	
class MyPromise {
	constructor(execute) {
		// 初始化状态为 pending
		this.status = PENDING;

		// 保存成功和失败的结果
		this.res_success = '';
		this.res_fail = '';

		// 定义成功执行的函数
		let resolve = (data) =>{
			this.res_success = data;
			this.status = RESOLVE;
		}

		// 定义失败的函数
		let reject = (err) =>{
			this.res_fail = err;
			this.status = REJECT;
		}

		// 用户可能自己向外抛出异常
		try {
			// 执行用户的自定义函数
			execute(resolve, reject);
		} catch(err) {
			reject(err);
		}
	}

	// then方法处理promise构造中的传递的结果
	then(resolve, reject) {	
		// 1. 判断该当前的状态
		if(this.status === RESOLVE) {
			resolve(this.res_success);
		} else if(this.status === REJECT) {
			reject(this.res_fail);
		} else if(this.status === PENDING) {
			// todo
		}
	}
}

使用promise

let promise = new MyPromise((res, rej)=>{
	// let result_success = 'task';
	let result_fail = 'err';
	// res(result_success);
	res(result_fail);
});

promise.then(data=>{
	console.log(1, data);
},err=>{
	console.log(1, err);
})

promise.then(data=>{
	console.log(2, data);
},err=>{
	console.log(2, err);
})

promise.then(data=>{
	console.log(3, data);
},err=>{
	console.log(3, err);
})

运行结果:

  1. 调用成功函数res在这里插入图片描述

  2. 调用失败函数rej在这里插入图片描述

  3. 用户自己抛出异常
    在这里插入图片描述

总结:

  • 该promise实现了用户的简单同步任务的调用。
  • 可以捕获用户的执行异常。
  • 存在缺陷
    1. 不能实现用户的异步任务
    2. 不能实现用户的链式调用

2. 增加异步任务的支持

promise类

// promise的三个状态
const PENDING = 'pending';	// 等待态
const RESOLVE = 'resolve';	// 成功
const REJECT = 'resject';	// 失败
	
class MyPromise {
	constructor(execute) {
		// 初始化状态为 pending
		this.status = PENDING;

		// 保存成功和失败的结果
		this.res_success = '';
		this.res_fail = '';

		// 存放所有成功和失败的回调函数
		this.arr_success = [];
		this.arr_fail = [];

		// 定义成功执行的函数
		let resolve = (data) =>{
			this.res_success = data;
			this.status = RESOLVE;

			// 一次执行成功回调函数
			this.arr_success.forEach(success=>{
				this.then(success);
			})
		}

		// 定义失败的函数
		let reject = (err) =>{
			this.res_fail = err;
			this.status = REJECT;

			// 一次执行失败的回调函数
			this.arr_fail.forEach(err=>{
				this.then(null, err);
			});
		}

		// 用户可能自己向外抛出异常
		try {
			// 执行用户的自定义函数
			execute(resolve, reject);
		} catch(err) {
			reject(err);
		}
	}

	// then方法处理promise构造中的传递的结果
	then(resolve, reject) {	
		// 1. 判断该当前的状态
		if(this.status === RESOLVE) {
			resolve(this.res_success);
		} else if(this.status === REJECT) {
			reject(this.res_fail);
		} else if(this.status === PENDING) {
			// 存放成功和失败回调,当异步任务执行完成之后在执行这些回调
			this.arr_success.push((data)=>{
				// todo
				resolve(data);
			});
			this.arr_fail.push((err)=>{
				// todo
				reject(err);
			});
		}
	}
}

执行任务

let promise = new MyPromise((res, rej)=>{
	setTimeout(function(){
		res('定时器任务');
		rej('出错了');
		// throw new Error('用户抛出异常');
	},2000);
});

运行结果:

  1. 成功的回调在这里插入图片描述
  2. 用户自己抛异常在这里插入图片描述

分析:

  • 有人可能觉得很奇怪,我们是对错误异常进行了错误捕获,但是浏览器却告诉我们运行出错了,为什么?
    原因很简单,我们回过头看一看这行代码:
    // 用户可能自己向外抛出异常
    try {
    	// 执行用户的自定义函数
    	execute(resolve, reject);
    } catch(err) {
    	reject(err);
    }
    
    我们仔细分析一下,try catch 是不是同步函数,既然是同步函数那么同步函数怎么能捕获到异步的抛错呢?
    接下来继续完善我们的promise

3. 改进异步捕获

promise修改部分

constructor(execute, asyncFun) {
		// 定义一个信号
		let signal = (data) =>{
			try{
				asyncFun(data);
			}catch(e) {
				reject(e);
			}
		}

		// 用户可能自己向外抛出异常
		try {
			// 执行用户的自定义函数
			execute(resolve, reject, signal);
		} catch(err) {
			reject(err);
		}
	}

执行任务

let promise = new MyPromise((res, rej, signal)=>{
	setTimeout(function(){
		signal('出错了');	// 触发信号
	},0);
}, function(data) {
	throw new Error(data);
});

运行结果:
在这里插入图片描述

思路:

  • 目前我们没有直接的办法对异步的任务进行捕获。
  • 我使用到了Linux中的 信号 思想,既然异步任务不会以及执行,那么我们不妨将异步任务存储起来,通过一个 信号 来告诉我什么时候执行。
  • 使用上述的思想,我们就可以将异步任务中的 核心逻辑 抽离出来进行捕获。
在实际中的promise中,构造函数中不支持抛出异常,更不支持异步的抛出异常,当然仔细想一想也有道理,如果出错直接使用 reject 失败回调即可。但是我增加这个功能不是为了显示自己的能力,而是提供给大家一种处理异步操作的一种思想,当然可能存在很多漏洞,希望大家多多包容,多多指教。

三、实现then的链式调用

1. 原理

链式调用最大的一个特点:上一个then可以返回 ,该 可以传递到下一个then方法中。同时链式调用无非就是 then 方法发挥了一个对象,该对象上拥有then 方法,从而支持链式调用,那么我们的目的首先明确一下:

  1. 调用 then 方法返回一个对象
  2. 该对象需要保存 then 中返回的值
  3. 该对象必须含有自己的 then 方法

综上目的,可以直接在 then 方法中返回一个promise实例。

2. 简易链式调用

promise类修改部分

// then方法处理promise构造中的传递的结果
then(resolve, reject) {	
	let promise2 = new MyPromise((res, rej)=>{  // new的过程会立即执行构造函数中函数
		// 1. 判断该当前的状态
		if(this.status === RESOLVE) {
			try {
				let x = resolve(this.res_success);
				res(x);
			} catch(e) {
				reject(e);
			}
		} else if(this.status === REJECT) {
			try {
				let x = reject(this.res_fail);
				rej(x);
			} catch(e) {
				reject(e);
			}
		} else if(this.status === PENDING) {
			// 存放成功和失败回调,当异步任务执行完成之后在执行这些回调
			this.arr_success.push((data)=>{
				// todo
				let x = resolve(data);
				res(x);
			});
			this.arr_fail.push((err)=>{
				// todo
				let x = reject(err);
				rej(x);
			});
		}
	});

	// 返回promise对象,实现链式调用
	return promise2;
}

执行任务

let promise = new MyPromise((res, rej)=>{
	setTimeout(()=>{
		res(1);
	});
}).then(data=>{
	console.log(1);
	return 2
}, err=>{
	console.log(err);
}).then(data=>{
	console.log(data)
	return 3;
}, err=>{
	console.log(err);
}).then(data=>{
	setTimeout(()=>{
		console.log(data);
		console.log(4);
	})
})

运行结果:在这里插入图片描述
分析:

  • 存在以下问题:
    1. 无法返回 promise 对象,只能返回基本数据类型和一般对象({xx:xx})。

3. 对then中返回的promise进行处理

promise修改部分

// then方法处理promise构造中的传递的结果
then(resolve, reject) {	
	let promise2 = new MyPromise((res, rej)=>{  // new的过程会立即执行构造函数中函数
		setTimeout(()=>{
			// 1. 判断该当前的状态
			if(this.status === RESOLVE) {
				try {
					let x = resolve(this.res_success);
					resolvePromise(promise2, x, res, rej);
				} catch(e) {
					reject(e);
				}
			} else if(this.status === REJECT) {
				try {
					let x = reject(this.res_fail);
					resolvePromise(promise2, x, res, rej);
				} catch(e) {
					reject(e);
				}
			} else if(this.status === PENDING) {
				// 存放成功和失败回调,当异步任务执行完成之后在执行这些回调
				this.arr_success.push((data)=>{
					// todo
					let x = resolve(data);
					resolvePromise(promise2, x, res, rej);
				});
				this.arr_fail.push((err)=>{
					// todo
					let x = reject(err);
					resolvePromise(promise2, x, res, rej);
				});
			}
		}, 0);	// 0 实际上所用的时间>=4ms
	});

	// 返回promise对象,实现链式调用
	return promise2;
}

resolvePromise函数

function resolvePromise(promise2, x, res, rej) {
	// 如果 闯将的对象 和 返回的对象是同一个对象,那么需要抛出错误
	if(promise2 === x) {
		return rej(new TypeError());
	}

	if(typeof x === 'object' && x !== null) {
	// 如果是promise,则需要调用promise中的then方法
		// 获取then方法
		x.then((data)=>{
			res(data);	// 将成功的值传递给promise2
		},(err)=>{
			rej(err);	// 将失败的值传递给promise2
		})
	} else {
	//如果是普通值直接调用res
		res(x);
	}
}

分析:

  • 在promise a+规范中,我们需要判断用户返回的promise是否是和实例是同一个对象,如果是同一个对象,那么就需要返回 TypeError 错误。
  • 如何获取到实例对象,我们必须知道 new执行完成之后才会返回实例对象,因次我采用了定时器的方法,将构造函数变成为宏任务,那么就可以在构造函数中获取到实例对象。

以上基本上算是已经完成了一个简易版的promise,但是现在还有实现promise的静态方法,接下来我会将promise中的一些基本的静态方法实现一下。

四、静态方法all实现

promise函数

// all 方法的实现
MyPromise.all = (arr)=>{
	return new Promise((resolve, reject)=>{
		let res_arr = [];
		let count = 0;

		// 计数器
		function countFun() {
			if(++count === arr.length) {
				resolve(res_arr);
			}
		}

		// 遍历arr中的成员
		for(let i = 0; i < arr.length; i++) {
			if(isPromise(arr[i])) {
			// 如果是promise
				arr[i].then((data)=>{
					res_arr[i] = data;
					countFun();
				},reject);
			} else {
			// 普通值
				res_arr[i] = arr[i];
				countFun();
			}
		}
	})
}

// 判断是否是promise
function isPromise(obj) {
	if(typeof obj !== 'object') return false;
	if(obj instanceof MyPromise) {
		return true;
	} else {
		return false;
	}
}

任务执行

let promise = new MyPromise((res, rej)=>{
	setTimeout(()=>{
		res('你好我是promise');
	}, 1000);
})
MyPromise.all([1, 2, promise, 3, 4]).then(data=>{
	console.log(data);
}, err=>{
	console.log(err);
})

运行结果:在这里插入图片描述
分析:

  • 我们每日有办法直接得到 all 静态函数返回的结果,那么我们就需要使用then方法。
  • 我们如何才能更高效的判断是否全部执行完成,不是遍历,而是计数器。
  • 当计数器到达最大的任务数量之后便会触发resolve函数,将执行的结果传递给then函数中。

五、最终代码整理

// promise的三个状态
const PENDING = 'pending';	// 等待态
const RESOLVE = 'resolve';	// 成功
const REJECT = 'resject';	// 失败
	
class MyPromise {
	constructor(execute) {
		// 初始化状态为 pending
		this.status = PENDING;

		// 保存成功和失败的结果
		this.res_success = '';
		this.res_fail = '';

		// 存放所有成功和失败的回调函数
		this.arr_success = [];
		this.arr_fail = [];

		// 定义成功执行的函数,目的保存用户传输的成功的值,同时调用异步函数
		let resolve = (data) =>{
			this.res_success = data;
			this.status = RESOLVE;

			// 一次执行成功回调函数
			this.arr_success.forEach(success=>{
				this.then(success);
			})
		}

		// 定义失败的函数,目的保存用户传输的失败的值,同时调用异步函数
		let reject = (err) =>{
			this.res_fail = err;
			this.status = REJECT;

			// 一次执行失败的回调函数
			this.arr_fail.forEach(err=>{
				this.then(null, err);
			});
		}

		// 用户可能自己向外抛出异常
		try {
			// 执行用户的自定义函数
			execute(resolve, reject);
		} catch(err) {
			reject(err);
		}
	}

	// then方法处理promise构造中的传递的结果
	then(resolve, reject) {	
		let promise2 = new MyPromise((res, rej)=>{  // new的过程会立即执行构造函数中函数
			setTimeout(()=>{
				// 1. 判断该当前的状态
				if(this.status === RESOLVE) {
					try {
						let x = resolve(this.res_success);
						resolvePromise(promise2, x, res, rej);
					} catch(e) {
						reject(e);
					}
				} else if(this.status === REJECT) {
					try {
						let x = reject(this.res_fail);
						resolvePromise(promise2, x, res, rej);
					} catch(e) {
						reject(e);
					}
				} else if(this.status === PENDING) {
					// 存放成功和失败回调,当异步任务执行完成之后在执行这些回调
					this.arr_success.push((data)=>{
						// todo
						let x = resolve(data);
						resolvePromise(promise2, x, res, rej);
					});
					this.arr_fail.push((err)=>{
						// todo
						let x = reject(err);
						resolvePromise(promise2, x, res, rej);
					});
				}
			}, 0);	// 0 实际上所用的时间>=4ms
		});

		// 返回promise对象,实现链式调用
		return promise2;
	}
}

// all 方法的实现
MyPromise.all = (arr)=>{
	/*
		如何得到最终的结果,因为存在异步任务,我们不能直接得到结果,需要借助其他手段
			此时我们想到了then,then方法可以得到异步结果,
			只有当resolve执行之后 then 才能得到异步结果,
			那么最终我们需要借助resolve和then。

		for遍历arr中的所有成员,目的是为了将arr中的函数或者值进行遍历。
			if是普通值就直接存在一个res数组中

			if是promise对象,那么就对promise对象就行操作
				调用promise对象中的then方法,将data参数传入res中
	 */
	return new Promise((resolve, reject)=>{
		let res_arr = [];
		let count = 0;

		// 计数器
		function countFun() {
			if(++count === arr.length) {
				resolve(res_arr);
			}
		}

		// 遍历arr中的成员
		for(let i = 0; i < arr.length; i++) {
			if(isPromise(arr[i])) {
			// 如果是promise
				arr[i].then((data)=>{
					res_arr[i] = data;
					countFun();
				},reject);
			} else {
			// 普通值
				res_arr[i] = arr[i];
				countFun();
			}
		}
	})
}

// 判断是否是promise
function isPromise(obj) {
	if(typeof obj !== 'object') return false;
	if(obj instanceof MyPromise) {
		return true;
	} else {
		return false;
	}
}

function resolvePromise(promise2, x, res, rej) {
	// 如果 闯将的对象 和 返回的对象是同一个对象,那么需要抛出错误
	if(promise2 === x) {
		return rej(new TypeError());
	}

	if(typeof x === 'object' && x !== null) {
	// 如果是promise,则需要调用promise中的then方法
		// 获取then方法
		x.then((data)=>{
			res(data);	// 将成功的值传递给promise2
		},(err)=>{
			rej(err);	// 将失败的值传递给promise2
		})
	} else {
	//如果是普通值直接调用res
		res(x);
	}
}

六、模拟async和await(额外研究)

1、迭代器

let obj = {
	0: 'hello',
	1: 'world',
	length: 2,
	[Symbol.iterator]() {
		let index = 0;
		return {
			next: ()=>{
				return {
					value: this[index],
					done: this.length === index++
				}
			}
		}
	}
}
console.log([...obj]); // ['hello','world']

2、生成器

let obj = {
	0: 'hello',
	1: 'world',
	length: 2,
	[Symbol.iterator]() {
		for(let i = 0; i < this.length; i++) {
			yield this[i];
		}
	}
}

3、小例子

function * test() {
	let a = yield 'hello';
	console.log(a);
	let b = yield 'world';
	console.log(b);
}

let it = test();
console.log(it.next());
console.log(it.next());
console.log(it.next());

运行结果:在这里插入图片描述
分析:

  • 生成器函数在调用后会返回一个 迭代对象
  • 迭代对象 含有一个 next 方法,调用 next 方法之后,会将遇到的第一个 yield 关键子之后的 值 包装成 {value:xx, done:xx},并且将其输出。同时执行也会停止在 yield 处。
  • 分析上面的第一个undefined,因为当遇到第一个 yield 时,便会停止,也即此时的 a 没有赋值,因此第二次调用 next 方法后,输出的 a 就是 undefined 。

function * test() {
	let a = yield 'hello';
	console.log(a);
	let b = yield 'world';
	console.log(b);
}

let it = test();
console.log(it.next());
console.log(it.next(1));
console.log(it.next(2));

运行结果:在这里插入图片描述
分析:

  • next 函数中可以接受一个参数, 这个参数的作用是,将该参数赋值给 上一个 yield的返回值。

2. await和async的模拟

上面已经介绍了生成器和迭代器的用法和特点,那么接下来就可以开始模拟async和await。
原理就是 生成器结合promise

① 简易版

function timeFun(data) {
	return new Promise((res, rej)=>{
		setTimeout(()=>{
			res(data);
		}, 1000);
	});
}

function * getRes() {
	let a = yield timeFun('hello,');
	let b = yield timeFun('world');
	let c = yield timeFun('我是一个快乐的前端工程师');
	return a+b+c;
}

let it = getRes();
it.next().value.then(data=>{ 
	console.log(data);
	it.next(data).value.then(data=>{ 
		console.log(data);
		it.next(data).value.then(data=>{
			console.log(data);
			let {value,done} = it.next(data);
			console.log(value);
		})
	}); 
})

运行结果:在这里插入图片描述
分析:

  • 每次调用之后会返回一个value,value为promise对象。
  • 我们存在一个问题,得到结果的过程存在很大的重复性,我们可以使用递归简化该过程。

② 改良版

function timeFun(data) {
	return new Promise((res, rej)=>{
		setTimeout(()=>{
			res(data);
		}, 1000);
	});
}

function * getRes() {
	let a = yield timeFun('hello,');
	let b = yield timeFun('world。');
	let c = yield timeFun('我是一个快乐的前端工程师!');
	return a+b+c;
}

function resFun(data) {
	let {value,done} = it.next(data);
	if(!done) {
		value.then(data=>{
			resFun(data);
		})
	} else {
		console.log(value);
	}
}

let it = getRes();
resFun();

分析:

  • 上述的代码通过递归已经简单的实现了模拟了 async 和 await 执行过程。

七、完结

这一篇博客,我一步一步的逐渐向深挖掘promise,当然最终的 promise 可能存在某些bug,需要读者见谅,同时我有错误的地方,十分欢迎读者给与指导。同时在这个过程中我总结了很多心得,希望跟大家分享。

  • 源码的阅读方法一定要避免 假大空,自己在网上随便找一篇文章或者博客然后就看是阅读,很多知识都是被动的吸收,自己又能真正理解多少,而且有的教程一上来就是 直奔 主题,那么自己可能又是一脸懵逼,没有人能一口吃个大胖子,循序渐进才是王道。
  • 代码的基础十分重要,就像盖房子一样,只有基础打扎实,所谓的基础打扎实就是需要在代码的构建初期需要把所有可能发生的问题全部做出相应的解决方案,为以后的增进打下了坚实的基础,否则越往后越难以控制。
  • 写代码的时候不要闷着头往前写,停下来下向后看一看,也许你会发现自己的某些代码可以复用,从而提高开发的效率。
  • 在处理异步的过程中,我得到了两种解决方案:
    1. 信号:适合单独异步操作。
    2. 计数器: 适合处理多个异步操作。

以上是我在研究 promise 过程中得到的一些心得体会。同时欢迎大家给我提一提宝贵的意见,促进大家一起进步。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值