ES6学习教程之Promise用法详解


Posted in Javascript onNovember 22, 2020

前言

promise用了这么多年了,一直也没有系统整理过。今天整理整理promise的相关东西,感兴趣的可以一起看一看。我尽量用更容易理解的语言来剖析一下promise

我准备分两篇文章来说明一下promise

一篇来理解和使用promise(本篇) 另一篇来从promise使用功能的角度来剖析下promise的源码(下一篇)

1、什么是Promise

我的理解是:实现让我们用同步的方式去写异步代码的一种技术。是异步解决方案的一种。

他可以将多个异步操作进行队列化,让它们可以按照我们的想法去顺序执行。

那么,Promise之前有没有其他的异步解决方案。肯定是有的,常见的有callback回调函数以及事件。

那Promise有啥优势,我认为Promise功能更为强大,且能让我们代码写的更为清晰

  • Promise提供了统一的API, 让我们控制异步操作更加容易
  • Promise可以避免callback回调函数的层层嵌套,使代码更为清晰。可读性性与维护性更高

2、Promise基本用法

首先,我们先来了解一些Promise的基本概念

2.1、Promise状态

Promise一共有3中状态,分别是Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败)
状态的改变只可能从Pending转------>Resolved,或者从Pending------->Rejected。并且状态一旦发生改变,就不会再更改了。而触发状态发生改变的,只有异步操作的结果。结果为成功 触发状态变更为 Resolved, 结果失败或者中途发生错误,则会触发状态变更为 Rejected

2.2 Promise结构

Promise是一个构造函数,故通过new Promise()可以实例化出来一个Promise对象

new Promise()时,接受一个函数作为参数,且这个函数,有两个参数,分别是resolve,reject。 而resolve和 reject也是两个函数。他们由JavaScript引擎提供,不用自己部署。

每一个被实例化出来的promise实例,都有.then() 和 .catch() 两个方法。且这两个方法的调用支持链式操作

好,了解完概念,我们看看Promise的基本用法

首先,如何实例化一个promise对象

const promise = new Promise((resolve, reject) => {
	setTimeout(() => {
		if (/* 成功 */) {
			resolve(res)
		} else {
			reject(err)
		}
	}, 100)
})

上图中,通过new Promise() 实例化了一个promise实例,注意:new Promise()方法中的函数是一个立即执行函数,即,在new Promise()的一瞬间就会被执行。函数内代码是同步代码。

resolve和reject用于返回异步操作的结果,当使用resolve()时,promise状态会由Pending—>Resolved, 并将异步的正确结果返回。当使用reject()时,promise状态由Pending---->Rejected,并将错误信息返回

再看这个对象如何接收返回的结果

promise.then((res) => {
	console.log(res)
}).catch((err) => {
	console.log(err)
})

上图中,.then的回调函数 和 .catch的回调函数分别用来接收resolve()返回的正确信息和reject返回的错误信息。

下面我们来详细看下.then() 和 .catch()

.then() 函数

then()函数是Promise实例的一个方法,他的作用是为Promise实例添加状态改变时的回调函数
它存在以下特点

  1. then()是添加在Promise的原型上的。即Promise.prototype.then(), 故所有Promise实例都存在.then()方法
  2. .then()可以进行链式操作 即promise.then().then().then(),then的回调函数将会按照次序调用
  3. .then()函数存在两个参数,这两个参数一般情况下是函数。其中,第一个函数是在状态变为Resolved的时候才会执行(我们下文中统称为.then的resolve回调),并且参数是Promise对象resolve(res)时的值。第二个函数是在状态变为Rejected的时候才会执行(我们下文统称为.then的reject回调),后面我们会说哪几种情况下,状态会变成Rejected
  4. Promise会存在值穿透的情况,当我们then()的两个参数不为函数时,会穿透到下一个then()里面,如果下一个then()参数也不是函数,则会继续向下穿透
  5. 我们上面说过了,Promise实例resolve()方法执行时,会将实例的状态变更为Resolved,故.then的resolve回调会在当前Promise实例resolve()时被触发

下面,我们重点来分析下第2,3,4,5

