Vue.js 的理解学习

Vue 整理

Vue 是什么?

Vue 是一个开源的响应式前端框架,使用了 MVVM 框架,方便开发者关注数据图层。

MVVM 框架是什么?

Model-View-ViewModel 是 MVC 的一个改进版本。那么 MVC 又是什么?

Model-View-Controller,Model 是指数据模型,View 指视图,Controller 是控制器,这个框架使得视图和数据模型分开来,通过控制器来进行操作,在数据发生改变的时候通知控制器,然后控制器去更新视图。

https://www.runoob.com/design-pattern/mvc-pattern.html 菜鸟的这个示例实现了一个 MVC 架构的程序。

MVVM 就是将 MVC 中的 Controller 改进成 ViewModel,把 Model 和 View 关联起来的就是 ViewModel。ViewModel 负责把 Model 的数据同步到 View 显示出来,还负责把 View 的修改同步回 Model。

让MVVM框架去自动更新DOM的状态,从而把开发者从操作DOM的繁琐步骤中解脱出来!

https://www.liaoxuefeng.com/wiki/1022910821149312/1108898947791072

Vue 是如何实现响应式的?

Object.defineProperty()

一开始我觉得通过这个方法,就可以自定义 get() 和 set() 两个方法,这样当这个数据对象发生改变或者获取数据对象的时候,就可以先对数据对象进行处理,那么进行的是什么样的处理?

Virtual DOM

Vue源码系列一:Vue中的data是如何驱动视图渲染的? 中介绍了 Vue 的源码,分析了 Vue 的初始化顺序,主要就是为了生成虚拟 DOM 树。

浏览器不是已经有了 DOM 树了嘛?Vue 这个DOM 树有什么不同的嘛?

