Promise
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise
对象。
所谓 Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
一个 Promise
有以下几种状态:
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
使用
举两个不同写法的例子
1 | // 回调地狱,主要是到最后无法分清闭合的 ) 或者 } 是谁的 |
简写
当我们只想用 Promise 来分布操作的代码,但这些代码不是异步操作,每次都要返回一个新的 Promise,并且还需要 resolve 出去,非常繁琐,这时你可以简写它。
简写一
1 | /* |
简写二
1 | new Promise((resolve, reject) => { |
Promise.all()
如果需要同时多个 Promise 完成了之后才能执行操作,可以使用 Promise.all().then()
方法
这是老写法
1 | // 网络请求1 |
Promise.all
写法
1 | Promise.all([ |
axios.all
在 axios
中,也有类似的方法 axios.all
,来处理并发请求,也就是当多个请求都返回结果了之后才执行回调。
1 | axios.all([ |
async、await
async 是 ES6 新增的关键字, await 是 ES7 新增。 async 添加在函数的开头,会将 async 函数的内容包装成一个 Promise 对象,这个函数的 return 将作为这个 Promise 的 resolve 。await 相当于 Promise.then()
它会把 await 之后的内容包裹进这个 then 中。 如
1 | async function func1() { |
Promise 执行顺序
推荐看完下面关于 Javascript 事件再来看这里。
1 | /* 下面的流程讲解写的很早,可能当时并不清楚,有些错误,但不打算修改,留着看看当时奇怪的想法,hhh */ |
Javascript 事件
javascript 是一门 单线程 语言,一切 javascript 多线程都是单线程模拟来的!。广义下,我们把任务分为两种,同步和异步任务,任务进入主线程才会被执行。
事件的定义
但实际上,我们对任务有更精细的定义:
- macro-task(宏任务):都是整体代码块,
script
标签内的代码,setTimeout
的回调函数内的代码,setInterval
回调函数内的代码(它们都算作一个宏任务,而每执行一个宏任务之后就会暂停宏任务,先去执行微任务)。 - micro-task(微任务):
Promise
的then
,process.nextTick
,注意只有一个微任务队列,只有当微任务队列被清空了之后,才会结束当前这轮的事件循环,接着开始下一个宏任务。
不同类型的任务会进入对应的 Event Queue,比如 setTimeout
和 setInterval
会进入相同的 Event Queue.
事件顺序
事件循环的顺序,决定 js 的代码执行顺序。进入整体代码(宏任务)后,开始第一次循环,宏任务此时进入主线程。接着执行所有的微任务(即微任务进入主线程)。然后再次从宏任务,接着其中一个任务队列执行完毕,再执行所有微任务,如此反复。
举例一
简单举例,参考自掘金
1 | setTimeout(function() { |
流程
- 这段代码开始作为宏任务,进入主线程。
- 先遇到
setTimeout
,那么将其回调函数注册后分发到宏任务 Event Queue。 - 接下来遇到了
Promise
,new Promise
立即执行,then
函数分发到微任务 Event Queue。 - 遇到 console.log(),立即执行。
- 好了,整体代码
script
作为第一个宏任务执行结束,现在看看当前的微任务,我们发现 then 在微任务队列中,执行它 - 至此,第一轮事件循环结束了,我们开始第二轮循环。当然,从宏任务队列开始,我们发现了宏任务中
setTimeout
对应的回调函数,立即执行它 - 结束。
举例二
另一个较为复杂的例子
1 | console.log('1'); |
流程
- 第一轮事件循环
- 整体
script
作为第一个宏任务进入主线程,遇到console.log
,输出1 - 遇到
setTimeout
,其回调函数被分发到宏任务 Event Queue 中,我们将其记为setTimeout1
- 遇到
process.nextTick()
,其回调函数被分发到微任务 Event Queue 中。我们记为process1
- 遇到
Promise
,new Promise
直接执行,输出 7。then
被分发到微任务 Event Queue 中,我们记为then1
- 又遇到
setTimeout
,其回调函数被分发到宏任务 Event Queue 中,我们记为setTimeout2
- 至此第一次宏任务执行完毕,此时已经输出 1 和 7。同时当前宏任务 Event Queue 存在两个任务,微任务 Event Queue 也两个任务。由于第一个宏任务已经结束,此时需要执行微任务了。
- 执行
process1
,输出 6 - 执行
then1
,输出8 - 微任务 Event Queue 全部执行完毕,现在是空的。至此,第一轮事件循环正式结束,输出结果是 1,7,6,8。接着查看宏任务 Event Queue,发现下一个任务是
setTimeout1
- 整体
- 第二轮事件循环
- 执行
setTimeout1
,遇到console.log
,输出 2 - 接下来遇到
process.nextTick()
,将其分发到微任务 Event Queue,记为process2
。 - 遇到
new Promise
,立即执行 console.log,输出 4。并把then
分发到微任务 Event Queue 中,记为then2
- 第二轮事件循环的宏任务结束,新增输出 2 和 4,此时发现有
process2
和then2
两个微任务可以执行。 process2
输出 3then2
输出 5- 此时微任务 Event Queue 已清空,至此第二轮事件循环结束,第二轮输出 2, 4, 3, 5。接着查看宏任务 Event Queue,发现下一个任务是
setTimeout2
- 执行
- 第三轮事件循环
- 执行
setTimeout2
,遇到console.log
,输出 9。 - 遇到
process.nextTick()
,将其分发到微任务 Event Queue 中。记为process3
- 遇到
new Promise
,立即执行console.log
,输出 11。并将then
分发到微任务 Event Queue 中,记为then3
- 第三轮事件循环的宏任务结束,新增输出 9 和 11。此时发现有
process3
和then3
;两个微任务需要执行 process3
输出 10then3
输出 12- 此时微任务 Event Queue 已清空,至此第三轮事件循环结束,新增输出 9, 11, 10, 12。
- 执行
总结:总共三轮事件循环,分别是 script
以及它对应的微任务 , setTimeout1
以及它对应的微任务, setTimeout2
以及它对应的微任务。 最后输出结果是 1, 7, 6, 8, 2, 4, 3, 5, 9, 11, 10, 12。(由于 node 环境下的事件监听依赖 libuv
与前端环境不完全相同,输出结果可能会有误差)。
libuv
库流程大体分为6个阶段:timers
,I/O callbacks
,idle、prepare
,poll
,check
,close callbacks
,和浏览器的 microtask
,macrotask
那一套有区别。
写在最后
1. javascript 的异步
再说一次 javascript 是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。
2. 事件循环 Event Loop
事件循环是 javascript 实现异步的一种方法,也是 js 的执行机制。
3. javascript 的执行和运行
执行和运行有很大区别,javascript 在不同的环境下,比如 node,浏览器,Ringo 等等,执行方式是不同的。而运行大多指 javascript 解析引擎,是统一的。
4. setImmediate
微任务和宏任务还有很多分类,比如 setImmediate
等,执行都是由共同点的,可以自行了解。
5. 再次说明
- javascript 是一门单线程语言
- Event Loop 是 javascript 的执行机制
牢记着两个基本点。
axios
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js (这是其他请求库所不具备的)中。它支持多种请求方式。 下面 []
内的内容是可选部分
axios(config)
axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
现在我们随便写两个请求
1 | axios({ |
马上就发现了,我们这两个 axios 都有相同的配置,我们应当把它抽离出来。axios 提供了全局配置 axios.defaults
1 | axios.defaults.baseURL = 'http://123.207.32.32:8000' // 请求根路径 |
如果我们需要在 header 里加入自定义请求头,可能需要加上 token 之类的。
1 | axios.defaults.headers:{'x-Requested-With':'XMLHttpRequest'} |
直接列出常见配置项吧。。一个个写太麻烦了。
1 | // 请求地址 |
axios 的封装
由于我们可能在多个组件内 import axios from 'axios'
,之后再使用它。这就导致了我们的项目对 axios
这个插件的依赖过高,一旦 axios
停止维护,或者出现重大 bug,被要求使用别的请求插件。那么,整个项目用到 axios
的地方都需要修改了。为了防止这种情况,我们需要对 axios
单独封装一层,再引入这个封装完毕的文件而不是 axios
,之后如果 axios
真的出问题了,我们也只需要修改自己封装的这个 request 文件,使用新的插件并改写 export 的内容就行,不用一个个修改用到 axios
的组件了。
思想
这种思想是解耦的思想,不管我们用什么插件,如果在多个组件中引入了,我们都可以对它进行封装,多封一层,防止对这个插件的依赖过高。
1 | // @/network/request.js |
坏处
这种写法的坏处在于,这样简单的封装之后,只允许使用 axios(config)
这种格式的请求了,如果需要使用比如 axios.get(url[, config])
这种格式的请求,就需要自己再在这个 http.js
文件中加点新的方法定义,然后 export 出一个对象,对象上有上面的 request
和新增的 get
方法。
所以,如果我们简单使用 axios
,一般都是直接挂载到 Vue.prototype
上,如 Vue.prototype.$http = axios
,之后调用就是 this.$http.get
之类的写法。
拦截器
总共两个拦截器,用于每次在发送请求或得到响应后,进行对应的处理。
axios.interceptors.request.use(config => config, err => err)
request(请求) 拦截,第一个参数是发送请求成功的拦截,第二个参数是发送请求失败的拦截axios.interceptors.response.use(response => response, err => err)
response(响应) 拦截,第一个参数是响应成功的拦截,第二个是响应失败的拦截
参数
use 要求传入的东西,可以看到,它要求的是两个函数。题外:这些插件虽然不是用 ts 写的,但他们一般都会建立一个 .d.ts 文件,用来告诉我们它要求传入的参数类型和返回值类型。
1 | export interface AxiosInterceptorManager<V> { |
使用
1 | // @/network/request.js |
完整版本的简单封装
1 | import axios from 'axios' |
拓展
目前拓展分为 token 需求和 restful 风格的 url 需求两种情况。
1 | // 不同 token 的需求 |
登录认证的两种方式
session
- 客户端访问服务器,服务器会在必要的时候,创建一个 session 对象(和前端的 sessionStorage 区分,这里的是服务器的),该 session 对象有一个唯一的 sessionId,该 session 对象存储在服务器的 session 集合中。
- 服务端一旦创建了 session 对象,会通过请求的 response 对象中的 set-cookies 方法,把当前的 sessionId 存入客户端浏览器的 cookies 中。
- 根据 http 协议,客户端之后的每次请求都会携带 cookie
- 服务器可以根据客户端的请求是否携带了 sessionId 来判断是否登录
- 服务端 session 有默认过期时间,但每次访问将会刷新过期时间。这个过期时间可以通过程序修改过期时间,过期后的 session 其客户端可以被识别。
特点:session 登录,大部分功能都是后台实现的,客户端几乎无感知。但是,如果客户端发送一个 ajax 的跨域请求,那么很多第三方库都是默认不携带 cookie 的。比如 jQuery、axios 等,但可以通过它们上面的方法,设置 withCredentials = true 来使其发送时携带 cookie
问题:
- 不适合分布式部署,当访问量很大时,可能有 Ngix 反向代理让多台服务器负载均衡,此时 session 只存在于你访问过的服务器上,但其他的服务器上不存在,如果上次代理的服务器和这次代理的服务器不一样,又需要重新登录。除非在服务器上统一 session 的状态,但这个在后台实现起来很麻烦。
- 移动端如 App、小程序,它们不支持 cookie
token
- 用户登录成功,服务器会根据客户信息,依据特定算法(可以看看 JWT),生成一个对应该客户端的唯一的标识,这个标识我们称之为 token,一般是一个字符串,并返回给客户端。
- 客户端需要自行实现 token 的保存,并且在之后的每次请求都携带token(一般在请求拦截器里添加特定的 token 键名与 token 值即可)
- 服务器会收到客户端的 token,并且根据生成算法验证当前 token 是否有效,因此如果在不知道生成算法的情况下篡改 token 并发送,将会被识别为无效 token,并返回 token 无效的信息。
- token 有过期时间,过期后页面的任何请求都将失效,并且后台应该返回一个 token 失效的信息,前端拿到信息后,需要重新发送请求获取新的 token,当然此时肯定不能让用户退出重新登陆,因此需要保存客户之前的登录信息,用什么方式转码存储就是另外的问题了,为了安全,必然需要与后台协调算法。
- 客户端退出时,token 并不会立即失效,直到 token 自身的过期时间到了。
特点:客户端需要手动实现 token 的保存和发送。token 无法防止丢失,但是能防止篡改,而且适合分布式部署,服务器不需要存储任务内容,仅需根据生成算法验证当前 token 是否有效即可。
这是算力换空间的一种体现。