0%

Vue学习笔记(五)- Promise和VueX

5 Promise

5.1 简介

ES6中一个非常重要和好用的特性就是Promise,但是初次接触Promise会一脸懵逼,这TM是什么东西?看看官方或者一些文章对它的介绍和用法,也是一头雾水。. Promise到底是做什么的呢?

Promise是异步编程的一种解决方案。那什么时候我们会来处理异步事件呢?

  • 一种很常见的场景应该就是网络请求了。
  • 我们封装一个网络请求的函数,因为不能立即拿到结果,所以不能像简单的3+4=7一样将结果返回。
  • 所以往往我们会传入另外一个函数,在数据请求成功时,将数据通过传入的函数回调出去。
  • 如果只是一个简单的网络请求,那么这种方案不会给我们带来很大的麻烦。但是,当网络请求非常复杂时,就会出现回调地狱。

一个简单使用Promise异步多次请求网络资源的例子:

// 链式编程
// 参数 -> 函数(resolve, reject)
// resolve, reject本身它们又是函数
new Promise((resolve, reject) => {
// 1.第一次网络请求的代码
setTimeout(() => {
resolve()
}, 1000)
}).then(() => {
// 第一次拿到结果的处理代码
console.log('Hello World!')
return new Promise((resolve, reject) => {
// 2. 第二次网络请求的代码
setTimeout(() => {
resolve()
}, 1000)
}).then(() => {
// 第二次拿到结果的处理代码
console.log('Hello Vuejs!')
return new Promise((resolve, reject) => {
// 3.第三次网络请求的代码
setTimeout(() => {
resolve()
}, 1000)
}).then(() => {
// 第三次拿到结果的处理代码
console.log('Hello Python!')
})
})
})

  • Q: 什么情况下会用到Promise?
  • A: 一般情况下,有异步操作时,使用Promise对这个异步操作进行封装

具体参数的一些说明:

// new -> 构造函数(1.保存了一些状态信息 2.执行传入的函数)
// 在执行传入的回调函数时, 会传入两个参数,resolve,reject本身又是函数
new Promise((resolve, reject) => {
setTimeout(() => {
// 此处进行网络请求
// 成功的时候调用resolve
resolve('hello world')
// 失败的时候调用reject
reject('error message')
}, 1000)
}).then((data) => {
// 此处处理网络请求data
console.log(data)
}).catch(err => {
// 此处处理失败请求,将reject中的参数传入
console.log(err)
})

5.2 Promise的三种状态和另外的处理方式

首先,当我们开发中有异步操作时,就可以给异步操作包装一个Promise,异步操作之后会有三种状态

  • pending: 等待状态,比如正在进行网络请求,或者定时器没有到时间
  • fulfill: 满足状态,当我们主动回调了resolve时,就处于该状态,并且会回调.then()
  • reject: 拒绝状态,当我们主动回调了reject时,就处于该状态,并且会回调.catch()

Promise的三种状态

调用的小例子:

new Promise((resolve, reject) => {
// 这里使用setTimeout模拟异步请求
setTimeout(() => {
resolve('Hello Vue.js')
reject('Error Message')
}, 1000)
// 其实.then()可以接收两个参数,一个成功调用的函数和一个失败调用的函数
}).then(data => {
// 请求成功,处理data
console.log(data)
}, err => {
// 请求失败,处理error
console.log(err)
})

Promise的链式调用

假设现在有如下需求:

// 网络请求: aaa -> 自己处理(10行)
// 处理:aaa111 -> 自己处理(10行)
// 处理:aaa111222 -> 自己处理

用Promise的最原始代码写法如下:

new Promise((resolve, reject) => {
setTimeout(() => {
resolve('aaa')
}, 1000)
}).then(res => {
console.log(res, '第一层的10行处理代码')
return new Promise((resolve) => {
resolve(res + '111')
}).then(res => {
console.log(res, '第二层的10行处理代码')
return new Promise(resolve => {
resolve(res + '222')
}).then(res => {
console.log(res, '第三层的10行处理代码')
})
})
})