其实没什么不同,因为这个树就是为了给浏览器进行渲染的,在这个树的 [官方文档](https://cn.vuejs.org/v2/guide/render-function. html#虚拟-DOM) 中,Vue 解释说使用这个虚拟 DOM 是

Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。

这就联系起来了。通过 Object.defineProperty() 方法可以劫持数据对象,通过虚拟 DOM 可以实现 MVVM 中的ViewModel 功能,更加详细的东西就是在 set() 和 get() 里面了。

Vue 实现了一个订阅-发布模式。在这个模式中,Vue 实现了依赖收集和派发更新。

VUE源码系列二:Vue响应式原理解析(附超详细源码注释和原理解析)

在上面的文章中,作者通过源码解析详细介绍了这个订阅-发布模式,可以将其分成三个模块,

  • Observer(劫持者)
    • 给对象的属性添加 getter 和 setter,用于依赖收集和派发更新
    • 被用在 defineReactive 中,使得对象的每个属性都添加上 getter 和 setter 成为响应式。
  • Dep(依赖收集)
    • 收集订阅者 subs : Array<Watcher> ,用在 defineReactive 的重写 get 中为 dep.depend 方法,为数据提供收集功能。
    • 同时提供了发布更新的作用,在 defineReactive 的重写 set 中为 dep.notify() 方法。
  • Watcher(观察者)
    • 在 Dep 发布更新后使用 dep.notify() 方法,循环执行 Dep 中的 subs[i].update() 使得数据相关的订阅者更新视图。
    • watcher.update() 中,使用了 queuewatcher() 方法,通过一个队列检查是否含有目标观察者,使得视图不会重复刷新。在队列中异步执行 flushSchedulerQueue() 方法,在此方法中排序 queue 确保父子组件的顺序更新,然后执行 run() 函数,使用新旧 value 值执行 Watcher 的回调 cb.call()

响应系统源码版

clipboard. png

这两张图片可以用来参考,来自 从发布-订阅模式到Vue响应系统

总结一下

Vue 通过 Object.defineProperty() 实现对数据对象的劫持,通过 Observer 类定义 getter 和 setter,使得对象的所有属性都具备响应性,在 getter() 中通过依赖收集 Dep 的 dep.depends() 管理订阅者(当数据被 get 即视为被订阅),在 setter() 中通过 dep.notify() 函数向订阅了该数据对象的 Watcher 发送 update() ,Watcher 接收到更新信息后确定组件更新顺序然后映射到 Virtual DOM 中去,Virtual DOM 通过 patch 新旧节点,通过 diff 算法实现对新旧节点的对比,对同层的树节点进行比较而非对树进行逐层搜索遍历。然后生成真实 DOM。

这是 Vue2. x 的响应式原理,在 Vue3. 0 中,官方重写了响应式的实现,改用 Proxy Reflect 代替 Object.defineProperty() 。可以实现对更多数据的操作比如数组元素的劫持和更多的劫持方式。

Vue 的生命周期

Vue 实例生命周期

如果是 keep-alive 的话还有两个:activated 和 deactivated

keep-alive的生命周期

\1. activated: 页面第一次进入的时候,钩子触发的顺序是created->mounted->activated
\2. deactivated: 页面退出的时候会触发deactivated,当再次前进或者后退的时候只触发activated

那么 keep-alive 是什么?

keep-alive 是 Vue 提供的一个抽象组件, <keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition> 相似, 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

这是 Vue 对象的生命周期,那么在 Vue 的父子组件中的生命周期是什么样的?

渲染过程:
父组件挂载完成一定是等子组件都挂载完成后,才算是父组件挂载完,所以父组件的mounted在子组件mouted之后
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

子组件更新过程:

\1. 影响到父组件: 父beforeUpdate -> 子beforeUpdate->子updated -> 父updted
\2. 不影响父组件: 子beforeUpdate -> 子updated

父组件更新过程:

\1. 影响到子组件: 父beforeUpdate -> 子beforeUpdate->子updated -> 父updted
\2. 不影响子组件: 父beforeUpdate -> 父updated

销毁过程:
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed

所以不管是加载更新还是销毁都是父组件会等待子组件完后操作后才执行操作。

v-model 是什么?

v-model 是经常用来双向绑定数据的一个语法,实际是一个 v-bind 和 v-on 结合的一个语法糖,等同于

1
2
v-bind:value="msg"
v-on:input="msg=$event.target.value"

v-bind 动态地绑定一个或多个特性,或一个组件 prop 到表达式。

v-on 绑定事件监听器。事件类型由参数指定。表达式可以是一个方法的名字或一个内联语句,如果没有修饰符也可以省略。

也就是说 v-model 通过绑定数据和绑定数据并监听数据的变化,形成双向绑定。

VUE源码系列七:v-model实现原理

这篇文章则是从源码角度去解释 v-model,并不是简单的转换为 v-bind 和 v-on。只是在最终结果上是一样的。

Vue 组件是什么?

简单说 Vue 组件是一个可以复用的 Vue 实例,通过将网页组件化,可以更加快捷的搭建更多的网页,所以 Vue 组件最主要的目的是复用,类似 Bootstrap 的组件库、Element-UI,他们其实都是组件库,通过自己设计的组件,形成组件库。

https://cn.vuejs.org/v2/guide/components. html 是 Vue 关于组件的文档,以及后续的深入组件内容。

1
Vue.component('my-component-name', { /* ... */ })

通常来说,我们可以使用这样的格式来注册一个组件,这种格式下注册的组件是可以全局使用的,但是很明显当你的页面比较多的时候,全局使用组件会造成一定程度的冗余,所以你可以通过变量赋值,然后通过 Vue 实例的 components 属性来添加当前实例的组件。

1
2
3
4
5
6
7
8
9
var ComponentA = { /* ... */ }
var ComponentB = { /* ... */ }
new Vue({
el: '#app',
components: {
'component-a': ComponentA,
'component-b': ComponentB
}
})

在设计组件的时候,我们需要考虑这个组件需要什么,以及他最后会呈现什么样子。

1
2
3
4
5
Vue.component('blog-post', {
// 在 JavaScript 中是 camelCase 的
props: ['postTitle'],
template: '<h3>{{ postTitle }}</h3>'
})

通过 props 可以为组件添加属性,这个属性是可以由父组件传递给子组件使用的。可以理解为这是一种函数模式,通过传入参数,使得组件呈现不同的样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})

除了 props 以外,我们还可以给组件添加自定义的事件属性,

注意这里在 template 中使用的是 v-bindv-on 而不是 v-model ,虽然他们所实现的目的是一样的,但是由于方便以及参数传输更加清楚,我们在使用这个组件的时候,通常使用 v-model

1
<base-checkbox v-model="lovingVue"></base-checkbox>

这样对应过来, lovingVue 属性绑定在了组件的 checked 属性上,并且在发生 change 事件时也会通过 v-on 方法返回到 lovingVue 属性上,实现子组件参数与父组件属性间的双重绑定。

更多的细节可以参考官方的详细文档,你就会发现组件其实和实例拥有几乎一模一样的属性和方法,包括 computed watch 以及生命周期方法。

但是我们可以看到,在 template 的编写过程中,我们完全得不到语法提示,因为他是一个字符串属性。

