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 相关文章推荐
JS限制文本框只能输入数字和字母方法
Feb 28 Javascript
JavaScript中window.open用法实例详解
Apr 15 Javascript
jQuery实现简易的天天爱消除小游戏
Oct 16 Javascript
jQuery头像裁剪工具jcrop用法实例(附演示与demo源码下载)
Jan 22 Javascript
javascript实现一个简单的弹出窗
Feb 22 Javascript
AngularJS入门教程之静态模板详解
Aug 18 Javascript
过期软件破解办法实例详解
Jan 04 Javascript
利用iscroll4实现轮播图效果实例代码
Jan 11 Javascript
Vue.js中的图片引用路径的方式
Jul 28 Javascript
javaScript封装的各种写法
Aug 14 Javascript
jQuery实现合并表格单元格中相同行操作示例
Jan 28 jQuery
JavaScript原生数组函数实例汇总
Oct 14 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下将图片以二进制存入mysql数据库中并显示的实现代码
2010/05/27 PHP
php简单解析mysqli查询结果的方法(2种方法)
2016/06/29 PHP
PHP 使用二进制保存用户状态的实例
2018/01/29 PHP
javascript 闭包
2011/09/15 Javascript
js父窗口关闭时子窗口随之关闭完美解决方案
2014/04/29 Javascript
JavaScript实现点击自动选择TextArea文本的方法
2015/07/02 Javascript
JS判断来路是否是百度等搜索索引进行弹窗或自动跳转的实现代码
2016/10/09 Javascript
js实现时间轴自动排列效果
2017/03/09 Javascript
详解在 Angular 项目中添加 clean-blog 模板
2017/07/04 Javascript
vant(ZanUi)结合async-validator实现表单验证的方法
2018/12/06 Javascript
javascript异步处理与Jquery deferred对象用法总结
2019/06/04 jQuery
JQuery常用简单动画操作方法回顾与总结
2019/12/07 jQuery
在VUE style中使用data中的变量的方法
2020/06/19 Javascript
Vue 请求传公共参数的操作
2020/07/31 Javascript
Vue如何将页面导出成PDF文件
2020/08/17 Javascript
零基础写python爬虫之urllib2中的两个重要概念:Openers和Handlers
2014/11/05 Python
Python的Django REST框架中的序列化及请求和返回
2016/04/11 Python
Django原生sql也能使用Paginator分页的示例代码
2017/11/15 Python
opencv改变imshow窗口大小,窗口位置的方法
2018/04/02 Python
python3使用matplotlib绘制条形图
2020/03/25 Python
python pygame实现方向键控制小球
2019/05/17 Python
基于Python模拟浏览器发送http请求
2020/11/06 Python
python 实现超级玛丽游戏
2020/11/25 Python
详解Selenium 元素定位和WebDriver常用方法
2020/12/04 Python
Python爬取某平台短视频的方法
2021/02/08 Python
UGG美国官网:购买UGG雪地靴、拖鞋和鞋子
2017/12/31 全球购物
Falconeri美国官网:由羊绒和羊毛制成的针织服装
2018/04/08 全球购物
MSC邮轮官方网站:加勒比海、地中海和世界各地的假期
2018/08/27 全球购物
美术师范毕业生自荐信
2013/11/16 职场文书
聘用意向书
2014/07/29 职场文书
人生遥控器观后感
2015/06/11 职场文书
电工实训心得体会
2016/01/14 职场文书
纯CSS实现hover图片pop-out弹出效果的实例代码
2021/04/16 HTML / CSS
如何理解Vue简单状态管理之store模式
2021/05/15 Vue.js
Python实现为PDF去除水印的示例代码
2022/04/03 Python
mysql查看表结构的三种方法总结
2022/07/07 MySQL