要知道,Promise是支持直接调用.resolve()方法,故上面的代码可以简写为:

// Promise支持直接调用resolve,故以上可以简写为
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('aaa')
}, 1000)
}).then(res => {
// 1.自己处理10行代码
console.log(res, '第一层的10行处理代码')
// 2.
return Promise.resolve(res + '111').then(
res => {
console.log(res, '第二层的10行处理代码')
return Promise.resolve(res + '222').then(
res => {
console.log(res, '第三层的10行处理代码')
})
})
})

事实上,就连Promise.resolve(都可以省略),直接return数据,es6解释器会在执行时自动加上Promise.resolve,故还可以进一步简写为:

// Promise.resolve事实上也可以省略
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('aaa')
}, 1000)
}).then(res => {
console.log(res, '第一层的10行处理代码')
return res + '111'}).then(
res => {
console.log(res, '第二层的10行处理代码')
return res + '222'}).then(
res => {
console.log(res, '第三层的10行处理代码')
})

6 Vuex

6.1 Vuex简介

Vuex是做什么的

Vuex是一个专为Vue.js应用程序开发的状态管理模式

  • 它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
  • Vuex也集成到Vue的官方调试工具devtools,提供了诸如零配置的time-travel调试,状态快照导入导出等高级调试功能

状态管理到底是什么

  • 状态管理模式,集中式存储管理这些名词看起来就非常高大上,让人捉摸不透
  • 其实,你可以简单的将其看成把需要多个组件共享的全局变量全部存储在一个对象里面
  • 然后,将这个对象放在顶层的Vue实例中,让其他组件可以使用
  • 那么,多个组件是不是就可以共享这个对象中的所有变量属性了呢

为什么需要使用专门的Vuex而不自己写个对象实现呢

  • 当然可以,但是我们要先想想VueJS带给我们最大的便利是什么?没错,就是响应式
  • 如果你自己封装实现一个对象能不能保证它里面所有的属性做到响应式呢?当然也可以,只是自己封装可能稍微麻烦一些
  • 不用怀疑,Vuex就是为了提供这样一个在多个组件间共享状态的插件,用它就可以了

管理什么状态呢

如果你做过大型项目开发,你一定遇到过多个状态在多个界面间共享的问题,如:

  1. 用户的登录状态、用户名称、头像、地理位置信息
  2. 商品的收藏,购物车中的物品等
  3. 这些状态信息,我们都可以放在统一的地方,对它进行保存和管理,而且它们还是响应式的

6.2 页面的状态管理

我们知道,要在单个组件中进行状态管理是一件非常简单的事情。什么意思呢?我们来看下面的图片。

VuexFlow

这图片中的三种东西,怎么理解呢?

  • State:不用多说,就是我们的状态。(你姑且可以当做就是data中的属性)

  • View :视图层,可以针对State的变化,显示不的信息。(这个好理解吧? )

  • Actions :这里的Actions主要是用户的各种操作:点击、输入等等,会导致状态的改变。

单页面状态管理的实现

<template>
<div class="test">
<div>当前计数:{{counter}}</div>
<button @click="counter+=1">+1</button>
<button @click="counter-=1">-1</button>
</div>
</template>

<script>
export default {
name: 'HelloWorld',
data() {
return {
counter: 0
}
}
}
</script>

<style scoped>
</style>
  • 在这个案例中,我们有木有状态需要管理呢?没错,就是个数counter.

  • counter需要某种方式被记录下来,也就是我们的State.

  • counter目前的值需要被显示在界面中,也就是我们的View部分。

界面发生某些操作时(我们这里是用户的点击,也可以是用户的input),需要去更新状态,也就是我们的Actions.·这不就是上面的流程图了吗?

多用户状态管理

Vue已经帮我们做好了单个界面的状态管理,但是如果是多个界面呢?

  • 多个试图都依赖同一个状态(一个状态改了,多个界面需要进行更新)
  • 不同界面的Actions都想修改同一个状态( Home.vue需要修改, Profile.vue也需要修改这个状态)也就是说对于某些状态(状态1/状态2/状态3)来说只属于我们某一个试图,但是也有一些状态(状态a/状态b/状态c)属于多个试图共同想要维护的
  • 状态1/状态2/状态3你放在自己的房间中,你自己管理自己用,没问题
  • 但是状态a/状态b/状态c我们希望交给一个大管家来统一帮助我们管理! ! !没错, Vuex就是为我们提供这个大管家的工具。