function getData(url) {
 return new Promise((resolve, reject) => {
 setTimeout(() => {
  if (url) {
  resolve({
   code: 200,
   message: 'ok',
   data: 123
  })
  } else {
  reject(new Error('缺少url'))
  }
 }, 100)
 })
}
getData('http://www.baidu.com').then((res) => {
 console.log('第一个回调')
 console.log(res)
}).then((res) => {
 console.log('第二个回调')
 console.log(res)
}).then((res) => {
 console.log('第三个回调')
 console.log(res)
})
// 第一个回调
// { code: 200, message: 'ok', data: 123 }
// 第二个回调
// undefined
// 第三个回调
// undefined

可以看出,首先,当getData() resolve() 执行时 .then的resolve回调函数被依次调用,但是只有第一个then()的resolve回调函数的参数有值,而其他两个是undefind,这是为什么呢?我们再来看一个代码

getData('http://www.baidu.com').then((res) => {
 console.log('第一个回调')
 console.log(res)
 return Promise.resolve()
}).then((res) => {
 console.log(res)
})
// 第一个回调
// { code: 200, message: 'ok', data: 123 }
// undefined

看这个代码我们可以发现,上一个then的resolve回调当return一个Promise.resolve()时,和我们不return 任何东西时得到的结果是一样的。那我们是不是可以理解为,每个.then()方法的resolve回调函数,执行完后默认都会返回一个Promise.resolve()。没错,我告诉你,是的。

至于Promise.resolve()得到的是一个什么,我先告诉你,他得到的是一个resolve状态的Promise实例。这个后面我们会再讲。

此时,我们可以总结出:从第二个.then()开始,调用这个.then的resolve回调函数的-----是上一个.then的resolve回调所返回的Promise实例。而.then回调函数的参数,便是上一个.then的回调函数所返回的Promise实例resolve的值。下面我们看一段代码验证一下

getData('http://www.baidu.com').then((res) => {
 console.log('第一个回调')
 console.log(res)
 return new Promise((resolve, reject) => {
 resolve('123')
 })
}).then((res) => {
 console.log(res)
})

// 第一个回调
// { code: 200, message: 'ok', data: 123 }
// 123

总结:

  1. 每一个.then的resolve回调都会返回默认返回一个Resolved状态的Promise对象
  2. 当你收到return了一个新的Promise实例时,会覆盖默认返回的Promise实例
  3. 返回的Promise实例resolve()的值,会作为下一个.then的resolve回调的参数返回

下面我们再来看下,如果then()的参数不是函数,那会怎么样,下面,我们看一段代码

var getData = function() {
 return new Promise(function(resolve, reject) {
  resolve(123)
 });
};
getData().
then(345)
.catch(err => {
 console.log('捕捉到一个错误')
 console.log(err)
}).then((res) => {
 console.log('我是第二个then')
 console.log(res)
})
// 输出
我是第二个then
123

如上图,可以看到,当我们第一个then的resolve回调不是函数,而是一个数字345时,resolve(123)穿透到第二个then中了,触发了第二个then的resolve回调执行,并将resolve的返回值给了第二个then的resolve回调。这种现象,叫做值穿透。

var getData = function() {
 return new Promise(function(resolve, reject) {
  reject(new Error(123))
 });
};
getData().
then(345)
.catch(678)
.then((res) => {
 console.log('我是第二个then')
 console.log(res)
}).catch(err => {
 console.log('我是第二个catch')
 console.log(err)
})
// 输出
我是第二个catch
Error: 123

可以看到,报错时,同样发生了值穿透

到此,.then()相关以及 then()的第一个参数就讲完了,而第二个参数,我们放到.catch()方法中一起将

.catch() 函数

catch()也是挂载在Promise对象原型下的方法(Promise.prototype),和then()一样, 故所有Promise对象也都有catch方法。它的作用是用来指定发生错误时的回调函数,也就是捕获异步操作所发生的错误
它有什么特点呢。我们先总结一下,后来再一一来验证

  1. .catch()会指定一个参数作为错误发生时的回调,故catch((err) => {})的参数会在Promise状态变更为Rejected时被触发。
  2. .then(null, (err) => {})的第二个参数,也是在Promise状态变更为Rejected时被触发。故其实.catch()和 .then()的reject回调函数本质上是一样的,只是写法不一样。但我们一般更倾向于使用.catch()而不使用.then的reject回调。原因后面会讲
  3. 代码抛出错误和reject()函数执行都会让Promise对象的状态转变为Rejected,故两种情况都会触发catch()的回调执行或者then()的reject回调执行。 所以,reject()的本质,其实就是抛出一个错误
  4. .catch()的回调函数以及.then的reject回调一样,执行时默认都会返回一个状态为Resolved的Promise对象(也就是 return Promise.resolve())
  5. .catch()和.then()一样,也可以写多个,也支持链式操作,原因就是上面的第三点
  6. 抛出的错误一旦被catch捕获,便不会再向外传播,只有再次向外抛出错误,才会继续被后面的catch所捕获。故错误具有冒泡性质,会一步一步向外传播,直到被catch捕获

