Vue共识:
在 Vue 中我们习惯把虚拟DOM称为 VNode,它既可以代表一个 VNode 节点,也可以代表一颗 VNode 树。
组件的核心是它能够产出一堆VNode。
对于 Vue 来说一个组件的核心就是它的渲染函数,组件的挂载本质就是执行渲染函数并得到要渲染的VNode,至于data/props/computed 这都是为渲染函数产出 VNode 过程中提供数据来源服务的,最关键的就是组件最终产出的VNode,因为这个才是要渲染的内容。
一、Vue基础
1. Vue的基本原理
当一个Vue实例创建时,vue会遍历data选项的属性,用 Object.defineProperty(vue3.0使用proxy )将它们转为 getter/setter 并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
2. 双向数据绑定的原理
vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
1、需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
2、compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
3、Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
①在自身实例化时往属性订阅器(dep)里面添加自己
②自身必须有一个update()方法
③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
4、MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
3. 使用 Object.defineProperty() 来进行数据劫持有什么缺点?
有一些对属性的操作,使用这种方法无法拦截,比如说通过下标方式修改数组数据或者给对象新增属性,vue 内部通过重写函数解决了这个问题。在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为这是 ES6 的语法。
4. MVVM和MVC的区别
MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化我们的开发效率。
在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,当时一旦项目变得复杂,那么整个文件就会变得冗长,混乱,这样对我们的项目开发和后期的项目维护是非常不利的。
(1)MVC
MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。
(2)MVVM
MVVM 分为 Model、View、ViewModel 三者。
Model代表数据模型,数据和业务逻辑都在Model层中定义;
View代表UI视图,负责数据的展示;
ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用 户交互操作;
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中 的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的 数据也会在Model中同步。
这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注 对数据的维护操作即可,而不需要自己操作DOM。
(2)MVP
MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中我们使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此我们可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。
5. Computed和Watch的区别
对于Computed:
它支持缓存,只有依赖的数据发生了变化,才会重新计算
不支持异步,当Computed中有异步操作时,无法监听数据的变化
computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data声明过,或者父组件传递过来的props中的数据进行计算的。
如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed
如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。
对于Watch:
它不支持缓存,数据变化时,它就会触发相应的操作
支持异步监听
监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
当一个属性发生变化时,就需要执行相应的操作
监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会出大其他操作,函数有两个的参数:
immediate:组件加载立即触发回调函数
deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。
当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。
总结:
computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的 属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每 当监听的数据变化时都会执行回调进行后续操作。
运用场景:
当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利 用 computed 的缓存特性,避免每次获取值时,都要重新计算。
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率, 并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
6. Computed 和 Methods 的区别
https://segmentfault.com/a/1190000014478664
methods与computed之间的差别:
(1)methods和computed里的方法在初始化执行过后,只要任何值有更新,那么所有在computed计算属性里和其相关的值都会更新。
methods只有在调用的时候才会执行对应的方法,不会自动同步数据。
(2) computed是属性访问,而methods是函数调用
computed带有缓存功能,而methods不是
computed其实是就是属性,之所以与data区分开,只不过为了防止文本插值中逻辑过重,会导致不易维护
(3) computed定义的方法我们是以属性的形式访问的,和data里的属性访问形式一样,{{function}}
但是methods定义的方法,我们必须要加上()来调用,如{{function()}},否则,视图会出现function (){[native code]}的情况
7. slot是什么?有什么作用?原理是什么?
slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。
(1) 默认插槽:又名匿名插槽,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
(2) 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。
(3)作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.slot.default,具名插槽为vm.slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
slot的意思是插槽,想想你的电脑主板上的各种插槽,有插CPU的,有插显卡的,有插内存的,有插硬盘的,所以假设有个组件是computer,组件computer:
<template>
<div>
<slot name="CPU">这儿插你的CPU</slot>
<slot name="GPU">这儿插你的显卡</slot>
<slot></slot>
<slot name="Memory">这儿插你的内存</slot>
<slot name="Hard-drive">这儿插你的硬盘</slot>
</div>
</template>
那么组装一个电脑,就可以在调用组件的页面这么写:
<template>
<computer>
<div slot="CPU">Intel Core i7</div>
<div slot="GPU">GTX980Ti</div>
<div>想加内容就加内容</div>
<div slot="Memory">Kingston 32G</div>
<div slot="Hard-drive">Samsung SSD 1T</divt>
</computer>
</template>
<script>
import computerfrom "./computer";
export default {
name: "page",
components: {
computer
},
data() {
return {
};
},
computed: {},
methods: {
}
};
</script>
页面显示:Intel Core i7 GTX980Ti 想加内容就加内容 Kingston 32G Samsung SSD 1T
二、生命周期
使用建议:
1. beforeCreate:加载loading事件
2. created:结束loading、初始化、请求数据、实现函数自执行
3. mounted:拿回数据,配合路由钩子做一些事
4. beforeDestory:destoryed:当前组件已被删除,清空相关内容
1. created和mounted的区别
(1)created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
(2)mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
2. 接口请求一般放在哪个生命周期中?
可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
能更快获取到服务端数据,减少页面loading 时间;
ssr不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
三、组件通信
如图所示:
A和B、B和C、B和D都是父子关系,C和D是兄弟关系,A和C是隔代关系(可能隔多代)。
eg:
props、on、vuex、children、listeners和provide/inject
方法一、props/$emit
父组件A通过props的方向子组件B传递,B到A通过在B组件中$emit,A组件中v-on的方式实现。
eg:
子组件
<template>
<div class="navBar">
<div class="navBarItem" v-for="item in dataList" :key="item.id" :index="item.id">
<span v-if="item.isNow" class="nowTitle" @click="navClick(item)">{{item.title}}</span>
<span v-else class="title" @click="navClick(item)">{{item.title}}</span>
<span class="separator">></span>
</div>
</div>
</template>
<script>
export default {
name: 'navBar',
data () {
return {}
},
props: {
dataList: Array
},
methods: {
navClick (item) {
this.$emit('navClick', item)
}
}
}
</script>
父组件:
<template>
<NavBar :dataList="navBarData" @navClick="navClick"></NavBar>
</template>
<script>
import NavBar from '@/components/NavBar'
export default {
name: 'detail',
data () {
return {
navBarData: [
{ title: '鲸选资源', url: '/whaleselect', id: 1, isNow: false },
{ title: '文件详情', url: '/jx', id: 2, isNow: true }
]}
},
props: {
dataList: Array
},
methods: {
navClick ({ url, id }) {
this.$utils.link(`${url}/` + id)
}
},
components: {
NavBar,
}
}
</script>
1.父组件向子组件传值
父组件通过props向下传递数据给子组件
2.子组件向父组件传值(通过事件形式)
子组件通过this.$emit('方法名',传递的值),父组件定义同名方法即可
EventBus
EventBus事件总线适用于父子组件、非父子组件等之间的通信;
全局或者公共组件注册一个vue实例,利用里面的注册喝监听事件。
(1)创建事件中心管理组件之间的通信
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
(2)发送事件
假设我们有两个兄弟组件C和D:
<template>
<div>
<C></C>
<D></D>
</div>
</template>
<script>
import Cfrom './C.vue'
import D from './D.vue'
export default {
components: { C, D}
}
</script>
在C组件中发送事件:
<template>
<div>
<button @click="add">加法</button>
</div>
</template>
<script>
import {EventBus} from './event-bus.js' // 引入事件中心
export default {
data(){
return{
num:0
}
},
methods:{
add(){
EventBus.$emit('addition', {
num:this.num++
})
}
}
}
</script>
(3)接收事件
在D组件中发送事件:
<template>
<div>求和: {{count}}</div>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
data() {
return {
count: 0
}
},
mounted() {
EventBus.$on('addition', param => {
this.count = this.count + param.num;
})
}
}
</script>
在上述代码中,这就相当于将num值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。
虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。
方法三 Vuex
1.原理
Vuex实现了一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过Mutation进行,Mutation同时提供了订阅者模式供外部插件调用获取State数据的更新。当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走Action,但是Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。
(1)vuex下的store.js文件
import Vuex from 'vuex'
export default new Vuex.Store({
// 定义状态 值 方法
state: {
// 获取文件夹
getfolderscallback: function () {},
// 验证用户的Id的token
token: '',
// 用户id
uid: '',
// 是否登录
isLogin: false,
// 用户名
username: '',
// 用户头像
logo: ''
},
mutations: {
// 获取文件夹
getfolders (state) {
state.getfolderscallback()
},
// 设置上面的全局变量
setAttr (state, data) {
state[data.name] = data.val
}
}
})
(2)页面改变值
mounted () {
// 设置全局变量-方法-获取文件夹
this.$store.commit('setAttr', {
name: 'getfolderscallback',
val: this.getfolders
})
},}
methods: {
getuser () {
// 本地判断Cookie,判断用户是否登录
if (this.$utils.getCookie(this.$glb.fmCookieName) !== null) {
this.$api.post('/center/getuser', {}, res => {
if (!res.status) {
return
}
this.$store.commit('setAttr', {name: 'isLogin', val: true})
this.$store.commit('setAttr', {name: 'logo', val: res.data.logo})
this.$store.commit('setAttr', {name: 'uid', val: res.data.userid})
this.$store.commit('setAttr', {name: 'token', val: res.data.token})
this.$store.commit('setAttr', {name: 'username', val: res.data.username})
})
}
},
// 上传文件框选择文件夹
getfolders () {
this.$api.post('/file/foldertreelist', {}, (res) => {
this.folderList = res.data
})
},
}
(3)页面调用vuex State中的值
<template>
<!-- 登录或未登录 --S-->
<div class="isLoginBox">
<!-- 登录 -->
<div class="loginBox" v-if="this.$store.state.isLogin">
退出
</div>
<!-- 未登录 -->
<div class="unLoginBox" v-else>
<div class="loginOrRegister">
<span class="login" >登录</span>
<span class="line"></span>
<span class="register">注册</span>
</div>
</div>
</div>
<!-- 登录或未登录 --E-->
</template>
<script>
export default {
name: 'SignUpBar',
data () {
return {
}
},
methods: {
}
</script>
2.各模块在核心流程中的主要功能:
1.Vue Components∶ Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。
- dispatch∶操作行为触发方法,是唯一能执行action的方法。
3.actions∶ 操作行为处理模块,由组件中的$store.dispatch('action 名称',data1)来触发。然后由commit()来触发mutation的调用,间接更新state。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。 - commit∶状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
- mutations∶状态改变操作方法,由actions中的commit('mutation 名称')来触发。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。
- state∶ 页面状态管理容器对象。集中存储Vuecomponents中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
7。 getters∶ state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。
3.Vuex(状态:数组)与localStorage(字符串)
vuex是vue的状态管理器,存储的数据是响应式的。但是并不会保存起来,刷新之后就回到初始状态,具体做饭应该在vuex里数据改变的时候拷贝一份保存到localStorge里面,刷新之后,如果localStorge里有保存的数据,取出来再替换store里面的state
方法四、 listeners
方法五、依赖注入 provide/inject
祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。provide/inject API主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
project / inject是Vue提供的两个钩子,和data、methods是同级的。并且project的书写形式和data一样。
project 钩子用来发送数据或方法
inject钩子用来接收数据或方法
eg:两个组件:A.vue和B.vue,B是A的子组件
// A.vue
export default {
provide: {
name: '测试张三'
}
}
// B.vue
export default {
inject: ['name'],
mounted () {
console.log(this.name); // 测试张三
}
}
核心用法:在A.vue里,设置了一个provide:name,值是测试张三,它的作用就是将name这个变量提供给它的所有子组件。
在B.vue中,通过inject注入了从A组件中提供的name变量,在B组件中,就可以直接通过this.name 访问这个变量,值就是测试张三。
注意:**provide和inject绑定并不是可响应的。这是可以为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。A.vue的name如果改变了,B.vue的this.name是不改变的,仍然是测试张三
provide与inject实现数据响应式
两个办法:
(1)provide祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件的实例的属性,不过这种方法有个缺点就是这个实例上挂载很多没有必要的东西,eg:props,methods
(2)使用2.6最新API Vue.observable 优化响应式 provide
// A 组件
<div>
<h1>A 组件</h1>
<button @click="() => changeColor()">改变color</button>
<ChildrenB />
<ChildrenC />
</div>
......
data() {
return {
color: "blue"
};
},
// provide() {
// return {
// theme: {
// color: this.color //这种方式绑定的数据并不是可响应的
// } // 即A组件的color变化后,组件D、E、F不会跟着变
// };
// },
provide() {
return {
theme: this//方法一:提供祖先组件的实例
};
},
methods: {
changeColor(color) {
if (color) {
this.color = color;
} else {
this.color = this.color === "blue" ? "red" : "blue";
}
}
}
// 方法二:使用2.6最新API Vue.observable 优化响应式 provide
// provide() {
// this.theme = Vue.observable({
// color: "blue"
// });
// return {
// theme: this.theme
// };
// },
// methods: {
// changeColor(color) {
// if (color) {
// this.theme.color = color;
// } else {
// this.theme.color = this.theme.color === "blue" ? "red" : "blue";
// }
// }
// }
// F 组件
<template functional>
<div class="border2">
<h3 :style="{ color: injections.theme.color }">F 组件</h3>
</div>
</template>
<script>
export default {
inject: {
theme: {
//函数式组件取值不一样
default: () => ({})
}
}
};
</script>
方法五、children 与ref
ref:如果在普通的DOM元素上使用,引用指向的就是DOM元素;如果用在子组件上,引用就指向组件实例;
children:访问父/子实例
注意:这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。
(1)用ref来访问组件:
// component-a 子组件
export default {
data () {
return {
title: 'Vue.js'
}
},
methods: {
sayHello () {
window.alert('Hello');
}
}
}
// 父组件(页面)
<template>
<component-a ref="comA"></component-a>
</template>
<script>
export default {
mounted () {
const comA = this.$refs.comA;
console.log(comA.title); // Vue.js
comA.sayHello(); // 弹窗
}
}
</script>
不过,这两种方法的弊端是:无法在跨级或者兄弟间通信
// parent.vue
<component-a></component-a>
<component-b></component-b>
<component-b></component-b>
总结
常用使用场景可以分为三类:
(1)父子通信:
父to子传递数据是通过props,子to父是通过parent/attrs/$listeners;
(2)兄弟通信:
EventBus;
Vuex;
(3跨级通信:
Event Bus;
Vuex;
provide/inject API;
listeners;