全局单例模式(大管家)

我们现在要做的就是将共享的状态抽取出来,交给我们的大管家,统一进行管理。之后,你们每个试图,按照我规定好的规定,进行访问和修改等操作。这就是Vuex背后的基本思想。

Vuex状态管理图例

注意:

  1. 不要在组件中直接修改state(组件->state),需要走组件-> action-> mutations->state修改state,这样devtools可以跟踪到修改,方便调试
  2. 官方是允许直接修改mutations的(即组件->mutations->state)
  3. 但mutations中的修改一般是同步操作,异步操作需要通过actions修改,否则Vue的调试工具devtools没有办法跟踪到,不方便调试

6.3 使用Vuex

以下是使用Vuex的一个小例子,用于改变count值:

@/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

// 1.安装插件
Vue.use(Vuex)

// 2.创建对象
const store = new Vuex.Store({
state: {
counter: 0
},
mutations: {
// 方法
increment (state) {
state.counter++
},
decrement (state) {
state.counter--
}
},
actions: {

},
getters: {

},
modules: {

}
})

// 3.导出store对象
export default store

App.vue

<template>
<div id="app">
<p>{{count}}</p>
<button @click="addition">+</button>
<button @click="subtraction">-</button>
<!--<router-view/>-->
</div>
</template>

<script>
export default {
name: 'App',
data () {
return {
message: '我是App组件'
}
},
computed: {
count () {
return this.$store.state.counter
}
},
methods: {
addition () {
this.$store.commit('increment')
},
subtraction () {
this.$store.commit('decrement')
}
}
}
</script>

<style>
</style>

@/main.js

import Vue from 'vue'
import App from './App'
import router from './router'
import store from '@/store'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})

好的,这就是使用Vuex最简单的方式了。

做一下简单的小节:

  1. 提取出一个公共的store对象,用于保存在多个组件中共享的状态

  2. 将store对象放置在new Vue对象中,这样可以保证在所有的组件中都可以使用到

  3. 在其他组件中使用store对象中保存的状态即可

    通过this.Sstore.state.属性的方式来访问状态

    通过this.$store.commit(‘mutation中方法)来修改状态

注意事项:

  • 我们通过提交mutation的方式,而非直接改变store.state.count

  • 这是因为Vuex可以更明确的追踪状态的变化,所以不要直接改变store.state.count的值。

6.4 Vuex核心概念

6.4.1 State及Getters

State单一状态树

Vuex提出使用单一状态树,什么是单一状态树呢?

  • 英文名称是single Source of Truth ,也可以翻译成单一数据源。

但是,它是什么呢?我们来看一个生活中的例子。

  • OK,我用一个生活中的例子做一个简单的类比。

  • 我们知道,在国内我们有很多的信息需要被记录,比如上学时的个人档案,工作后的社保记录,公积金记录,结婚后的婚姻信息,以及其他相关的户口、医疗、文凭、房产记录等等(还有很多信息)。

  • 这些信息被分散在很多地方进行管理,有一天你需要办某个业务时(比如入户某个城市),你会发现你需要到各个对应的工作地点去打印、盖章各种资料信息,最后到一个地方提交证明你的信息无误。

  • 这种保存信息的方案,不仅仅低效,而且不方便管理,以及日后的维护也是一个庞大的工作(需要大量的各个部门的人力来维护,当然国家目前已经在完善我们的这个系统了)。

这个和我们在应用开发中比较类似:

  • 如果你的状态信息是保存到多个Store对象中的, 那么管理和维护将会变得非常困难
  • 所以Vuex也使用了单一状态树来管理应用层级的全部状态
  • 单一状态树能够让我们最直接的方式找到某个状态片段,而且在之后的维护和调试中,也可以方便的管理和维护

