什么是VUE?
Vue.js(读音 /vjuː/, 类似于view)是一套构建用户界面的渐进式框架。与其他重量级框架不同的是,Vue 采用自底向上增量开发的设计。Vue 的核心库只关注视图层,并且非常容易学习,非常容易与其它库或已有项目整合。另一方面,Vue 完全有能力驱动采用单文件组件和Vue生态系统支持的库开发的复杂单页应用。
Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件 [1] 。
Vue.js 自身不是一个全能框架——它只聚焦于视图层。因此它非常容易学习,非常容易与其它库或已有项目整合。另一方面,在与相关工具和支持库一起使用时,Vue.js 也能完美地驱动复杂的单页应用。
VUE的工作原理?
Vue实现这种数据双向绑定的效果,需要三大模块:
- Observer:能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
- Compile:对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
- Watcher:作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
Observer
Observer的核心是通过Obeject.defineProperty()
来监听数据的变动,这个函数内部可以定义setter
和getter
,每当数据发生变化,就会触发setter
。这时候Observer
就要通知订阅者,订阅者就是Watcher
。
Watcher
Watcher
订阅者作为Observer
和Compile
之间通信的桥梁,主要做的事情是:
- 在自身实例化时往属性订阅器(dep)里面添加自己
- 自身必须有一个
update()
方法 - 待属性变动
dep.notice()
通知时,能调用自身的update()方法,并触发Compile
中绑定的回调
Compile
Compile
主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
VUE目录简析
VUE生命周期
钩子函数 | 触发的行为 | 在此阶段可操作的事情 |
beforeCreadted | vue实例的挂载元素$el和数据对象data都为undefined,还未初始化。 | 加loading事件/过渡效果 |
created | vue实例的数据对象data有了,$el还没有 | 结束loading/过渡效果、请求数据为mounted渲染做准备 |
beforeMount | vue实例的$el和data都初始化了,但还是虚拟的dom节点,具体的data.filter还未替换。 | |
mounted | vue实例挂载完成,data.filter成功渲染(页面全部加载渲染完毕) | 对dom节点做操作,请求数据 |
beforeUpdate | data更新前触发 | |
updated | data更新时触发 | 数据更新时,做一些处理(此处也可以用watch进行观测) |
beforeDestroy | 组件销毁前触发 | 组件销毁前,可以做一些清除操作,比如清除计时器等 |
destroyed | 组件销毁时触发,vue实例解除了事件监听以及和dom的绑定(无响应了),但DOM节点依旧存在 | 组件销毁时进行提示 |
VUE路由
npm下载
npm install vue-router
如果在一个模块化工程中使用它,必须要通过 Vue.use() 明确地安装路由功能:
在你的文件夹下的 src 文件夹下的 main.js 文件内写入以下代码
import Vue from 'vue'
import router from 'vue-router'
Vue.use(router)
注意:需要在原来的组件上添加< router-view >,所有的路由都是在< router-view >里面渲染的。
路由懒加载
当打包构建应用时,Javascript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。
首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):
const Foo = () => Promise.resolve({ /* 组件定义对象 */ })
第二,在 Webpack 2 中,我们可以使用动态 import语法来定义代码分块点 (split point):
import('./Foo.vue') // 返回 Promise
注意:如果您使用的是 Babel,你将需要添加 syntax-dynamic-import
插件,才能使 Babel 可以正确地解析语法。
例如:
const Foo = () => import('./Foo.vue')
const router = new VueRouter({
routes: [
{ path: '/foo', component: Foo }
]
})
把组件按组分块
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
添加webpackChunkName和不加的区别,增加了,打包
export default new Router({
// 这个是隐藏路由的#
mode: 'history',
routes: [
{
path: '/blogList',
name: 'blogList',
component: BlogList
},
]
出来的文件名,即你命名的名字,不加则是一串哈希字符串。
当然,默认是不加webpackChunkName的,你想要打包出来的文件名称是什么,完全取决于你。
实际上,路由的懒加载是很简单的没有那么复杂,只需要使用es6的引入方式,并配置webpack即可。
路由的跳转
1.
this.$router.push({
path: "/describe",
})
如果是上种方式,也可以使用此种方式代替,因为默认path,所以path这个key可以不写
this.$router.push("/describe")
2.这也不算第二种,算是第一种的改变形式,既然可以以path跳转,那肯定可以用name(路由的name)来作为跳转
this.$router.push({
name: "blogList",
})
前提你需要在路由里配置路由的name
export default new Router({
// 这个可以隐藏路由的#
mode: 'history',
routes: [
{
path: '/blogList',
name: 'blogList',
component: BlogList
},
]
3.声明式:直接写在页面的template标签内
<router-link :to="{name:'blogList'}">
至于规则,和之前两种规则一样,可以根据path和name做跳转
路由的传参
方案一:
getDescribe(id) {
// 直接调用$router.push 实现携带参数的跳转
this.$router.push({
path: `/describe/${id}`,
})
注意:这种方式不可以以name作为参数。
方案一,需要对应路由配置如下:
{
path: '/describe/:id',
name: 'Describe',
component: Describe
}
很显然,需要在path中添加/:id来对应 $router.push 中path携带的参数。在子组件中可以使用来获取传递的参数值。
this.$route.params.id
方案二:
注意:这种方式页面刷新,参数会消失,但是类似于post请求,它不是明文。
父组件中:通过路由属性中的name来确定匹配的路由,通过params来传递参数。
this.$router.push({
name: 'Describe',
params: {
id: id
}
})
对应路由配置: 注意这里不能使用:/id来传递参数了,因为父组件中,已经使用params来携带参数了。
{
path: '/describe',
name: 'Describe',
component: Describe
}
子组件中: 这样来获取参数
this.$route.params.id
这种方式有种问题,刷新页面参数就会获取失败.
当然有可以在刷新页面的情况下的传参方式。
方案三:
注意:query方式传参不会随着刷新而失去参数,但是就像get请求,它是明文的,所以,你懂的
父组件:使用path来匹配路由,然后通过query来传递参数
这种情况下 query传递的参数会显示在url后面?id=?
this.$router.push({
path: '/describe',
query: {
id: id
}
})
对应路由配置:
{
path: '/describe',
name: 'Describe',
component: Describe
}
对应子组件: 这样来获取参数
this.$route.query.id
以上三种传参形式,也适用于
<router-link :to="{name:'blogList'}">
具体形式,和上面的一样,直接在后面跟参数即可。
路由钩子
全局钩子
你可以使用 router.beforeEach 注册一个全局的 before 钩子:
beforeEach钩子函数,是在路由跳转之前的函数,你可以在此函数里在页面跳转之前做一些操作,例如登陆验证等。
to:所要去的路由
from:从哪个路由来的,路由信息
注意:next()方法是必须要使用的,它相当于continue,如果不调用,方法则不会继续执行。
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
同样可以注册一个全局的 after 钩子,不过它不像 before 钩子那样,after 钩子没有 next 方法,不能改变导航
router.afterEach(route => { // ...})
某个路由独享的钩子
你可以在路由配置上直接定义 beforeEnter 钩子:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
这些钩子与全局 before 钩子的方法参数是一样的。
VUE传参(并不是路由传参)
1.vuex
安装、使用 vuex
首先我们在 vue.js 2.0 开发环境中安装 vuex :
npm install vuex --save
然后 , 在 main.js
中加入 :
import vuex from 'vuex'
Vue.use(vuex);
var store = new vuex.Store({//store对象
state:{
show:false
}
})
再然后 , 在实例化 Vue对象时加入 store 对象 :
new Vue({
el: '#app',
router,
store,//使用store
template: '<App/>',
components: { App }
})
完成到这一步 , 上述例子中的 $store.state.show
就可以使用了。
modules
前面为了方便 , 我们把 store 对象写在了 main.js 里面 , 但实际上为了便于日后的维护 , 我们分开写更好 , 我们在 src
目录下 , 新建一个 store
文件夹 , 然后在里面新建一个 index.js
:
import Vue from 'vue'
import vuex from 'vuex'
Vue.use(vuex);
export default new vuex.Store({
state:{
show:false
}
})
那么相应的 , 在 main.js 里的代码应该改成 :
//vuex
import store from './store'
new Vue({
el: '#app',
router,
store,//使用store
template: '<App/>',
components: { App }
})
这样就把 store 分离出去了 , 那么还有一个问题是 : 这里 $store.state.show
无论哪个组件都可以使用 , 那组件多了之后 , 状态也多了 , 这么多状态都堆在 store 文件夹下的 index.js
不好维护怎么办 ?
我们可以使用 vuex 的 modules
, 把 store 文件夹下的 index.js
改成 :
import Vue from 'vue'
import vuex from 'vuex'
Vue.use(vuex);
import dialog_store from '../components/dialog_store.js';//引入某个store对象
export default new vuex.Store({
modules: {
dialog: dialog_store
}
})
这里我们引用了一个 dialog_store.js
, 在这个 js 文件里我们就可以单独写 dialog 组件的状态了 :
export default {
state:{
show:false
}
}
做出这样的修改之后 , 我们将之前我们使用的 $store.state.show
统统改为 $store.state.dialog.show
即可。
如果还有其他的组件需要使用 vuex , 就新建一个对应的状态文件 , 然后将他们加入 store 文件夹下的 index.js 文件中的 modules
中。
modules: {
dialog: dialog_store,
other: other,//其他组件
}
mutations
前面我们提到的对话框例子 , 我们对vuex 的依赖仅仅只有一个 $store.state.dialog.show
一个状态 , 但是如果我们要进行一个操作 , 需要依赖很多很多个状态 , 那管理起来又麻烦了 !
mutations
登场 , 问题迎刃而解 :
export default {
state:{//state
show:false
},
mutations:{
switch_dialog(state){//这里的state对应着上面这个state
state.show = state.show?false:true;
//你还可以在这里执行其他的操作改变state
}
}
}
使用 mutations 后 , 原先我们的父组件可以改为 :
<template>
<div id="app">
<a href="javascript:;" @click="$store.commit('switch_dialog')">点击</a>
<t-dialog></t-dialog>
</div>
</template>
<script>
import dialog from './components/dialog.vue'
export default {
components:{
"t-dialog":dialog
}
}
</script>
使用 $store.commit('switch_dialog')
来触发 mutations
中的 switch_dialog
方法。
这里需要注意的是:
mutations
中的方法是不分组件的 , 假如你在 dialog_stroe.js 文件中的定义了switch_dialog
方法 , 在其他文件中的一个switch_dialog
方法 , 那么$store.commit('switch_dialog')
会执行所有的switch_dialog
方法。mutations
里的操作必须是同步的。
你一定好奇 , 如果在 mutations
里执行异步操作会发生什么事情 , 实际上并不会发生什么奇怪的事情 , 只是官方推荐 , 不要在 mutationss
里执行异步操作而已。
actions
多个 state
的操作 , 使用 mutations
会来触发会比较好维护 , 那么需要执行多个 mutations 就需要用 action
了:
export default {
state:{//state
show:false
},
mutations:{
switch_dialog(state){//这里的state对应着上面这个state
state.show = state.show?false:true;
//你还可以在这里执行其他的操作改变state
}
},
actions:{
switch_dialog(context){//这里的context和我们使用的$store拥有相同的对象和方法
context.commit('switch_dialog');
//你还可以在这里触发其他的mutations方法
},
}
}
那么 , 在之前的父组件中 , 我们需要做修改 , 来触发 action 里的 switch_dialog 方法:
<template>
<div id="app">
<a href="javascript:;" @click="$store.dispatch('switch_dialog')">点击</a>
<t-dialog></t-dialog>
</div>
</template>
<script>
import dialog from './components/dialog.vue'
export default {
components:{
"t-dialog":dialog
}
}
</script>
使用 $store.dispatch('switch_dialog')
来触发 action
中的 switch_dialog
方法。
官方推荐 , 将异步操作放在 action 中。
这里也提一下,实际上mutation和action的功能很相似,但是为什么这两个同时存在呢,那是因为mutation只能做同步操作,而action可以做异步操作,而且这样细分,也有助于追踪数据,便于测试。
getters
getters
和 vue 中的 computed
类似 , 都是用来计算 state 然后生成新的数据 ( 状态 ) 的。
还是前面的例子 , 假如我们需要一个与状态 show
刚好相反的状态 , 使用 vue 中的 computed
可以这样算出来 :
computed(){
not_show(){
return !this.$store.state.dialog.show;
}
}
那么 , 如果很多很多个组件中都需要用到这个与 show 刚好相反的状态 , 那么我们需要写很多很多个 not_show
, 使用 getters
就可以解决这种问题 :
export default {
state:{//state
show:false
},
getters:{
not_show(state){//这里的state对应着上面这个state
return !state.show;
}
},
mutations:{
switch_dialog(state){//这里的state对应着上面这个state
state.show = state.show?false:true;
//你还可以在这里执行其他的操作改变state
}
},
actions:{
switch_dialog(context){//这里的context和我们使用的$store拥有相同的对象和方法
context.commit('switch_dialog');
//你还可以在这里触发其他的mutations方法
},
}
}
我们在组件中使用 $store.state.dialog.show
来获得状态 show
, 类似的 , 我们可以使用 $store.getters.not_show
来获得状态 not_show
。
注意 : $store.getters.not_show
的值是不能直接修改的 , 需要对应的 state 发生变化才能修改。
mapState、mapGetters、mapActions
很多时候 , $store.state.dialog.show
、$store.dispatch('switch_dialog')
这种写法又长又臭 , 很不方便 , 我们没使用 vuex 的时候 , 获取一个状态只需要 this.show
, 执行一个方法只需要 this.switch_dialog
就行了 , 使用 vuex 使写法变复杂了 ?
使用 mapState、mapGetters、mapActions
就不会这么复杂了。
以 mapState 为例 :
<template>
<el-dialog :visible.sync="show"></el-dialog>
</template>
<script>
import {mapState} from 'vuex';
export default {
computed:{
//这里的三点叫做 : 扩展运算符
...mapState({
show:state=>state.dialog.show
}),
}
}
</script>
相当于 :
<template>
<el-dialog :visible.sync="show"></el-dialog>
</template>
<script>
import {mapState} from 'vuex';
export default {
computed:{
show(){
return this.$store.state.dialog.show;
}
}
}
</script>
mapGetters、mapActions 和 mapState 类似 , mapGetters
一般也写在 computed
中 , mapActions
一般写在 methods
中。
详细参考:https://segmentfault.com/a/1190000009404727
不过,如果页面较少,或者各个页面之间的交互很少,则没必要使用vuex。
2.组件间的传参
概述
几种通信方式无外乎以下几种:
Prop
(常用)$emit
(组件封装用的较多).sync
语法糖 (较少)$attrs
和$listeners
(组件封装用的较多)provide
和inject
(高阶组件/组件库用的较多)- 其他方式通信
详述
下面逐个介绍,大神请绕行。
1. Prop
这个在我们日常开发当中用到的非常多。简单来说,我们可以通过 Prop 向子组件传递数据。用一个形象的比喻来说,父子组件之间的数据传递相当于自上而下的下水管子,只能从上往下流,不能逆流。这也正是 Vue 的设计理念之单向数据流。而 Prop 正是管道与管道之间的一个衔接口,这样水(数据)才能往下流。说这么多,看代码:
<div id="app">
<child :content="message"></child>
</div>
// Js
let Child = Vue.extend({
template: '<h2>{{ content }}</h2>',
props: {
content: {
type: String,
default: () => { return 'from child' }
}
}
})
new Vue({
el: '#app',
data: {
message: 'from parent'
},
components: {
Child
}
})
let MyButton = Vue.extend({
template: '<button @click="triggerClick">click</button>',
data () {
return {
greeting: 'vue.js!'
}
},
methods: {
triggerClick () {
this.$emit('greet', this.greeting)
}
}
})
new Vue({
el: '#app',
components: {
MyButton
},
methods: {
sayHi (val) {
alert('Hi, ' + val) // 'Hi, vue.js!'
}
}
})
2. $emit
现在prop已经传参到子组件了,但是子组件里并不能修改传过来的数据,只能通过$emit提交一个事件,让父组件修改属性,这也算
vue的一种安全机制吧。
官方说法是触发当前实例上的事件。附加参数都会传给监听器回调。按照我的理解不知道能不能给大家说明白,先简单看下代码吧:
<div id="app">
<my-button @greet="sayHi"></my-button>
</div>
let MyButton = Vue.extend({
template: '<button @click="triggerClick">click</button>',
data () {
return {
greeting: 'vue.js!'
}
},
methods: {
triggerClick () {
this.$emit('greet', this.greeting)
}
}
})
new Vue({
el: '#app',
components: {
MyButton
},
methods: {
sayHi (val) {
alert('Hi, ' + val) // 'Hi, vue.js!'
}
}
})
3. .sync 修饰符
这个家伙在 vue@1.x 的时候曾作为双向绑定功能存在,即子组件可以修改父组件中的值。因为它违反了单向数据流的设计理念,所以在 vue@2.0 的时候被干掉了。但是在 vue@2.3.0+ 以上版本又重新引入了这个 .sync 修饰符。但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 v-on 监听器。说白了就是让我们手动进行更新父组件中的值了,从而使数据改动来源更加的明显。下面引入自官方的一段话:
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。
既然作为一个语法糖,肯定是某种写法的简写形式,哪种写法呢,看代码:
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event">
</text-document>
于是我们可以用 .sync
语法糖简写成如下形式:
<text-document v-bind:title.sync="doc.title"></text-document>
假如我们想实现这样一个效果:改变子组件文本框中的值同时改变父组件中的值。怎么做?列位不妨先想想。先看段代码:
let Login = Vue.extend({
template: `
<div class="input-group">
<label>姓名:</label>
<input v-model="text">
</div>
`,
props: ['name'],
data () {
return {
text: ''
}
},
watch: {
text (newVal) {
this.$emit('update:name', newVal)
}
}
})
new Vue({
el: '#app',
data: {
userName: ''
},
components: {
Login
}
})
代码里有这一句话:
this.$emit('update:name', newVal)
官方语法是:update:myPropName
其中 myPropName
表示要更新的 prop 值。当然如果你不用 .sync 语法糖使用上面的 .$emit 也能达到同样的效果。仅此而已!
4. $attrs
和 $listeners
- 官网对
$attrs
的解释如下:
包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (
class
和style
除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class
和style
除外),并且可以通过v-bind="$attrs"
传入内部组件——在创建高级别的组件时非常有用。
- 官网对
$listeners
的解释如下:
包含了父作用域中的 (不含
.native
修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件——在创建更高层次的组件时非常有用。
我觉得 $attrs
和 $listeners
属性像两个收纳箱,一个负责收纳属性,一个负责收纳事件,都是以对象的形式来保存数据。看下面的代码解释:
<div id="app">
<child
:foo="foo"
:bar="bar"
@one.native="triggerOne"
@two="triggerTwo">
</child
</div>
从 Html 中可以看到,这里有俩属性和俩方法,区别是属性一个是 prop
声明,事件一个是 .native
修饰器。
let Child = Vue.extend({
template: '<h2>{{ foo }}</h2>',
props: ['foo'],
created () {
console.log(this.$attrs, this.$listeners)
// -> {bar: "parent bar"}
// -> {two: fn}
// 这里我们访问父组件中的 `triggerTwo` 方法
this.$listeners.two()
// -> 'two'
}
})
new Vue({
el: '#app',
data: {
foo: 'parent foo',
bar: 'parent bar'
},
components: {
Child
},
methods: {
triggerOne () {
alert('one')
},
triggerTwo () {
alert('two')
}
}
})
我们可以通过 $attrs
和 $listeners
进行数据传递,在需要的地方进行调用和处理,还是很方便的。当然,我们还可以通过 v-on="$listeners"
一级级的往下传递,子子孙孙无穷尽也!
一个插曲!
当我们在组件上赋予了一个非Prop 声明时,编译之后的代码会把这些个属性都当成原始属性对待,添加到 html 原生标签上,看上面的代码编译之后的样子:
<h2 bar="parent bar">parent foo</h2>
这样会很难看,同时也爆了某些东西。如何去掉?这正是 inheritAttrs 属性的用武之地!给组件加上这个属性就行了,一般是配合 $attrs
使用。看代码:
// 源码
let Child = Vue.extend({
...
inheritAttrs: false, // 默认是 true
...
})
再次编译:
<h2>parent foo</h2>
5. provide
/ inject
他俩是对CP, 感觉挺神秘的。来看下官方对 provide / inject 的描述:
provide
和inject
主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。并且这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
看完描述有点懵懵懂懂!一句话总结就是:小时候你老爸什么东西都先帮你存着等你长大该娶媳妇儿了你要房子给你买要车给你买只要他有的尽量都会满足你。下面是这句话的代码解释:
<div id="app">
<son></son>
</div>
let Son = Vue.extend({
template: '<h2>son</h2>',
inject: {
house: {
default: '没房'
},
car: {
default: '没车'
},
money: {
// 长大工作了虽然有点钱
// 仅供生活费,需要向父母要
default: '¥4500'
}
},
created () {
console.log(this.house, this.car, this.money)
// -> '房子', '车子', '¥10000'
}
})
new Vue({
el: '#app',
provide: {
house: '房子',
car: '车子',
money: '¥10000'
},
components: {
Son
}
})
参考:https://www.cnblogs.com/lhb25/p/10-way-of-vue-data-interact.html
VUE的监听事件
1.watch
简单实例:
watch: {
// 第一个参数newName,代表改变后的值,第二个参数oldName,代表改变前的旧值
firstName(newName, oldName) {
this.fullName = newName + ' ' + this.lastName;
}
}
注意,不要使用es6的箭头函数,因为es6的箭头函数,this的指向是动态的,而不一定指向vue实例,导致获取不到vue实例上的数据。
或者使用下面这种方式:
firstName代表所要监听的字段,requestNotify代表所监听字段数据改变,所要执行的方法,参数同上。
watch: {
"firstName": "requestNotify"
}
methods: {
requestNotify(newValue, oldValue) {
}
}
handler方法和immediate属性
这里 watch 的一个特点是,最初绑定的时候是不会执行的,要等到 firstName
改变时才执行监听计算。那我们想要一开始就让他最初绑定的时候就执行改怎么办呢?我们需要修改一下我们的 watch 写法,修改过后的 watch 代码如下:
watch: {
firstName: {
handler(newName, oldName) {
this.fullName = newName + ' ' + this.lastName;
},
// 代表在wacth里声明了firstName这个方法之后立即先去执行handler方法
immediate: true
}
}
注意到handler
了吗,我们给 firstName 绑定了一个handler
方法,之前我们写的 watch 方法其实默认写的就是这个handler
,Vue.js会去处理这个逻辑,最终编译出来其实就是这个handler
。
而immediate:true
代表如果在 wacth 里声明了 firstName 之后,就会立即先去执行里面的handler方法,如果为 false
就跟我们以前的效果一样,不会在绑定的时候就执行。
到这里是不是觉得watch已经结束了,而且挺简单的,但是,当你监听一个对象或者数组,你发现,明明你已经改变对象里面的某个数据了,但是并没有执行事件,why?接着看下去,watch还没讲完,,莫急。
watch默认是不会深度监听的,简单的说也就是它默认只能监听单独的某个字段,而无法监听对象这类的数据改变;
换言之,就是我可以监听对象中的某个数据的改变,例如:
params = {
name: "1",
"age": "24",
}
watch: {
"params.name": function (new, old) {
}
}
这种是可以的,but,下方这种形式是不行的:
params = {
name: "1",
"age": "24",
}
watch: {
"params": function (new, old) {
}
}
解决办法,就需要用到watch的deep属性了。
deep属性
watch 里面还有一个属性 deep
,默认值是 false
,代表是否深度监听。
使用方法:
watch: {
obj: {
handler(newName, oldName) {
console.log('obj.a changed');
},
deep: true,
}
2.computed
computed相当于属性的一个实时计算,如果实时计算里关联了对象,那么当对象的某个值改变的时候,同事会触发实时计算。
computed: {
age: function() {
return this.childrens.age +10;
}
},
计算属性computed的特点
- 计算属性会依赖于他使用的data中的属性,只要是依赖的属性值有改变,则自动重新调用一下计算属性;
- 如果他所依赖的这些属性值没有发生改变,那么计算属性的值是从缓存中来的,而不是重新编译,那么性能要高一些,所以vue中尽可能使用computed替代watch。
3.watch和computed的区别
实际上watch和computed本质上存在着区别,但是具体使用上,区别性还是比较小的。
1.watch可以执行异步操作,computed只能执行同步。(这个算是主要区别吧,就像action和mutation)
2.算是建议,vue中尽可能使用computed替代watch。
axios
基于promise用于浏览器和node.js的http客户端
特点:
支持浏览器和node.js
支持promise
能拦截请求和响应
能转换请求和响应数据
能取消请求
自动转换JSON数据
浏览器端支持防止CSRF(跨站请求伪造)
默认配置
全局修改axios默认配置
global.js文件信息
/**
* 全局常量配置
*/
let BASE_URL = "http://localhost:8080";
if (process.env.NODE_ENV === "production") {
BASE_URL = "生成环境url";
}
export default {
BASE_URL,
};
import { BASE_URL } from "@/tool/global";
axios.defaults.baseURL = BASE_URL; // url的配置路径
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
实例默认配置
// 创建实例时修改配置
var instance = axios.create({
baseURL: 'https://api.example.com'
});
// 实例创建之后修改配置
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;
配置优先级
配置项通过一定的规则合并,request config > instance.defaults > 系统默认,优先级高的覆盖优先级低的。
请求拦截器
axios请求取消后,如何在此唤醒请求 参考:https://segmentfault.com/q/1010000016399720
axios.interceptors.request.use(config => {
const request = JSON.stringify(config.url) + JSON.stringify(config.data);
// `cancelToken` 指定用于取消请求的 cancel token
config.cancelToken = new CancelToken((cancel) => {
sources[request] = cancel;
});
// 1.判断请求是否已存在请求列表,避免重复请求,将当前请求添加进请求列表数组;
requestList.includes(request) ? sources[request]("取消重复请求") :
requestList.push(request);
// 如果headers中需要携带参数,可在此处配置
// loading
return config;
}, error => {
return Promise.reject(error);
});
响应拦截器
axios.interceptors.response.use(response => {
// 将当前请求从请求列表中删除
const request = JSON.stringify(response.config.url) + JSON.stringify(response.config.data);
requestList.splice(requestList.findIndex(item => item === request), 1);
return response;
}, error => {
return Promise.resolve(error.response);
});
校验状态码
const checkStatus = function(response) {
// loading
// 如果http状态码正常,则直接返回数据
if (response && (response.status === 200 || response.status === 304)) {
return response;
// 如果不需要除了data之外的数据,可以直接 return response.data
}
// 如果 http 状态码是 401 ,则跳转到登录页
if (response && response.status === 401) {
// router.push({path: "/login"});
// isTokenOverdue = false;
sessionTimeout();
}
// 异常状态下,把错误信息返回去,status: -404并无特殊意义,也可定义为其他状态码
return {
status: -404,
msg: "网络异常",
};
};
校验code
const checkCode = function(res) {
// 如果code异常(这里已经包括网络错误,服务器错误,后端抛出的错误)
if (res.status === -404) {
Vue.prototype.$alert("通讯失败");
// alert(res.msg)
}
// 校验前后端约定的状态码
if (res.data && /^E4[0-9]*/.test(res.data.code)) {
return Promise.reject(res.data);
}
return res.data;
};
封装axios请求
// url: 请求的url,data:请求的参数,config:请求的头部信息, method: 请求的方式
const request = (url, data, config, method) => {
let params = {};
params.params = data;
// get,delete和post,put的参数形式不一样,在此做一层封装
if (method === "post" || method === "put") {
params = data;
}
return axios[method](url, params, config).then(
(response) => {
return checkStatus(response);
}
).then(
(res) => {
return checkCode(res);
}
);
};
封装axios方法
const post = (url, data) => {
return request(url, data, headers, "post");
};
const get = (url, params) => {
return request(url, params, headers, "get");
};
// delete 不可以使用,故命名为deletes,不合适也可以更改
const deletes = (url, params) => {
return request(url, params, headers, "delete");
};
const put = (url, data) => {
return request(url, data, headers, "put");
};
export default {post, get, deletes, put, sources};
使用
这里使用了async-await的方式,不理解的可以看一下这篇文章https://segmentfault.com/a/1190000011526612
import api from "@/common/api";
async loadListData() {
this.loading = true;
const params = this.pageData;
params.page = params.page;
params.processStatus = this.processStatus;
try {
const res = await api.get("你的url", params);
this.listData.length = 0;
this.listData = res.data.content;
this.totalElements = res.data.totalElements;
this.loading = false;
} catch (e) {
console.log(e);
}
},
完整的代码
import Vue from "vue";
import axios from "axios";
import { BASE_URL } from "@/tool/global";
import router from "../router";
// 请求列表
const requestList = [];
// 取消列表
const CancelToken = axios.CancelToken;
const sources = {};
// 基础的url路径
axios.defaults.baseURL = BASE_URL;
// 默认headers
const headers = {"Content-Type": "application/json; charset=UTF-8"};
function sessionTimeout() {
Vue.prototype.$notify.error({
title: "Error",
message: "会话过期了需要重新登陆",
showClose: true,
onClose: function() {
router.push("/login");
// isTokenOverdue = true;
},
});
}
axios.interceptors.request.use(config => {
const request = JSON.stringify(config.url) + JSON.stringify(config.data);
// `cancelToken` 指定用于取消请求的 cancel token
config.cancelToken = new CancelToken((cancel) => {
sources[request] = cancel;
});
// 1.判断请求是否已存在请求列表,避免重复请求,将当前请求添加进请求列表数组;
requestList.includes(request) ? sources[request]("取消重复请求") : requestList.push(request);
// 如果headers中需要携带参数,可在此处配置
// loading
return config;
}, error => {
return Promise.reject(error);
});
axios.interceptors.response.use(response => {
// 将当前请求从请求列表中删除
const request = JSON.stringify(response.config.url) + JSON.stringify(response.config.data);
requestList.splice(requestList.findIndex(item => item === request), 1);
return response;
}, error => {
return Promise.resolve(error.response);
});
const checkStatus = function(response) {
// loading
// 如果http状态码正常,则直接返回数据
if (response && (response.status === 200 || response.status === 304)) {
return response;
// 如果不需要除了data之外的数据,可以直接 return response.data
}
// 如果 http 状态码是 401 ,则跳转到登录页
if (response && response.status === 401) {
// router.push({path: "/login"});
// isTokenOverdue = false;
sessionTimeout();
}
// 异常状态下,把错误信息返回去,status: -404并无特殊意义,也可定义为其他状态码
return {
status: -404,
msg: "网络异常",
};
};
const checkCode = function(res) {
// 如果code异常(这里已经包括网络错误,服务器错误,后端抛出的错误)
if (res.status === -404) {
Vue.prototype.$alert("通讯失败");
// alert(res.msg)
}
// 校验前后端约定的状态码
if (res.data && /^E4[0-9]*/.test(res.data.code)) {
return Promise.reject(res.data);
}
return res.data;
};
// url: 请求的url,data:请求的参数,config:请求的头部信息, method: 请求的方式
const request = (url, data, config, method) => {
let params = {};
params.params = data;
// get,delete和post,put的参数形式不一样,在此做一层封装
if (method === "post" || method === "put") {
params = data;
}
return axios[method](url, params, config).then(
(response) => {
return checkStatus(response);
}
).then(
(res) => {
return checkCode(res);
}
);
};
const post = (url, data) => {
return request(url, data, headers, "post");
};
const get = (url, params) => {
return request(url, params, headers, "get");
};
// delete 不可以使用,故命名为deletes,不合适也可以更改
const deletes = (url, params) => {
return request(url, params, headers, "delete");
};
const put = (url, data) => {
return request(url, data, headers, "put");
};
export default {post, get, deletes, put, sources};
// example
// 第一种方式
// 需要注意的是,接口返回的数据不再是response.data,而是response,所以对于返回数据的处理上需要注意一下。
// 需要使用async 和 await或者promise
// import api from "@/common/api";
// async loadListData() {
// this.loading = true;
// const params = this.pageData;
// params.page = params.page;
// params.processStatus = this.processStatus;
// try {
// const res = await api.get(“url”, params);
// if (res.code === "S200") {
// this.listData.length = 0;
// this.listData = res.data.content;
// this.totalElements = res.data.totalElements;
// this.loading = false;
// }
// } catch (err) {
// console.log(err);
// }
// },
// 第二种方式
// import api from "@/common/api";
// api.put(“url”, params).then(res => {
// this.$message({
// message: "更新成功",
// type: "success",
// });
// }).catch((err) => {
// this.$message({
// message: err.message || "更新错误",
// type: "error",
// });
// });
此次封装还有待优化,可以根据自己的需求,自行修改。
参考:https://segmentfault.com/a/1190000016474460
混入 (mixins)
混入 (mixins) 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
简单使用:
import * as restApi from "@/service/fetchData"
import { mapGetters } from 'vuex'
export const baseMixin = {
data() {
return {
// 请求返回的原始数据
storeAttributeData: []
}
},
computed: {
...mapGetters(['ageText2Value'])
},
methods: {
/**
* 获取原始数据
* @param
* {
* "time_ranges": [[1514739661,1515344461],[1514764800,1514800800]],
* "attributes": ["gender", "age", "important_level", "stay_time_avg", "old", "amount", ...]
* }
* @returns {Promise<void>}
*/
async getStoreAttributeDataAction(time_ranges, attributes) {
let response = await restApi.getStoreAttributeData(JSON.stringify(time_ranges), JSON.stringify(attributes))
if (response && response.code === 200) {
this.storeAttributeData = response.data
}
}
}
}
和你直接在组件里实现的方式基本是一样的,只是没有渲染dom而已。
调用方式也很简单:
<template>
</template>
<script>
import { baseMixin } from '@/components/mixins/baseMixin'
export default {
mixins: [baseMixin],
data() {
return {
}
},
mounted() {
},
methods: {
}
}
</script>
如果多个minxins的话
mixins: [baseMixin, ...]// 逗点分开,即可
当你引入mixins后,mixins中的钩子函数和方法会合并到当前组件中,当然mixins也是有原则的,如果当前组件已经存在的方法,会自动覆盖掉mixins中的此方法,也就是组件为大,如果发生冲突了,以组件为本。
还有一点需要注意,比如在mixins中的created()中定义了一个方法,在组件的created()也定义了一个方法,mixins中的方法先执行,你可以理解为,有事了,小弟先上,老大随后跟进。
当然,小弟怎么了,mixins虽然是小弟,但是人家也是有父母的,简单的说就是mixins也是可以继承的。
可以通过关键词extends做继承:
import { baseMixin } from '@/components/mixins/baseMixin'
export const businessMixin = {
extends: baseMixin,
data() {
return {
}
},
mounted() {
},
methods: {
}
}
如上,baseMixin中的方法和数据都被businessMixin继承了。
与vuex的区别
经过上面的例子之后,他们之间的区别应该很明显了哈~
-
vuex:用来做状态管理的,里面定义的变量在每个组件中均可以使用和修改,在任一组件中修改此变量的值之后,其他组件中此变量的值也会随之修改。
-
Mixins:可以定义共用的变量,在每个组件中使用,引入组件中之后,各个变量是相互独立的,值的修改在组件中不会相互影响。
与公共组件的区别
同样明显的区别来再列一遍哈~
-
组件:在父组件中引入组件,相当于在父组件中给出一片独立的空间供子组件使用,然后根据props来传值,但本质上两者是相对独立的。
-
Mixins:则是在引入组件之后与组件中的对象和方法进行合并,相当于扩展了父组件的对象与方法,可以理解为形成了一个新的组件。
对于mixins中的异步请求,结果获取问题
我看别人的解决方案:不要返回结果而是直接返回异步函数
而我的解决办法是,在mixins中获取到返回的值,因为组件中也是可以直接访问mixins中定义的data的,所以直接取值即可。
Vue指令大全
1. v-text
v-text主要用来更新textContent,可以等同于JS的text属性。
<span v-text="msg"></span>
这两者等价:
<span>{{msg}}</span>
2. v-html
双大括号的方式会将数据解释为纯文本,而非HTML。为了输出真正的HTML,可以用v-html指令。它等同于JS的innerHtml属性。
<div v-html="rawHtml"></div>
这个div的内容将会替换成属性值rawHtml,直接作为HTML进行渲染。
3. v-pre
v-pre主要用来跳过这个元素和它的子元素编译过程。可以用来显示原始的Mustache标签。跳过大量没有指令的节点加快编译。
<div id="app">
<span v-pre>{{message}}</span> //这条语句不进行编译
<span>{{message}}</span>
</div>
最终仅显示第二个span的内容
4. v-cloak
这个指令是用来保持在元素上直到关联实例结束时进行编译。
<div id="app" v-cloak>
<div>
{{message}}
</div>
</div>
<script type="text/javascript">
new Vue({
el:'#app',
data:{
message:'hello world'
}
})
</script>
在页面加载时会闪烁,先显示:
<div>
{{message}}
</div>
然后才会编译为:
<div>
hello world!
</div>
5. v-once
v-once关联的实例,只会渲染一次。之后的重新渲染,实例极其所有的子节点将被视为静态内容跳过,这可以用于优化更新性能。
<span v-once>This will never change:{{msg}}</span> //单个元素
<div v-once>//有子元素
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<my-component v-once:comment="msg"></my-component> //组件
<ul>
<li v-for="i in list">{{i}}</li>
</ul>
上面的例子中,msg,list即使产生改变,也不会重新渲染。
6. v-if
v-if可以实现条件渲染,Vue会根据表达式的值的真假条件来渲染元素。
<a v-if="ok">yes</a>
如果属性值ok为true,则显示。否则,不会渲染这个元素。
7. v-else
v-else是搭配v-if使用的,它必须紧跟在v-if或者v-else-if后面,否则不起作用。
<a v-if="ok">yes</a>
<a v-else>No</a>
8. v-else-if
v-else-if充当v-if的else-if块,可以链式的使用多次。可以更加方便的实现switch语句。
<div v-if="type==='A'">
A
</div>
<div v-if="type==='B'">
B
</div>
<div v-if="type==='C'">
C
</div>
<div v-else>
Not A,B,C
</div>
9. v-show
<h1 v-show="ok">hello world</h1>
也是用于根据条件展示元素。和v-if不同的是,如果v-if的值是false,则这个元素被销毁,不在dom中。但是v-show的元素会始终被渲染并保存在dom中,它只是简单的切换css的dispaly属性。
注意:v-if有更高的切换开销
v-show有更高的初始渲染开销。
因此,如果要非常频繁的切换,则使用v-show较好;如果在运行时条件不太可能改变,则v-if较好
10. v-for
用v-for指令根据遍历数组来进行渲染
有下面两种遍历形式
<div v-for="(item,index) in items"></div> //使用in,index是一个可选参数,表示当前项的索引
<div v-for="item of items"></div> //使用of
下面是一个例子,并且在v-for中,拥有对父作用域属性的完全访问权限。
<ul id="app">
<li v-for="item in items">
{{parent}}-{{item.text}}
</li>
</ul>
<script type="text/javascript">
var example = new Vue({
el:'#app',
data:{
parent:'父作用域'
items:[
{text:'文本1'},
{text:'文本2'}
]
}
})
</script>
会被渲染为:
<ul id="app">
<li>父作用域-文本1</li>
<li>父作用域-文本2</li>
</ul>
注意:当v-for和v-if同处于一个节点时,v-for的优先级比v-if更高。这意味着v-if将运行在每个v-for循环中
11. v-bind
v-bind用来动态的绑定一个或者多个特性。没有参数时,可以绑定到一个包含键值对的对象。常用于动态绑定class和style。以及href等。
简写为一个冒号【 :】
<1>对象语法:
//进行类切换的例子
<div id="app">
<!--当data里面定义的isActive等于true时,is-active这个类才会被添加起作用-->
<!--当data里面定义的hasError等于true时,text-danger这个类才会被添加起作用-->
<div :class="{'is-active':isActive, 'text-danger':hasError}"></div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
isActive: true,
hasError: false
}
})
</script>
渲染结果:
<!--因为hasError: false,所以text-danger不被渲染-->
<div class = "is-active"></div>
<2>数组语法
<div id="app">
<!--数组语法:errorClass在data对应的类一定会添加-->
<!--is-active是对象语法,根据activeClass对应的取值决定是否添加-->
<p :class="[{'is-active':activeClass},errorClass]">12345</p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
activeClass: false,
errorClass: 'text-danger'
}
})
</script>
渲染结果:
<!--因为activeClass: false,所以is-active不被渲染-->
<p class = "text-danger"></p>
<3>直接绑定数据对象
<div id="app">
<!--在vue实例的data中定义了classObject对象,这个对象里面是所有类名及其真值-->
<!--当里面的类的值是true时会被渲染-->
<div :class="classObject">12345</div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
classObject:{
'is-active': false,
'text-danger':true
}
}
})
</script>
渲染结果:
<!--因为'is-active': false,所以is-active不被渲染-->
<div class = "text-danger"></div>
12. v-model
这个指令用于在表单上创建双向数据绑定。
v-model会忽略所有表单元素的value、checked、selected特性的初始值。因为它选择Vue实例数据做为具体的值。
<div id="app">
<input v-model="somebody">
<p>hello {{somebody}}</p>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
somebody:'小明'
}
})
</script>
这个例子中直接在浏览器input中输入别的名字,下面的p的内容会直接跟着变。这就是双向数据绑定。
v-model修饰符
<1> .lazy
默认情况下,v-model同步输入框的值和数据。可以通过这个修饰符,转变为在change事件再同步。
<input v-model.lazy="msg">
<2> .number
自动将用户的输入值转化为数值类型
<input v-model.number="msg">
<3> .trim
自动过滤用户输入的首尾空格
<input v-model.trim="msg">
13. v-on
v-on主要用来监听dom事件,以便执行一些代码块。表达式可以是一个方法名。
简写为:【 @ 】
<div id="app">
<button @click="consoleLog"></button>
</div>
<script>
var app = new Vue({
el: '#app',
methods:{
consoleLog:function (event) {
console.log(1)
}
}
})
</script>
事件修饰符
.stop
阻止事件继续传播.prevent
事件不再重载页面.capture
使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理.self
只当在 event.target 是当前元素自身时触发处理函数.once
事件将只会触发一次.passive
告诉浏览器你不想阻止事件的默认行为
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="doThis"></a>
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用
v-on:click.prevent.self
会阻止所有的点击,而v-on:click.self.prevent
只会阻止对元素自身的点击。
参考:https://www.jianshu.com/p/c4a87e1b4ef7
VUE中一些小点:
1.数据更新问题
vue是mvvm框架,数据是双向绑定的,但是vue的动态更新比较死板,有点和watch很像,说到这,如果你对watch已经很了解的话,应该已经知道了,vue也不会深度监听数据改变,虽然这样说并不是完全正确的,它也并不是完全不会深度监听更新数据。
对于对象和数组等类型的更新,如果想要触发重绘,需要使用this.$set()方法。
当然这是仅限于当前组件的重绘,如果父组件数据改变,想触发子组件的重绘,这个时候,使用this.$set()是没有作用的,
之前也说过了,vue的数据监听是比较懒的。
解决办法:
子组件使用watch的深度监听;
还有就是每次数据改变都使用深拷贝,也就是重新new一个数据,这个数据和之前的数据没有任何联系,不要简单的拷贝,而是深拷贝,比如lodash的cloneDeep。
当然以上都是仅限于对象,数组等类型的数据,如果你是一个单独的字段,那就没必要这么操作了,直接传就好了。
2.获取dom节点
如果你引入了jquery了,你可以直接使用jquery获取dom元素的方式,或者原生的获取dom元素方式,都是可以的。
不过,vue也有属于自己的获取dom元素的方式:refs。
使用的方法:
<div id="app">
<input type="text" ref="input1"/>
</div>
this.$refs.input1.value ="22";
3.在实现功能的前提下,一些公用方法,或者公用组件,最好抽离出来,就像java里的抽象方法,在前端是把组件抽象出来,就叫它抽象组件(随意定的)吧。
一些配置信息,或者全局数据,最好统一管理。
数据交互较多,比如一个接口,多个页面都要使用,这个时候就不要吝啬使用vuex了。
4.如果你使用了组件,在组件渲染之前,确保你的数据已经存在,换言之,当你数据已经准备完毕时,才渲染组件,反之,你会发现,你明明数据是存在的,但是组件里却什么都没有,一片空白,特别是图表类的组件。
5.mounted事件,总会先执行子组件中的mounted,最后才执行父组件的mounted,但除了mounted之外的生命周期事件,是先执行父组件,再执行子组件。