所以更好的办法是我们把组件写成一个实例。通过局部引入来进行调用。也就是单文件组件。

在单文件组件中,组件编写更加的方便,同时数据上的传递思路也更加明确。

关于插槽

插槽是 Vue 组件中一个很重要的方法,简单来说,插槽的目的是提供另外一种参数传递的方式,

1
2
3
<navigation-link url="/profile">
Your Profile
</navigation-link>

当你使用了一个 navigation-link 的组件时,正常情况下,组件间的数据都会被忽略,因为对于组件来说,中间的数据没有任何意义,如果不能正确的传递参数,中间的数据很容易打乱组件的结构。

1
2
3
<a v-bind:href="url" class="nav-link">
<slot></slot>
</a>

但是当组件中设定了一个 slot 标签后,父组件在使用子组件时,标签内部的信息都会被转移到 slot 标签中,而不是被直接抛弃。所以最后会渲染成:

1
2
3
<a v-bind:href="url" class="nav-link">
Your Profile
</a>

插槽中不仅可以填写这种纯文本,使用 HTML 代码甚至是使用两个{}格式包裹起来的数据属性也是可以的。

但是在使用 双{} 格式传递数据时我们需要思考一个问题,我们是在使用子组件的时候,在子组件标签中间使用的 双{} ,那么里面的属性我们自然是填入父组件的数据属性。

1
2
3
<navigation-link url="/profile">
{{ user.name }}
</navigation-link>

我们可以用作用域来解释,即子组件的作用域,仅仅是在子组件中,甚至说在父组件中这个子组件的标签属性,也是数据父组件的作用域范围内,所以我们可以通过标签属性来传递父组件的数据到子组件中。只有数据传递过去后,才能算是子组件的数据属性,属于子组件的作用域范围。

更多详细的关于插槽的信息参考文档 https://cn.vuejs.org/v2/guide/components-slots.html

需要了解的包括有插槽的内容、作用域、默认内容、具名插槽、插槽prop、动态插槽名等相关知识。

关于动态组件

1
2
3
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>

在经典 Vue 示例中,我们经常会看到 <router-view> 这个标签,这个标签常常使用在 Vue 项目的入口 App.vue 中,用来对视图进行切换,在实际项目中,当父组件动态调用子组件的时候,通常来说,在切换子组件后子组件会被销毁。但是当我们添加 <keep-alive> 标签后,被切换隐藏掉的子组件并不会被销毁,这样可以在特定场景下, 提升网页反应速度。

在测试的时候发现在 <router-view> 外也是可以使用 <keep-alive> 来实现同样的效果。

组件通信

父 => 子

通过子组件设定好的 props 进行传值通信。

通过 this.children

当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源。

也就是说适用于批量生成子组件,如果只是普通的组件通信最好是不要用。

子 => 父

通过自定义事件进行通信

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// super.vue
<template>
<div id="app">
<app-header v-on:titleChanged="updateTitle" ></app-header>//与子组件titleChanged自定义事件保持一致
// updateTitle($event)接受传递过来的文字
<h2>{{title}}</h2>
</div>
</template>
<script>
export default {
methods:{
updateTitle(e){ //声明这个函数
this.title = e;
}
}
}
</script>

// child.vue
<template>
<h1 @click="changeTitle">{{title}}</h1>//绑定一个点击事件
</template>
<script>
export default {
methods:{
changeTitle() {
this.$emit("titleChanged","子向父组件传值");//自定义事件 传递值“子向父组件传值”
}
}
}
</script>

通过 this.parent 访问到父组件的实例。可以直接访问到 data 和方法

兄弟组件

可以通过新建一个中间组件,在两个组件间引入,相当于这个中间组件是他们两个的子组件,通过 $emit()$on() 进行事件通信。

Vue Router

Vue Router 是 Vue. js 官方的路由管理器。它和 Vue. js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:

  • 嵌套的路由/视图表
  • 模块化的、基于组件的路由配置
  • 路由参数、查询、通配符
  • 基于 Vue. js 过渡系统的视图过渡效果
  • 细粒度的导航控制
  • 带有自动激活的 CSS class 的链接
  • HTML5 历史模式或 hash 模式,在 IE9 中自动降级
  • 自定义的滚动条行为

和大部分的路由管理器一样,Vue Router 通过对 URL 进行正则匹配指导用户转向不同的页面,和一些服务端的路由不同的是,Vue Router 通常用于建立单页面应用,这里的单页面并不是指只有一个页面,而是通过在同一个页面,通过路由的不同显示不同的预先写好的模板页面。