1、我们先看第一点:

var getData = function() {
 return new Promise(function(resolve, reject) {
  reject(123)
 });
};
getData().then((res) => {
 console.log('成功')
}).catch((err) => {
 console.log('捕捉到错误')
 console.log(err)
})

// 捕捉到错误
// 123

毫无疑问,reject(123)抛出一个错误,catch的回调捕捉到错误,并输出

2、再看第二点:

var getData = function() {
 return new Promise(function(resolve, reject) {
  reject(123)
 });
};
getData().then((res) => {
 console.log('成功')
}, (err) => {
	console.log('捕捉到一个错误')
	console.log(err)
})

// 捕捉到错误
// 123

从代码上也可以看出,上面这两种方式是一样的。

现在,我来说说为什么建议使用catch() ,而不推荐使用then()的reject回调呢。看下下面的代码

var getData = function() {
 return new Promise(function(resolve, reject) {
  resolve(123)
 });
};
getData().then((res) => {
 console.log('成功')
 return new Promise((resolve, reject) => {
  reject(new Error('123'))
 })
}, (err) => {
	console.log('捕捉到一个错误')
	console.log(err)
})

// 成功

此时,只输出了成功, 而then的resolve回调中所抛出的错误,并没有被捕捉到

再看下面一段代码

var getData = function() {
 return new Promise(function(resolve, reject) {
  resolve(123)
 });
};
getData().then((res) => {
 console.log('成功')
 return new Promise((resolve, reject) => {
  reject(new Error('123'))
 })
}).catch((err) => {
	console.log('捕捉到一个错误')
	console.log(err)
})

成功
捕捉到一个错误
Error: 123

看,同样的错误,但是使用catch(),可以捕捉到,而使用then()的reject回调,却捕捉不到。

结论:catch()可以通过放到操作链的最底部而捕捉到任意地方(指的是Promise内)的错误。而then()的reject回调,只能捕捉到这个.then()执行之前的错误,当前执行的then的resolve回调内的错误无法捕捉到,后面再执行的代码所抛出的错误也无法捕捉到。并且.catch的写法,代码层面也更为清晰

var getData = function() {
 return new Promise(function(resolve, reject) {
  resolve(123)
 });
};
getData().then((res) => {
 console.log('成功')
 return new Promise((resolve, reject) => {
  reject(new Error('123'))
 })
}, (err) => {
 console.log('第一个错误捕捉')
}).then((res) => {
 console.log('第二个resolve回调')
}, err => {
 console.log('第二个错误捕捉')
})

成功
第二个错误捕捉

如上图中,第一个then的resolve回调中抛出的错误被第二个then中reject回调所捕捉

故 结论:一般情况下,不要去用then的第二个参数,而尽可能的去用.catch()方法去捕捉错误

3、下面我们再看第三点

var getData = function() {
 return new Promise(function(resolve, reject) {
  resolve(x)
 });
};
getData().then((res) => {
 console.log('成功')
}).catch(err => {
 console.log('捕捉到一个错误')
 console.log(err)
 throw new Error('我抛出了一个错误')
}).catch(err => {
 console.log('我也捕捉到了一个错误')
 console.log(err)
})

捕捉到一个错误
ReferenceError: x is not defined
我也捕捉到了一个错误
Error: 我抛出了一个错误

上面代码可以看出,不是只有reject()执行了才会抛出一个错误,x未定义,系统会自动抛出一个错误,throw new Error是我们自己手动抛出一个错误。而这些都会使得Promise对象的状态变更为Rejected,从而触发catch。
同时上面的代码我们还可以看出我们上面写的第六点,错误会冒泡式向外传播,当被catch之后,便不会再进行传播了。直到再次抛出错误。上面代码中,第一个错误被第一个catch捕获后,原本第二个catch是不会再走的,但因为在第一个catch中又抛出了一个错误,才导致了第二个catch的执行。