关于state和getters一个例子:

@store/index.js

import Vue from 'vue'
import Vuex from 'vuex'

// 1.安装插件
Vue.use(Vuex)

// 2.创建对象
const store = new Vuex.Store({
state: {
students: [
{ id: 1, name: 'kobe', age: 37 },
{ id: 2, name: 'James', age: 34 },
{ id: 3, name: 'Nash', age: 33 }
]
},
mutations: {},
actions: {},
getters: {
more20stu (state) { // getters内函数第一个参数固定为state,用以从state内获取值
return state.students.filter(s => s.age > 20)
},
more20stuLength (state, getters) { // getters内除了state,第二个可选参数为getters,意味着它可以访问其它getters的数据
return getters.more20stu.length // 访问另一个getters的数据,并返回其长度
}
},
modules: {}
})

// 3.导出store对象
export default store

@App.vue

<template>
<div id="app">
<p>{{$store.getters.more20stu}}</p>
<p>{{$store.getters.more20stuLength}}</p>
</div>
</template>

6.4.2 Mutations

Mutations

Vuex的store状态的更新的唯一方式: 提交Mutation

Mutation主要包括两部分:

  • 字符串事件类型(type)

  • 一个回调函数(handler),该回调函数的第一个参数就是state

  • mutation的定义方式:

    mutations: {
    increment(state) {
    state.count++
    }
    }
  • 通过mutation更新

    <template>
    <div id="app">
    <p>{{count}}</p>
    <button @click="addition">+</button>
    </div>
    </template>
    <script>
    ......
    methods: {
    addition () {
    this.$store.commit('increment') // 透过调用mutation中的方法更新
    }
    }
    </script>

Mutation传递参数

在通过mutation更新数据的时候,有可能我们希望携带一些额外的参数,这些参数被称为是mutation的载荷(payload)

Mutation中的代码:

decrement(state, n) {
state.count -= n
}
。。。。
decreament: function () {
this.$store.commit('decrement', 2)
}

但是如果参数不是一个呢?

  • 比如我们有很多参数需要传递

  • 这个时候,我们通常会以对象的形式传递,也就是payload是一个对象

  • 这个时候可以再从对象中取出相关的信息

    changeCount(state, payload) {
    state.count = payload.count
    }

    changeCount: function () {
    this.$store.commit('changeCount', { Count: 0 })
    }

Mutation提交风格

上面的通过commit进行提交是一种普通的方式,Vue还提供了另外一种风格, 它是一个包含type属性的对象

this.$store.commit({
type: 'changeCount',
count: 100
})
// methods中    
addCount (count) {
// 1.普通的提交对象
// this.$store.commit('incrementCount', count)
// 2. 特殊的提交封装
this.$store.commit({
type: 'incrementCount',
payload // 此处payload为一个对象
})
}

// Mutation中
// Mutation中的处理方式是将整个commit的对象作为payload使用,所以代码没有改变,依然如下
changeCount(state, payload) {
state.count = payload.count
}

Mutation的响应规则

Vuex的store中的state是响应式的,当state中的数据发生改变时,Vue组件会自动更新,这就要求我们必须遵守一些Vuex对应的规则:

  • 提前在store中初始化好所需的属性

  • 当给state中的对象新属性时,使用下面的方式:

    1. 使用Vue.set(obj, ‘newProp’, 123)

    2. 用新对象给旧对象重新赋值

      state.obj = { ...state.obj, newProp: 123 }

通过这种方式添加元素,是不能在页面上做到实时响应式的:

state.info['address'] = "Los Angeles"

如果要做到响应式,需要通过这种方式:

// 添加
Vue.set(state.info, 'address', 'Los Angeles')
// 删除
Vue.delete(state.info, 'age')

通常情况下,Vuex要求我们mutation中的方法必须是同步方法

  • 主要的原因是当我们使用devtools时,devtools可以帮助我们不做mutation的快照
  • 但是如果是异步操作,那么devtools将不能很好的最终这个操作什么时候完成