模式

Vue Router 默认使用 hash 模式 URL

1
http://localhost:8080/#/login

通过 # 区分浏览器 URL 和 Vue Router 路由所使用的 URL。在 hash 模式下,可以更加方便的理解整个页面程序的路由,但是我们会发现他不太美观,如果你想使用正常的浏览器形式的 URL,需要修改 Vue Router 的模式。

1
2
mode: 'history'
http://localhost:8080/login

需要注意的时,这是在 Vue 提供的临时服务器中,所以可能感受不到 Vue 的单页面应用和其他的多页面应用的区别,因为 Vue 的临时服务器会通过 Vue 的 Router 解析端口传递的 URL 请求并指向到正确的显示页面。

回到浏览器上,当我们输入了一个 URL 后,如果是普通的多页面应用,会通过 URL 解析并指向目标 HTML 文件,返回文件进行渲染,对单页面应用来说,一个是前端页面的不同路由,一个是服务器接口的路由。如果不使用 hash 模式通过 # 进行区分的话,是不能通过 http://localhost:8080/login 直接访问到 login 页面的

png 404

当前使用 Vue 生成的静态文件作为 Python Django 服务器的模板,通过 / 路由确定 index.html 的位置并显示 Vue 页面,然后开始使用 Vue 的路由。如果直接在 URL 栏上输入 Vue 的路由是不能转到目标页面的。

关于路由方面需要了解的大概就是嵌套路由,正则表达,路由参数,有的部分是和组件有些类似的地方,比如路由参数和组件间的参数传递,通常来说路由参数是用于同级组件间的参数传递,组件间参数是父子组件间的数据传递。

动态路由匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}

const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
name:'user',
{ path: '/user/:id', component: User }
]
})
// 在组件中使用的话
this.$router.push({
name:'user', // 这里只能用 name
params:{
id:id
}
})
// 接收参数
this.$route.params.id // 参数并不会在url上显示

使用 params 可以匹配 /user/ 等路由,id 将作为值传给组件,通过 $route.params.id 进行调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const SearchUser = {
template: '<div>User {{ query }}</div>'
}

const router = new VueRouter({
routes: [
{ path: '/search', component: SearchUser, props: (route) => ({ query: route.query.q }) }
]
})
// 在组件中使用的话
this.$router.push({
path:'/user',
query:{
id:id
}
})
// 接收参数
this.$route.query.id // 参数会在url上显示

URL /search?q=vue 会将 {query: 'vue'} 作为属性传递给 SearchUser 组件。

导航守卫

“导航”表示路由正在发生改变。

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2. 2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2. 5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

所以守卫实际上是设定好的函数,通过路由变化触发。在完整的 beforeEach=>beforeRouteUpdate=>beforeEnter=>beforeRouteEnter=>afterEach 状态路径上对网页进行控制。

相当于是路由的生命周期,类似于组件的生命周期一样。也是一种钩子函数。

通过导航守卫可以对路由进行重定向、身份验证、数据获取等功能。

路由传参

取代与 $route 的耦合

1
2
3
4
5
6
7
8
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User }
]
})

这里使用的是通过 :param 在 url 上添加参数,在组件中通过 $route.params.param 进行调用。但是这样会使组件和对应路由形成高度耦合,限制了路由的灵活性。

通过 props 解耦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },

// 对于包含命名视图的路由,你必须分别为每个命名视图添加 `props` 选项:
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
})

通过 props 进行传参我们可以看到,是在路由配置中添加 props:true ,在组件中使用 props 属性,这样就可以吧路由参数当做普通的组件数据来使用。

如果 props 被设置为 trueroute.params 将会被设置为组件属性。

如果 props 是一个对象,它会被按原样设置为组件属性。当 props静态的时候有用。

1
2
3
4
5
6
7
8
9
const router = new VueRouter({
routes: [
{ path: '/promotion/from-newsletter', component: Promotion, props: { newsletterPopup: false } }
]
})
const User = {
props:['newsletterPopup'], // 值为 false
...
}

你可以创建一个函数返回 props。这样你便可以将参数转换成另一种类型,将静态值与基于路由的值结合等等。

1
2
3
4
5
const router = new VueRouter({
routes: [
{ path: '/search', component: SearchUser, props: (route) => ({ query: route.query.q }) }
]
})

URL /search?q=vue 会将 {query: 'vue'} 作为属性传递给 SearchUser 组件。

类似于一个动态的对象组件,和 /search/:q 的效果是类似的。

