双向绑定的原理
好东西不能藏着,这个视频教学我看了不下3遍,思路清晰透明,看不懂的话多看两次!Vue MVVM原理
Vue的生命周期 及 每个阶段做了什么事情
官方的vue生命周期图,来自vue生命周期图
// vue生命周期
beforeCreate
created
beforeMount
mounted
updated
beforeUpdate
updated
beforeDestroy
destroyed
// 使用了keep-alive 后存在这两个生命周期
activated
deactivated
beforeCreate的时候,数据/事件还未初始化,无法访问到数据和真实到dom。
beforeCreated —> created 这一段时间内,进行初始化事件和数据。
created的时候,数据和事件已经初始化了,在这个阶段可以对数据进行更改,在这里更改数据不会触发updated函数。
created —> beforeMounted 这一段时间内,会先判断vue内有没有el这个元素,如果有则继续往下执行,如果没有则停止编译,直到在该vue实例上调用vm.$mount(el)。
然后会进行判断,在vue对象中有没有定义template,如果有的话则使用定义的template作为模版,如果没有则定义使用vue的el属性绑定的dom区域作为模版。
beforeMount —> mounted 这一段时间内,开始编译模板 (下面一个板块Vue的编译过程),挂载$el。替换真实节点。
mounted —> 挂载完成,模板中的html渲染到了页面中,mounted只会执行一次。这个时候页面的事件和数据都已经挂载了,真实dom也渲染好了。在这一步可以执行DOM操作。
在vue的对象中,当data的值发生改变时就会先调用beforeUpdate函数。
beforeUpdate --> updated vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染。渲染完以后就会执行updated钩子函数。
beforeDestroy钩子函数在实例销毁之前调用。在这一步,实例仍然完全可用。
destroyed钩子函数在Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
Vue的编译过程
学习视频
参考: Vue.js的template编译过程
解析器(parse) - 将 模板字符串 转换成 element ASTs
优化器(optimize) - 对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化
代码生成器(generate) - 使用 element ASTs 生成 render 函数代码字符串
AST(abstract syntax tree 抽象语法树), 是源代码的抽象语法结构的树状表现形式。
解析器(parse)
parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的
优化器(optimize)
通过 optimize 把整个 AST 树中的每一个 AST 元素节点标记了 static 和 staticRoot, optimize 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成 DOM 永远不需要改变
代码生成器(generate)
把优化后的 AST 树转换成render函数,render函数转换成虚拟dom,然后在打入真实节点
Vue的组件通信方式
参考: Vue组件通信
一、父子间通信 props / $emit
父组件
<template>
<div>
<sonComponent :msg="msg" @sonEmit="getSonEmit"></sonComponent>
</div>
</template>
<script>
import sonComponent from './sonComponent.vue';
export default {
name: 'FatherComponent',
components: { sonComponent },
data() {
return {
msg: '我来自父亲组件',
sonValue: 'hi'
}
},
methods: {
getSonEmit(emitValue) {
this.sonValue = emitValue;
}
}
}
</script>
子组件
<template>
<div>
<span>{{msg}}</span>
<button @click="onClike()">click me!</button>
</div>
</template>
<script>
export default {
name: 'sonComponent',
props: ['msg']
data() {
return {
sonValue: '我来自子组件'
}
},
methods: {
onClike() {
this.$emit('sonEmit', this.sonValue);
}
}
}
</script>
父亲组件通过 :值 的方式将值传递给子组件,子组件在props中定义并接住父组件传递的值。
子组件通过调用$emit传递一个名称和值,在父亲组件中用@名称的形式定义一个方法,并在方法中接住子组件传递过来的值
二、通过 $parent / $children
父组件
<template>
<div>
<sonComponent :msg="msg" />
<button @click="updateSonValue">点击我修改子组件的sonValue值</button>
</div>
</template>
<script>
import sonComponent from './sonComponent.vue';
export default {
name: 'FatherComponent',
components: { sonComponent },
data() {
return {
msg: '我来自父亲组件'
}
},
methods: {
updateSonValue() {
this.$children[0].sonValue = '子组件的值被父组件修改拉!'
}
}
}
</script>
子组件
<template>
<div>
<span>{{msg}}</span>
<button @click="onClick()">点击修改父组件的值!</button>
</div>
</template>
<script>
export default {
name: 'sonComponent',
props: ['msg']
data() {
return {
sonValue: '我来自子组件'
}
},
methods: {
onClick() {
this.$parent.msg = '父组件的值被子组件修改了!';
}
}
}
</script>
三、provide / inject
可以用于父子组件,隔代组件之间的通信
父组件
<template>
<div>
<sonComponent :msg="msg" />
</div>
</template>
<script>
import sonComponent from './sonComponent.vue';
export default {
components: { sonComponent },
data() {
return {
msg: '我来自父亲组件'
}
},
provide: {
aaprovide: "aaprovide"
}
}
</script>
子组件或者孙子组件
<template>
<div>
<span>{{provide}}</span>
</div>
</template>
<script>
export default {
data() {
return {
provide: this.aaprovide
}
},
inject: ['aaprovide']
}
</script>
四、EventBus (参考自标题文章)
eventBus 又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。eventBus也有不方便之处, 当项目较大,就容易造成难以维护的灾难
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
// 需要在组件中先引入EventBus
import {EventBus} from './event-bus.js'
// 在代码中调用即可
EventBus.$emit('addition', {
num: 5 // 可以传入当前组件的变量
})
// 响应
EventBus.$on('addition', param => {
this.count = this.count + param.num;
})
// 移除该事件
EventBus.$off('addition', {})
五、Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。分为五个模块:
state类似与vue的data,用于数据的存储。
getter类似与computed,计算属性。基于state数据的二次包装,常用于数据的筛选和多个数据的相关性计算。
mutation中定义了同步的方法,通过它来修改state,是修改state数据的唯一途径。
action中定义了异步的方法,一般用于请求,可以通过调用mutation来修改state。
modules用于项目中将各个模块的状态分开定义和操作,便于维护。
将需要进行通信的数据存储在store中,需要数据的组件通过连接store获取state的值即可实现组件间的通信。该方式可实现全部组件的通信。
六、localStorage / sessionStorage
该方法是最常见的组件通信方法之一了。在不同的组件,跨页面都可以进行通信的一种方式。
// 存储的两种方式
localStorage.setItem("isConfirm", "true");
localStorage.isConfirm= "true";
// 读取
localStorage.getItem("isConfirm");
localStorage.isConfirm;
// 存储的两种方式
sessionStorage.setItem("isConfirm", "true");
sessionStorage.isConfirm= "true";
// 读取
sessionStorage.getItem("isConfirm");
sessionStorage.isConfirm;
keep-alive
参考:keep-alive
通过vue cli 创建了一个工程,在该工程的app.vue中
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
是这个样子的,点击了router-link会根据路由,在router-view中显示对应路由的组件内容,想要添加缓存,则需要修改成以下样子
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</div>
<keep-alive>
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />
</div>
</template>
意思是如果我们在router配置了keepAlive为true,则会使用keep-alive区域内的router-view,从而数据就会进行缓存了!相反则使用keep-alive外的router-view。
当然,前提是我们必须先在router中配置好keepAlive
const routes = [
{
path: '/',
name: 'Home',
component: Home,
meta: {
keepAlive: true // 需要被缓存
}
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue'),
meta: {
keepAlive: true // 需要被缓存
}
}
]
这样子的话,两个页面之间互相切换,都可以进行值的缓存了!
当然在组件中的部分区域也是可以实现缓存的。看以下例子:
// router
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue'),
children: [ //需要执行缓存有关联操作的页面,都要写在children下
{
path: '/pageFirst',
name: 'pageFirst',
component: () => import('../components/pageFirst.vue')
},
{
path: '/pageSecond',
name: 'pageSecond',
component: () => import('../components/pageSecond.vue')
}
]
}
]
我们在about页面下配置了children,即为about页面配置了两个子路由。
about页面:
<template>
<div class="about">
<router-link to="/pageFirst">goFrist</router-link>
<router-link to="/pageSecond">goSecond</router-link>
<h1>This is an about page</h1>
<keep-alive>
<router-view />
</keep-alive>
</div>
</template>
点击router-link,下面的router-view区域就会跳转到对应的组件,用keep-alive包裹住就可以将两个组件的数据缓存起来了!
v-if 和 v-show 的区别
v-if:添加了vif的标签,类似于给标签定义了display 的css属性,当值为true时正常显示,当值为false就类似diaplay: none。它带来的影响就是会影响到当前的文档流排布,该dom元素会被隐藏不会被渲染,它不占据空间。如果是定义在组件标签上,vif的值在true和false来回切换时,相当于是重渲染,是会执行该组件的正常生命周期函数的。因为影响到了文档流排布,会引起了回流的现象。
v-show:添加了vshow的标签,类似于给标签定义了visibility 的css属性,当值为true时正常显示,当值为false就类似visilibity: hidden。设置了该值的区域会隐藏起来,但是它还会占据原本的空间,不影响文档流的排布。就只是单纯的“看不见”。当值在true和false来回切换时,是不会执行该组件的正常生命周期函数的。但是会触发重绘的现象。
当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。
当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。
Vue Router
我们用 vue cli 创建完工程以后,都会有一个router文件夹,里面有一个index.js文件,配置了基本的路由。
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
}
]
基本路由
如上配置了两个页面的基本路由,当我们启动工程的时候会默认加载app.vue页面,app.vue页面内的<router-view />
区域则会默认加载路径为’/'的页面,即为引入home组件。
我们可以在页面上定义<router-link to="/home" />
标签,用来点击后跳转到’/home’路由。也可以在页面的其他标签绑定click方法,在方法中调用this.$router.push('/home')
来跳转。
子路由
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue'),
children: [ //需要执行缓存有关联操作的页面,都要写在children下
{
path: '/pageFirst',
name: 'pageFirst',
component: () => import('../components/pageFirst.vue'),
},
{
path: '/pageSecond',
name: 'pageSecond',
component: () => import('../components/pageSecond.vue'),
}
]
}
]
如上,在About路由中配置了两个子路由,所以在about页面中的<router-view />
区域则会显示对应子路由组件的内容。
路由传参数
const routes = [
{
path: '/home/:id/:name',
name: 'Home',
component: Home
}
]
如上,可以在一个路由中设置多段“路径参数”,对应的值都会设置到 $route.params 中。
Vuex
参考:Vuex 官方文档
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
-
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
-
你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
State
// 最简单的store例子
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0,
name: 'tom'
}
})
template: `<div id="app">{{ count }}</div>`,
computed: {
count() {
return this.$store.state.count
}
}
mapState 辅助函数
我们可以通过computed计算属性讲store中state的值绑定在vue对象中进行使用,但是当这样的变量变多了一个一个绑定就变得不太方便了。所以就出现了mapState辅助函数。
首先我们需要将他引入到我们的组件中。import { mapState } from 'vuex'
然后在computed中配合上…扩展运算符
当映射的计算属性的名称与 state 的子节点名称相同时,我们可以给 mapState 传一个字符串数组。
computed: {
...mapState([ 'count', 'name' ]),
}
在…mapStete([])数组中写入要引入的state值,这样子就绑定好了。
如果想要自定义变量的名称,可以传入一个对象,有多种方式可以获取state值
data: {
return {
number: 2
}
},
computed: {
...mapState({
countNumber: state => state.count, // 第一种方式用箭头函数返回state中的count 箭头函数中的this会出问题
countNumber2: 'count' // 第二种方式 直接写字符串与state中的变量同名即可
countNumber3(state) { // 第三种方式 ,此方式的this即是vue对象
return state.count + this.number
}
}),
}
使用这种方式也可以绑定。
Getter
getter其实就是store的计算属性,在官方文档中是这样定义的:Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Getter 接受 state 作为其第一个参数:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
Getter 也可以接受其他 getter 作为第二个参数:
getters: {
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
mapGetters 辅助函数
它的用法跟mapState是类似的
import { mapGetters } from 'vuex'
export default {
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodos',
'doneTodosCount',
// ...
])
}
}
当然也能自定义变量名称
...mapGetters({
doneTodosFake: 'doneTodos',
doneTodosCount: 'doneTodosCountFake'
})
Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
// 最简单的store例子
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0,
name: 'tom'
},
mutations: {
increment (state) {
state.count++
}
}
})
我们可以调用mutations中的increment方法来使state中的count值得到修改。在组件中,如果我们需要进行获取state或者调用mutation的方法时,首先我们得绑定store。
new Vue({
el: '#app',
store,
})
绑定了以后就可以在页面中展示store中的变量或者调用方法了!
template: `<div id="app">{{ count }}</div>`,
computed: {
count() {
return this.$store.state.count
}
},
methods: {
increment() {
this.$store.commit('increment')
console.log(this.$store.state.count)
}
}
官方:你可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload),其实就是可以传递参数拉,真的太官方了。。
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
调用的时候将参数传递进去
store.commit('increment', 10)
官方提示:!
Mutation 需遵守 Vue 的响应规则
既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:
最好提前在你的 store 中初始化好所有所需属性。
当需要在对象上添加新属性时,你应该
使用 Vue.set(obj, ‘newProp’, 123), 或者
以新对象替换老对象。例如,利用对象展开运算符 (opens new window)我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }
mapMutation
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
Action
因为修改state的方式只能通过mutation,所以在action中如果想要修改state,需要提交mutation。
Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象。其实第一个参数就是store本身,类似于我们在组件中的 this.$store。所以自然的可以调用context.commit('increment')
,当前还可以context.state
或者context.getter
实践中,我们会经常用到 ES2015 的 参数解构 (opens new window)来简化代码(特别是我们需要调用 commit 很多次的时候):
actions: {
increment ({ commit, state, getter }) {
commit('increment')
}
}
在组件中调用mutation的方式是store.commit
,调用action的话就是store.dispatch
mapAction
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
Action 通常是异步的,store.dispatch
可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch
仍旧返回 Promise:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
store.dispatch('actionA').then(() => {
// ...
})
在另外一个 action 中也可以:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
如果使用 async / await
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
module 我基本没怎么用到过,想要学习可以看官方的文档
Vue 虚拟DOM
真的是一篇好文章啊!vue虚拟dom
Vue源码中虚拟DOM构建经历 template编译成AST语法树 -> 再转换为render函数 最终返回一个VNode(VNode就是Vue的虚拟DOM节点)
nextTick
computed 和 watch 的区别
computed是计算属性,需要依赖一个数据,基于data中声明过或者父组件传递的props中的数据通过计算得到的值,当依赖当数据发生改变时,会重新进行计算得到新值。是不支持异步的。如果computed的属性值是函数,那么默认会调用get方法,函数的返回值就是属性值。
watch是一个监听函数,如下,watch是支持异步的。当他监听的属性发生改变时则会调用这个监听方法handler,该监听方法接收两个参数,一个值属性改变后的值,一个是修改前的值。还可以为该监听方法定义两个属性,deep和immediate。immediate为true时,则组件一加载就立即触发handler函数的执行。deep为true时,进行深度监听,为了发现对象内部值的变化,复杂类型的数据时使用。
watch: {
cityName: {
handler(newName, oldName) {},
deep: true,
immediate: true
}
}
自定义指令
参考:vue自定义指令
全局注册
<div id="app" class="demo">
<!-- 全局注册 -->
<input type="text" placeholder="我是全局自定义指令" v-focus>
</div>
<script>
Vue.directive("focus", {
inserted: function(el){
el.focus();
}
})
new Vue({
el: "#app"
})
</script>
局部注册
即在vue对象内注册自定义指令
<div id="app" class="demo">
<!-- 局部注册 -->
<input type="text" placeholder="我是局部自定义指令" v-focus2>
</div>
<script>
new Vue({
el: "#app",
directives: {
focus2: {
inserted: function(el){
el.focus();
}
}
}
})
</script>
自定义指令钩子函数
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用。
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。
以上的钩子函数会被传入以下参数:
el: 指令所绑定的元素,可以用来直接操作 DOM,就是放置指令的那个元素。
binding: 一个对象,里面包含了几个属性,这里不多展开说明,官方文档上都有很详细的描述。
vnode:Vue 编译生成的虚拟节点。
oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
举例:
Vue.directive('demo', function (el, binding) {
console.log(binding.value.color) // "white"
console.log(binding.value.text) // "hello!"
})