4、下面我们再看第四点(catch()的回调函数也会返回一个状态是Resolved的Promise实例)
其实这一点,我们从上面那张图中也是可以看出来的,第一个catch()的回调原本是要返回了一个Resolved状态的Promise,但是因为throw了一个错误,导致这个Promise实例状态变更为Rejected并返回,而变成成Rejected变触发了第二个catch的回调执行

我们看下下面的代码,再次验证下

var getData = function() {
 return new Promise(function(resolve, reject) {
  resolve(x)
 });
};
getData().then((res) => {
 console.log('成功')
}).catch(err => {
 console.log('捕捉到一个错误')
 console.log(err)
}).then((res) => {
 console.log('我是第二个then')
})

捕捉到一个错误
ReferenceError: x is not defined
我是第二个then

上面代码可以看出,catch的回调执行后,后面的then依然被执行了,为什么,就是因为catch的回调执行后默认返回了一个Resolved状态的Promise实例(return Promise.resolve())

第五点,第六点我们已经验证过了。不再多说。

实现简单的axios

axios我们比较常用,大家应该都发现了,axios的使用方式,和Promise好像是一样的,

axios({
 url:'http://www.baidu.com',
 method: 'post',
 data: {}
}).then((res) => {
 console.log(res)
}).catch((err) => {
 console.log(err)
})

没错。axios就是一个Promise实例。他是一个用Promise来封装的一个XMLHttpRequest
下面我们也来实现一个简单的axios

function MyAxios(option) {
 return new Promise((resolve, reject) => {
  const http = new XMLHttpRequest()
  http.open(option.method, option.url);
  http.responseType = "json";
  http.setRequestHeader("Accept", "application/json");
  http.onreadystatechange = myHandler;
  http.send();

  function myHandler() {
   if (this.readyState !== 4) {
    return;
   }
   if (this.status === 200) {
    resolve(this.response);
   } else {
    reject(new Error(this.statusText));
   }
  }
 })
}
MyAxios({
 url:'http://www.baidu.com',
 method: 'post'
}).then((res) => {
 console.log(res)
}).catch((err) => {
 console.log(err)
})

Promise.all, Promise.race 以及两者的区别

1、Promise.all

Promise.all()可以并行执行多个Promise(), 并返回一个新的Promise实例

var p = Promise.all([p1, p2, p3]); // p1,p2,p3为3个Promise实例

Promise.all()的参数不一定是数组,只要具有Iterator接口的数据都可以(Iterator是一个遍历器,我这里就不做过多介绍,感兴趣的可以自己去官网看看)。但是参数遍历后返回的成员必须必须是Promise对象(如上面的,p1,p2,p3都必须是Promise对象,如果不是,则会先调用Promise.resolve(p1)将他转化为Promise实例)

那么,Promise.all()返回的Promise实例的状态是如何定义的。

  • 只有参数的各个成员(p1,p2,p3)状态都变成Resolved,p的状态才会变成Resolved,
  • 参数的各个成员中,有任意一个状态变成Rejected, p的状态都会立刻变成Rejected
function getData (data) {
 return new Promise((resolve, reject) => {
  setTimeout(() => {
   if (data === 6) {
    reject(new Error('请求发生错误了'))
   } else {
    resolve(data)
   }
  }, data * 500)
 })
}
const promises = [1,3,5,7].map((item) => {
 return getData(item)
})
Promise.all(promises)
.then((res) => {
 console.log('请求成功')
 console.log(res)
}).catch((err) => {
 console.log('请求失败')
 console.log(err)
})

// 3.5s后输出
请求成功
[ 1, 3, 5, 7 ]

如上图, 最后一个成员(上图中7返回的promise实例)的状态是在3.5s后才变更为Resolved,故.then()的resolve回调在3.5s后才执行

function getData (data) {
 return new Promise((resolve, reject) => {
  setTimeout(() => {
   if (data === 6) {
    reject(new Error('请求发生错误了'))
   } else {
    resolve(data)
   }
  }, data * 500)
 })
}
const promises = [2,4,6,8].map((item) => {
 return getData(item)
})
Promise.all(promises)
.then((res) => {
 console.log('请求成功')
 console.log(res)
}).catch((err) => {
 console.log('请求失败')
 console.log(err)
})