请尽可能保持 props 函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义 props,请使用包装组件,这样 Vue 才可以对状态变化做出反应。

路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):

1
2
3
4
5
6
7
8
9
const Foo = () => Promise.resolve({ /* 组件定义对象 */ })
// 下面2行代码,没有指定webpackChunkName,每个组件打包成一个js文件。
/* const Home = () => import('@/components/home')
const Index = () => import('@/components/index')
const About = () => import('@/components/about') */
// 下面2行代码,指定了相同的webpackChunkName,会合并打包成一个js文件。 把组件按组分块
const Home = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/home')
const Index = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/index')
const About = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '@/components/about')
1
2
3
4
5
6
7
8
9
10
const HelloWorld = () => import("@/components/HelloWorld")
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component:HelloWorld
}
]
})

同样的方法在组件中也可以使用,将子组件懒加载。

第二,在 Webpack 2 中,我们可以使用动态 import语法来定义代码分块点 (split point):

1
import('./Foo.vue') // 返回 Promise
1
2
3
4
5
6
7
8
9
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: resolve=>require(["@/components/HelloWorld"],resolve)
}
]
})

Vuex

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

状态管理模式是什么?

简单说就是一个全局的数据状态,多个组件可以共享这个状态。避免多个组件间的复杂的数据传递和状态修改的实现。

需要注意的是 Vuex 的数据都是存在内存中的,如果页面刷新会丢失数据,需要在数据修改后及时保存到 localStorage 中。

store

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
try {
window.localStorage.setItem('count', JSON.stringify(state.count));
// 数据改变的时候把数据拷贝一份保存到localStorage里面
} catch (e) {}
}
}
})

现在,你可以通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更:

1
2
3
store.commit('increment')

console.log(store.state.count) // -> 1

再次强调,我们通过提交 mutation 的方式,而非直接改变 store.state.count ,是因为我们想要更明确地追踪到状态的变化。这个简单的约定能够让你的意图更加明显,这样你在阅读代码的时候能更容易地解读应用内部的状态改变。此外,这样也让我们有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。有了它,我们甚至可以实现如时间穿梭般的调试体验。

Action

之前说当我们要修改 store 中的数据时,只能显式提交 mutation,Action 也不会破坏这个规则。

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.statecontext.getters 来获取 state 和 getters。

Action 通过 store.dispatch 方法触发:

1
store.dispatch('increment')

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:

1
2
3
4
5
6
7
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}

Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

首先,你需要明白 store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise:

1
2
3
4
5
6
7
8
9
10
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}

现在你可以:

1
2
3
store.dispatch('actionA').then(() => {
// ...
})

在另外一个 action 中也可以:

1
2
3
4
5
6
7
8
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}

最后,如果我们利用 async / await,我们可以如下组合 action:

1
2
3
4
5
6
7
8
9
10
11
// 假设 getData() 和 getOtherData() 返回的是 Promise

actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}

一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

也就是说 Action 是为了解决 mutation 不能异步的问题,使用 Promise 封装,方便不同的操作之间异步执行。

Module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}

const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}

const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

钩子函数

在我的理解中,钩子函数意味着在某些事件或时间节点自动触发的函数,例如 Vue 和 React 的生命周期函数,在 React 的 Hook 章节中,React 把钩子函数使用得更加的灵活,虽然目的是为了在函数式组件中使用 state 和生命周期等功能。

也就是说钩子函数提供一种挂载功能,可以为组件挂载一些单独的其他功能。

Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。Hook 不能在 class 组件中使用 —— 这使得你不使用 class 也能使用 React。

在 Vue 中,使用钩子函数挂载生命周期是很常见的用法,而生命周期的思维在很多地方都有体现。

Vue 实例生命周期

这张图是组件的生命周期,每一个行为都可以自定义一些操作。

最近才发现的 Vue 的过渡中也有生命周期。

在进入/离开的过渡中,会有 6 个 class 切换。

  1. v-enter:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
  2. v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
  3. v-enter-to2.1.8 版及以上定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成之后移除。
  4. v-leave:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
  5. v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
  6. v-leave-to2.1.8 版及以上定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除。

我们可以通过以下 attribute 来自定义过渡类名:

  • enter-class
  • enter-active-class
  • enter-to-class (2.1.8+)
  • leave-class
  • leave-active-class
  • leave-to-class (2.1.8+)

虽然过渡是通过 css 来实现的,但是依然提供了这些事件节点的钩子函数,通过 v-on 进行绑定。