我们强调,不要在mutation中进行异步操作

  • 但是某些情况,我们确实希望在Vuex中进行一些异步操作,比如网络请求
  • 这事就需要使用action来代替mutation进行异步操作

6.4.3 Actions

来一段代码的例子吧:

@/store/index.js

...
const store = new Vuex.Store({
state: {
info: 'kobe'
},
mutations: {
// 方法
updateInfo (state) {
state.info = 'James'
}
},
actions: {
// context: 上下文
aUpdateInfo (context, payload) {
setTimeout(() => {
console.log(payload)
context.commit('updateInfo')
})
}
}
...

@/app.vue

<template>
<div id="app">
<p>{{$store.state.info}}</p>
<button @click="updateInfo">change</button>
</div>
</template>

<script>

export default {
name: 'App',
data () {
return {
message: '我是App组件'
}
},
methods: {
updateInfo () {
this.$store.dispatch('aUpdateInfo', '我是payload')
}
}
}
</script>

6.4.4 Module

Module是模块的意思,为什么在Vuex中我们要使用模块呢?

  • Vue使用单一状态树,那么也意味着很多状态都会交给Vuex来管理.
  • 当应用变得非常复杂时,store对象就有可能变得相当壁肿.

为了解决这个问题, Vuex允许我们将store分割成模块(Module),而每个模块拥有自己的state, mutation.action, getters等

const store = new Vuex.Store({
state: {
counter: 0,
info: 'kobe'
},
mutations: {
// 方法
increment (state) {
state.counter++
},
decrement (state) {
state.counter--
},
incrementCount (state, count) {
state.counter += count
},
updateInfo (state) {
state.info = 'James'
}
},
actions: {},
getters: {},
modules: { // 将一部分代码才分至moduleA中,在store中引入
a: moduleA
}
})
// moduleA代码
const moduleA = {
state: {
name: 'Allen'
},
mutations: {
updateName (state, payload) {
state.name = payload
}
},
actions: {
aUpdateName (context) { // 使用moduleA中的updateName方法
context.commit('updateName', 'Yao')
}
},
getters: {
fullName (state) { // moduleA中的getters
return state.name + ' Team'
},
fullName2 (state, getters) { // 和store一样,moduleA中也可以getter套娃getters
return getters.fullName + ' has win '
},
fullName3 (state, getters, rootState) { // 可以接收一个额外的rootState参数,用于获取root中的state值
return getters.fullName2 + rootState.counter + ' times'
}
}
}
<template>
<div id="app">
<h2>----------------App内容: modules中的内容--------</h2>
<h2>{{$store.state.a.name}}</h2> # Allen
<p>{{$store.getters.fullName}}</p> # Allen Team
<p>{{$store.getters.fullName2}}</p> # Allen Team has win
<p>{{$store.getters.fullName3}}</p> # Allen Team has win 0 times
<button @click="updateName">修改名字</button>
</div>
</template>

<script>

export default {
name: 'App',
data () {
return {
message: '我是App组件'
}
},
methods: {
updateName () {
this.$store.commit('updateName', 'Curry')
}
}
}
</script>

<style>
</style>

notes:

此处需要注意,当需要访问子模块state中的name时,需要使用$store.state.a.name这种形式,因为实际上,Vuex会把子模块的state作为一个对象放入root的state中,类似{a: {子模块state内容}}这种形式

6.4.5 补:Vuex的项目结构

当我们的Vuex帮助我们管理过多的内容时,好的项目结构可以让我们的代码更清晰:

store
| index.js # 我们组装模块并导出store的地方
| actions.js # 根级别的action
| mutation.js # 根级别的mutation
| modules
| cart.js # 购物车模块
| products.js # 产品模块

7 Axios

几种方式的对比
ajax:
优点:局部更新;原生支持】
缺点:可能破坏浏览器后退功能;嵌套回调】
jqueryAjax:
【在原生的ajax的基础上进行了封装;支持jsonp】
fetch:
优点:解决回调地狱】
缺点:API 偏底层,需要封装;默认不带Cookie,需要手动添加; 浏览器支持情况不是很友好,需要第三方的ployfill】
axios:
【几乎完美】

axios的特点
支持浏览器和node.js
支持promise
拦截请求和响应
能转换请求和响应数据
取消请求
自动转换JSON数据
浏览器端支持防止CSRF(跨站请求伪造)

发送get请求演示

import axios from 'axois'

export default {
name: 'app',
created () {
axios.get('http://httpbin.org').then(
res => {
console.log(res)
}
).catch(err => {
console.log(err)
})
}
}

发送并发请求

有时候我们可能需要同时发送两个请求,使用axios.all, 可以放入多个请求的数组

axios.all([])返回的结果是一个数组,使用axios.spreed可将数组[res1, res2] 展开为res1, res2

import axios from 'axios'

export default {
name: 'app',
created() {
axios.all(
[
axios.get('http://123.207.32.32:8000/category'),
axios.get('http://123.207.32.32:8000/home/data',
{params: {type: 'sell', page: 1}})
]
).then(axios.spread(res1, res2) => {
console.log(res1)
console.log(res2)
})
}
}

全局配置

在上面的示例中,我们的BaseURL是固定的

  • 事实上,在开发中可能很多参数都是固定的
  • 这个时候我们可以进行一些抽取,也可以利用axios全局配置
axios.defaults.baseURL = '123.207.32.32:8000'
axios.defaults.header.post['Content-Type'] = 'application/x-www-form-urlencoded'

常见的配置选项

  • 请求地址:url: ‘/user’

  • 请求类型: method: ‘get’

  • 请求路径:baseURL: ‘http://www.mt.com/api'

  • 请求前的数据处理: transformRequestL [function(data) {}]

  • 请求后的数据处理: transformResponse: [function(data){}]

  • 自定义的请求头: headers: [‘x-Requested-With: ‘XMLHttpRequest’]

  • URL查询对象: params: { id: 12 }

  • 查询对象序列化的函数: paramsSerializer: function(params) {}

  • request body: data: {key: ‘aa’}

  • 超时设置s: timeout: 1000

  • 跨域是否带Token: withCredentials: false

  • 自定义请求处理: adapter: function(resolve, reject, config){}

  • 身份验证信息: auth: { uname: ‘’, pwd: ‘12’ }

  • 响应的数据格式json/blob/document/arraybuffer/text/stream

    responseType: ‘json’

封装axios实例

import axios from 'axios'

// 基础封装
export function request (config, success, failure) {
// 1.创建axios实例
const instance = axios.create({
baseURL: 'http://123.207.32.32:8000',
timeout: 5000
})

// 发送真正的网络请求
instance(config).then(res => {
success(res)
}).catch(err => {
failure(err)
})
}

// 使用Promise方式封装
export function request (config) {
return new Promise((resolve, reject) => {
// 1. 创建axios的实例
const instance = axios.create({
baseURL: 'http://123.207.32.32:8000',
timeout: 5000
})
// 发送真正的网络请求
instance(config).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}

// 进一步简化
export function request (config) {
const instance = axios.create({
baseURL: 'http://123.207.32.32:8000',
timeout: 5000
})

return instance(config)
}

request({
url: '/home/multidata'
}).then(res => {
console.log(res)
})

拦截器

axios提供了拦截器,用于我们在发送每次请求或者得到响应后,进行对应的处理

如何使用呢?

export function request (config) {
// 1. 创建axios实例
const instance = axios.create({
baseURL: 'http://123.207.32.32:8000',
timeout: 5000
})
// 2.拦截器
instance.interceptors.request.use(config => {
// 请求拦截
// 1.比如config中的请求不符合服务器要求
// 2.比如每次发送网络请求时,都希望在界面中显示一个请求图标
// 3.比如登录时需要携带token
console.log(config)
return config // 请求成功, 记得一定要把数据return出去
}, err => {
console.log(err) // 请求失败
})
// 响应拦截
instance.interceptors.response.use(res => {
console.log(res) // 响应成功,记得一定要把数据return出去
return res.data
}, err => {
console.log(err) // 响应失败
})

// 3. 发送真正的网络请求
return instance(config)
}

欢迎关注我的其它发布渠道