// 3s后输出
请求失败
Error: 请求发生错误了

上图可以看出,当我们改用 2,4,6,8去得到promise成员时,第3s得时候 发生了错误,此时,Promise.all()返回得Promise实例得状态立刻变更为Rejected,catch()的回调立即触发。故输出错误

2、Promise.race()

Promise.race()和Promise.all()的作用是一样的,都是并发处理多个Promise实例,并返回一个新的实例。
而区别在于,两者返回的新的Promise实例的状态改变的时机不同。

Promise.all是 所有Promise子成员状态都变为Resolved, 新的Promise实例状态才会变成Resolved。中途如果有任何一个子成员状态变成了Rejected,新的Promise实例的状态就会立刻变为Rejected

Promise.race是 只要子成员中,有任何一个的状态发生了变化(不管是变成Resolved还是Rejected),那么返回的新的Promise实例的状态也会立刻发生变化,而变化的状态就是那个子成员所变化的状态。

function getData (data) {
 return new Promise((resolve, reject) => {
  setTimeout(() => {
   if (data === 6) {
    reject(new Error('请求发生错误了'))
   } else {
    resolve(data)
   }
  }, data * 500)
 })
}
const promises = [2,4,6,8].map((item) => {
 return getData(item)
})
Promise.race(promises)
.then((res) => {
 console.log('请求成功')
 console.log(res)
}).catch((err) => {
 console.log('请求失败')
 console.log(err)
})

// 1s后输出
请求成功
2

上图可以看出,1s后 第一个子成员状态变更为Resolved,那么返回的新Promise实例状态也立马变更为Resolved,故1s后.then()的resolve回调执行。输出请求成功.

最后,我们来说一说前面用到了的Promise.resolve()吧

Promise.resolve和Promise.reject

Promise.resolve()

前面我们说到过 Promise.resolve可以返回一个状态是Resolved的Promise对象。没错,其实它等同于

new Promise((resolve, reject) => {
 resolve()
})

当Promise.resolve()有参数时,会返回一个Promise对象的同时,将参数做作为then()resolve回调的参数返回(当参数是thenable对象除外,后面会将)。主要有以下几种情况

1、参数是一个Promise对象时

将会直接返回这个参数,不做任何更改

2、参数是thenable对象时,(即,存在.then()方法的对象),如下

let obj= {
 then: function(resolve, reject) {
 resolve('我是thenable对象');
 }
};

此时,Promise.resolve(obj) 会返回一个Promise对象,并且调用obj的then()方法,哎,这里注意了,这个.then()并不是 新Promise对象的.then() , obj的then()会立即执行,可不代表 新的Promise对象的then() 的回调也会执行, 还记得吗,我们前面说的Promise对象的then()的回调执行的条件是这个Promise对象的状态发生变化了才会执行。

let obj= {
 then: function(resolve, reject) {
  console.log(123)
 }
};
 
let p1 = Promise.resolve(obj);
p1.then(function(value) {
 console.log('成功')
 console.log(value); // 42
});
// 输出
123

从上图可以看出来,立即执行了obj.then(),但Promise的then的回调并没有被执行

3、参数不是对象,或者说是没有.then方法的对象

会返回一个Promise实例,并将参数作为.then()的resolve回调的参数返回

如,Promise.resolve(‘123') 等价于

new Promise((resolve, reject) => {
 resolve('123')
})

4、不带参数,即Promise.resolve(),也就是我们前面说的。

返回了一个Resolved状态的Promise对象,但是.then()的resolve回调没有参数。

new Promise((resolve, reject) => {
 resolve()
}).then((res) => {
	console.log(res)
})
// 输出
undefined

Promise.resolve()

Promise.reject() 也是返回一个Promise对象,只是这个对象的状态是Rejected
至于参数的用法和Promise.resolve()完全一样,唯一的区别是没有thenable参数一说,也就是说有参数时,参数不论哪种情况,都会被当做catch()的回调参数返回。也就是说参数没有前面1,2,3种的区别。大家可以去试试,我就不过多说明了。

总结

到此这篇关于ES6学习教程之Promise用法详解的文章就介绍到这了,更多相关ES6之Promise用法内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
ext 列表页面关于多行查询的办法
Mar 25 Javascript
JavaScript使用过程中需要注意的地方和一些基本语法
Aug 26 Javascript
禁用页面部分JavaScript不是全部而是部分
Sep 03 Javascript
javascript简单实现滑动菜单效果的方法
Jul 27 Javascript
jquery实现带渐变淡入淡出并向右依次展开的多级菜单效果实例
Aug 22 Javascript
创建基于Bootstrap的下拉菜单的DropDownList的JQuery插件
Jun 02 Javascript
jQuery插件EasyUI获取当前Tab中iframe窗体对象的方法
Aug 05 Javascript
神级程序员JavaScript300行代码搞定汉字转拼音
May 20 Javascript
bootstrap时间控件daterangepicker使用方法及各种小bug修复
Oct 25 Javascript
nvm、nrm、npm 安装和使用详解(小结)
Jan 17 Javascript
Vue组件模板的几种书写形式(3种)
Feb 19 Javascript
js实现鼠标切换图片(无定时器)
Jan 27 Javascript
Node.js文本文件BOM头的去除方法
Nov 22 #Javascript
JavaScript手写数组的常用函数总结
Nov 22 #Javascript
JavaScript实现点击图片换背景
Nov 20 #Javascript
JavaScript实现鼠标经过表格某行时此行变色
Nov 20 #Javascript
JavaScript实现复选框全选和取消全选
Nov 20 #Javascript
JavaScript实现网页下拉菜单效果
Nov 20 #Javascript
JavaScript实现网页tab栏效果制作
Nov 20 #Javascript
You might like
PHP中使用curl伪造IP的简单方法
2015/08/07 PHP
PHP实现搜索地理位置及计算两点地理位置间距离的实例
2016/01/08 PHP
PHP实现活动人选抽奖功能
2017/04/19 PHP
php创建多级目录与级联删除文件的方法示例
2019/09/12 PHP
Laravel 创建可以传递参数 Console服务的例子
2019/10/14 PHP
Gambit vs ForZe BO3 第二场 2.13
2021/03/10 DOTA
IE php关于强制下载文件的代码
2008/08/23 Javascript
给Flash加一个超链接(推荐使用透明层)兼容主流浏览器
2013/06/09 Javascript
跟我学习javascript的var预解析与函数声明提升
2015/11/16 Javascript
在JavaScript中使用JSON数据
2016/02/15 Javascript
BootStrap中的table实现数据填充与分页应用小结
2016/05/26 Javascript
原生JavaScript实现Tooltip浮动提示框特效
2017/03/07 Javascript
vue学习教程之带你一步步详细解析vue-cli
2017/12/26 Javascript
Webstorm2016使用技巧(SVN插件使用)
2018/10/29 Javascript
Bootstrap的aria-label和aria-labelledby属性实例详解
2018/11/02 Javascript
优雅的处理vue项目异常实战记录
2019/06/05 Javascript
初学Python实用技巧两则
2014/08/29 Python
python获取从命令行输入数字的方法
2015/04/29 Python
基于并发服务器几种实现方法(总结)
2017/12/29 Python
Python 限制线程的最大数量的方法(Semaphore)
2019/02/22 Python
Python中psutil的介绍与用法
2019/05/02 Python
解决django后台样式丢失,css资源加载失败的问题
2019/06/11 Python
python flask搭建web应用教程
2019/11/19 Python
后端开发使用pycharm的技巧(推荐)
2020/03/27 Python
解决python3.6用cx_Oracle库连接Oracle的问题
2020/12/07 Python
PyChon中关于Jekins的详细安装(推荐)
2020/12/28 Python
世界上最大的艺术和工艺用品商店:MisterArt.com
2018/07/13 全球购物
在Java开发中如何选择使用哪种集合类
2016/08/09 面试题
今冬明春火灾防控工作方案
2014/05/29 职场文书
2014年后勤管理工作总结
2014/12/01 职场文书
2015选调生工作总结
2015/07/24 职场文书
2015年教师个人业务工作总结
2015/10/23 职场文书
学习委员竞选稿
2015/11/20 职场文书
2016庆祝国庆67周年宣传语
2015/11/25 职场文书
2016年综治宣传月活动宣传标语口号
2016/03/16 职场文书
python 实现的截屏工具
2021/05/08 Python