VUE相关
!1.vue响应式原理
响应式原理即数据更新,视图也跟着更新。
React是依靠虚拟DOM和diff算法实现的;
而VUE是利用Object.defineProperty的setter和getter方法对数据进行劫持;
利用观察者模式来观察数据;
利用发布订阅者模式,使用dep收集watcher,通过notify方法通知dep里的watcher去进行相应的更新
这个过程定义了三个类:Dep、Watcher、Observer
//Dep类
//Dep对象用于依赖收集,它实现了一个发布订阅模式,完成了数据 Data 和渲染视图 Watcher 的订阅
//每个响应式对象和子对象(每一个数据)都拥有一个Dep实例,subs是Watcher实例数组,数据变化时,通过notify通知给各个watcher
//Dep类实例用于管理-->依赖数据的watcher类实例
class Dep {
constructor() {
//用来存放Watcher对象的数组
this.subs = []
}
//在subs中添加一个Watcher对象--收集依赖--收集所有使用到这个data的Watcher对象
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
this.subs.$remove(sub);
}
//get调用depend
depend() {
if (Dep.target) {
// 搜集依赖,最终会调用下面的 addSub 方法,target是一个Watcher对象
Dep.target.addDep(this);
}
//通知所有的Watcher对象更新视图---向依赖发送消息(通知所有的Watcher对象更新
//set调用notify
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
// 调用对应的 Watcher,更新视图
subs[i].update() //该update方法即Wacther类中的开启一个异步队列的方法
}
}
//订阅者Dep类用于收集依赖、删除依赖和向依赖发送消息等,说白了就是存放创建的一些Watcher观察者对象。
//可以把Watcher理解成一个中介,当数据发生改变时通知Dep类,然后再通知所有的Watcher对象更新视图
// Watcher类
// Watcher 实现了渲染方法 _render 和 Dep 的关联, 初始化 Watcher 的时候,打上 Dep.target 标识,然后调用 get 方法进行页面渲染。
// watcher实例对象分为三种,一是(渲染)render watcher,指编译过程中通过addsub收集依赖;二是user watcher,指在响应化时,对每个对象创建dep实例,也叫做收集依赖;三是计算属性computed watcher,指对compued属性初始化时-没见过
function Watcher() {
constructor(vm, expOrFn) {
this.getter = expOrFn //将vm._render的方法赋值给getter --expOrFn就是vm._render--用来生成虚拟dom、执行diff算法、更新真实dom
this.value = this.get()
},
get() {
Dep.target = this //Dep.target赋值为当前的Watcher对象
const value = this.getter.call(this.vm, this.vm)
return value
},
addDep(dep) {
// 将当前的 Watcher 添加到 Dep 收集池中
dep.addSub(this);
}
update() {
// 开启异步队列,批量更新 Watcher
queueWatcher(this);
}
run() {
// 数据更新之后重新走get方法,和初始化一样,会调用 get 方法,更新视图
const value = this.get();
}
}
//Observer类
//作用是:
//1、把Obeserver类的实例对象挂载在__ob__属性上,以给后续观测数据使用.
//2、避免被重复实例化。
//3、对每一条数据进行实例化Dep类实例,并将数据(对象/数组)保存下来,如果数据是对象,就执行walk函数,对其响应化处理,即调用defineReactive方法;如果数据是数组,就执行observeArray方法,递归地对数组的每个元素调用observe()
class Observer {
constructor(value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (isArray(value)) {
//如果数据是数组,并检查原型链上有没有对数组进行改造,如果没有需要对数组方法进行重写,以便能对数组进行响应式处理
var augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
//完成后调用数组处理方法
this.observeArray(value)
} else {
//不是数组就是对象
this.walk(value)
}
}
walk(obj) {
var keys = Object.keys(obj)
for (var i = 0, l = keys.length; i < l; i++) {
//遍历调用convert方法
this.convert方法(keys[i], obj[keys[i]])
}
}
observeArray(items) {
// 对数组每个元素进行处理
// 主要是处理数组元素中还有数组的情况
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
convert(key, val) {
//响应化处理,对每条数据添加getter和setter
defineReactive(this.value, key, val)
}
addVm(vm) {
(this.vms || (this.vms = [])).push(vm)
}
removeVm(vm) {
this.vms.$remove(vm)
}
}
基于观察者模式,涉及三个类、三个阶段,
Observe类用于给每条数据添加getter、setter;
Watcher类起到一个中介作用,数据更新时通知dep类,然后通知所有订阅的Watcher对象重新渲染,更新视图;
Dep类用于收集依赖、删除依赖、向依赖发送消息,也就是管理订阅的Watcher对象。
- 组件初始化阶段,给每一个Data属性通过
Object.defineProperty都注册getter、setter,也就是data响应化reactive化,完成数据代理;同时对每条数据创建Dep实例----用来对数据进行user watcher依赖收集,即每个数据的Watcher对象。 - 编译模板阶段,new一个Watcher对象,此时watcher会立即调用组件的
render函数去生成虚拟DOM,并将Dep.target标识为当前Watcher。 - 编译模板过程中,当调用data属性时,即触发get请求,会通过
Dep.addSub将new的Watcher对象加入到依赖收集池Dep的subs中。--render watcher依赖收集 - 数据更新阶段,data属性改变,触发set方法,通过
Dep.notify遍历subs中的所有的watcher对象,通知他们调用update方法,去重新渲染组件。--向依赖发送消息
总之就是给数据注册一个getter、setter,当数据发生改变时,vue监听到了改变,于是重新渲染组件。
但vue无法监听对象的属性新增和删除,直接通过obj.xxx=xxx是检测不到的。只能对对象的属性进行监听,无法对数组的变化监听。
Vue出于性能考虑没有对数组下标进行劫持,而是通过改写数组原型方法,如果想监听数组变化就需要对数组的常用方法进行重写。解决方法:
splice:arr.splice(index, 1, value)Vue.set(target, index, value)- 想实现对象的属性的新增和删除使网页能更新视图,即view可监听到,解决方法:
- 新增属性:
Vue.set(target, key, value) - 删除属性:
Vue.delete(target, key)
还有一个问题则是,如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问题。
!?2.VUE双向绑定原理
- 监听器(Observer):对所有数据的属性进行监听
- 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
- 观察者(Watcher):组件编译阶段时,会new一个watcher对象,也就是订阅数据,绑定更新函数的过程,以供监听器监听到数据变化通知到Dep对象,再通知到Watcher对象,再调用update函数来更新视图
也是涉及了三个类,三个过程。
Observer类用于给每一条属性注册setter、getter,完成数据代理;
Watcher类起到一个中介作用,在初始化时new一个Watcher对象,添加到Desp中参与订阅,更新时通知dep中所有参与订阅的对象更新视图,重新渲染;
Dep类用于收集依赖、删除依赖、向依赖发送消息,管理所有的依赖。
- 组件初始化阶段,给每一个Data属性通过
Object.defineProperty都注册getter、setter,也就是data响应化reactive化,完成数据代理,这个过程在Observer中完成。 - 编译阶段,找到v-model双向绑定的数据,从data中获取初始化视图,这个过程在Compile中完成。(model->view)
- 数据更新阶段,data变化,触发了set方法,通过
Dep中的notify方法遍历所有订阅者subs(Wacther对象)执行update函数,重新渲染组件,更新视图。(view->model)
<input id='text' />
<span id='sp'></span>
<script>
let obj = {}
let input = document.getElementById('text')
let span = document.getElementById('sp')
function defineProperty(obj, 'msg', {
configurable: true,
enumerable: true,
get() {
console.log('获取数据了')
},
set(newVal) {
console.log('数据更新了')
text.value = newVals
sp.innerHTML = newVal
}
})
//输入监听--当文本框输入内容时,改变obj.msg的值
text.addEventListener('keyup', function(event) {
obj.msg = event.target.value
})
</script>
// 以上就是一个简单的vue双向绑定
3.vue中的key有什么作用?
作用:
用于管理可复用的元素,vue为了高效渲染元素,会首先考虑复用,而不是从头渲染。这样做虽然有时候挺快,但是有时候不符合实际的需求。如需要进行不同的input切换时(两种登陆方式),切换后需要清除input中的内容,如果直接复用就不能实现,这就需要给每个input添加一个具有唯一值的key属性即可。或者数据项的顺序发生变化了,不可以简单对元素进行复用,也要给每个元素添加唯一的key属性,保证高效地更新虚拟DOM。
或者可以解释为当数据发生变化时,vue内部要进行节点也就是虚拟DOM的对比,key值是进行判断新旧节点是否是同一个节点的依据,也可以说是判断是否发生变化的依据。
如v-for中,如果不加key,当数据发生改变时,并不会重新渲染,也就是不会重新创建新的dom结构,仅仅是替换了元素,对原结构进行复用,这样的话对新传入的数据做一些操作就不会触发?
4.vue改变数组中的值vue怎么监听到的?
通过Object.defineProperty()劫持数组为其设置getter和setter后,调用的数组的push、splice、pop等方法改变数组元素时并不会触发数组的setter,这就会造成使用上述方法改变数组后,页面上并不能及时体现这些变化,也就是数组数据变化不是响应式的。
但是实际开发时,页面能实现数组的数据响应式变化。
这是因为vue重写了数组的push等一系列方法。
3 // 获取数组的原型Array.prototype,上面有我们常用的数组方法
4 const arrayProto = Array.prototype
5 // 创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
6 export const arrayMethods = Object.create(arrayProto)
7
8 // 列出需要重写的数组方法名
9 const methodsToPatch = [
10 'push',
11 'pop',
12 'shift',
13 'unshift',
14 'splice',
15 'sort',
16 'reverse'
17 ]
18 // 遍历上述数组方法名,依次将上述重写后的数组方法添加到arrayMethods对象上
19 methodsToPatch.forEach(function (method) {
20 // 保存一份当前的方法名对应的数组原始方法
21 const original = arrayProto[method]
22 // 将重写后的方法定义到arrayMethods对象上,function mutator() {}就是重写后的方法
23 def(arrayMethods, method, function mutator (...args) {
24 // 调用数组原始方法,并传入参数args,并将执行结果赋给result
25 const result = original.apply(this, args)
26 // 当数组调用重写后的方法时,this指向该数组,当该数组为响应式时,就可以获取到其__ob__属性
27 const ob = this.__ob__
28 let inserted
29 switch (method) {
30 case 'push':
31 case 'unshift':
32 inserted = args
33 break
34 case 'splice':
35 inserted = args.slice(2)
36 break
37 }
38 if (inserted) ob.observeArray(inserted)
39 // 将当前数组的变更通知给其订阅者
40 ob.dep.notify()
41 // 最后返回执行结果result
42 return result
43 })
44 })
5.vue的传值方式
一、父子之间传值
1.通过props使父组件向子组件传值
<template>
<div class="parent">
<h2>{{ msg }}</h2>
<son psMsg="父传子的内容:叫爸爸"></son> <!-- 子组件绑定psMsg变量-->
</div>
</template>
<script>
import son from './Son' //引入子组件
export default {
name: 'HelloWorld',
data () {
return {
msg: '父组件',
}
},
components:{son},
}
</script>
父组件中通过在子组件上添加值,然后子组件通过props接收一下,子组件就可以用了
<template>
<div class="son">
<p>{{ sonMsg }}</p>
<p>子组件接收到内容:{{ psMsg }}</p>
</div>
</template>
<script>
export default {
name: "son",
data(){
return {
sonMsg:'子组件',
}
},
props:['psMsg'],//接收psMsg值
}
</script>
2.通过emit事件使子组件向父组件传值
<template>
<div class="parent">
<h2>{{ msg }}</h2>
<p>父组件接收到的内容:{{ username }}</p>
<son psMsg="父传子的内容:叫爸爸" @transfer="getUser"></son> <!--绑定自定义事件transfer-->
</div>
</template>
<script>
import son from './Son'
export default {
name: 'HelloWorld',
data () {
return {
msg: '父组件',
username:'',
}
},
components:{son},
methods:{
getUser(msg){
this.username= msg
}
}
}
</script>
emit事件包含两个参数,第二个值是要传给父组件的值,第一个值是父组件的点击事件,该点击事件触发父组件的方法去获取传来的值。
子组件点击按钮,触发setUser方法,通过emit事件触发了父组件中的transfer自定义事件,父组件触发getUser方法,将子组件通过emit事件一起携带过来的user通过msg的形式赋给了username。
<template>
<div class="son">
<p>{{ sonMsg }}</p>
<p>子组件接收到内容:{{ psMsg }}</p>
<!--<input type="text" v-model="user" @change="setUser">-->
<button @click="setUser">传值</button>
</div>
</template>
<script>
export default {
name: "son",
data(){
return {
sonMsg:'子组件',
user:'子传父的内容'
}
},
props:['psMsg'],
methods:{
setUser:function(){
this.$emit('transfer',this.user)//将值绑定到transfer上传递过去
}
}
}
</script>
3.children
在组件内部可以直接通过子组件$parent对父组件进行操作,父组件通过$children对子组件进行操作.
定义一个方法:
this.$parent.message = this.mymessage
4.使用v-model实现父子双向动态传值
v-model实现数据双向绑定的原理等同于单向绑定并触发了input事件
<input v-model="something">
等同于
<input
v-bind:value="something"
v-on:input="something = $event.target.value"> // 这里的 $event.target.value 是指的js对象的property
<input
:value="something"
@input="something = $event.target.value">
根据这个原理我们可以通过子组件的$emit方法来触发input事件,父组件监听input事件中传递的value值,并储存在父组件的data中;
然后父组件通过props的形式传给子组件value值,在子组件的input中绑定value属性即实现了双向的绑定及传输。
具体实现过程:
子组件中给input标签绑定input事件,使用$emit触发父组件的input事件,通过这种方法实现子组件给父组件传值
<input type="text" @input="$emit('input', $event.target.value)" :value='value'>
//...
export default {
name: 'childClass',
props: ['value']
}
父组件监听input事件,然后将事件携带的input输入的值传入data中
<child-class @input="value = $event" :value='value'></child-class>
export default {
// ...
data () {
return {
value: 'initial value' // 维护一个 value 状态
}
}
}
最后给父组件中的child-class增加:value="value"再通过props传value给子组件,子组件的input用:value="value"双向绑定
如果不是input而是checkbox则触发的是change事件,value就是checked
二、不同组件之间传值。
1.通过vuex实现不同组件之间传值
2.通过eventBus传值(小项目适用-跨多级和兄弟组件传)
第一步,新建一个bus.vue文件
第二步在发送消息的组件中,导入bus.vue,再用bus.$emit来传入两个参数,第一个为消息名或者事件名,第二个为通信的值。
第三步在接收消息的组件中,导入bus.vue,再用bus.$on来接收两个参数,第一个为消息名或者事件名,第二个为一个回调函数,函数的形参为通信的值。
或者不在每个组件中引入bus.vue文件,只引入到main.js中,用Vue.prototype.$bus=Bus挂载到全局,这样直接在组件中使用this.$bus.$emit()和this.$bus.$on()即可。
3.$attrs和$listeners(跨多级传值-爷给孙用attrs--孙给爷用listeners)
$attrs就像是一个容器,存放着父组件通过v-bind绑定的传给子组件的数据,但子组件没有用props声明接收的数据。
<template>
<div id="app">
我是爷组件
<fu v-bind:msg1="msg1" :msg2="msg2" @fromSun="fromSun"></fu> <!-- msg就是要传给父组件和子组件的数据 -->
</div>
</template>
<script>
import fu from "./views/fu.vue";
export default {
components: {
fu,
},
data() {
return {
msg1: "孙悟空",
msg2: "猪八戒",
}
},
methods: {
fromSun(payload) {
console.log("孙传祖", payload);
this.fromSunData = payload;
}
}
}
</script>
父组件中只用props来接收msg1的数据,msg2不做接收,但是由于给了v-bind,所以自动存在了父组件的$attrs中了,可以通过$attrs.msg2来接收。
$listeners的用法:
第一步,在父组件中加上v-on="$listeners"
第二步,爷组件中定义事件方法fromSun
第三步,孙组件中用点击事件sendToZu去触发父组件的事件方法,把孙中的数据传给爷。
<template>
<div class="fatherClass">
我是父组件
<h2>{{ msg1 }}</h2>
<h2>{{ $attrs.msg2}}</h2>
<sun-class v-bind="$attrs" v-on="$listeners"></sun-class>
</div>
</template>
<script>
export default {
name: "DemoFather",
props: {
msg1: {
type: String,
default: "",
},
},
};
</script>
再通过在父组件中v-bind="$attrs"把数据传给孙组件,这样孙组件就可以用props来接收数据。
<template>
<div class="sunClass">
我是孙子组件
<h2>接收爷组件数据:-->{{ msg2 }}</h2>
<button @click='sendToZu'>孙传祖</button>
</div>
</template>
<script>
export default {
// $attrs一般搭配interitAttrs 一块使用
inheritAttrs: false, // 默认会继承在html标签上传递过来的数据,类似href属性的继承
/*
孙子组件通过props,就能接收到父组件传递过来的$attrs了,就能拿到里面的数据了,也就是:
爷传父、父传子。即:祖孙之间的数据传递。
*/
props: {
msg2: {
type: String,
default: "",
}
},
data() {
return {
data: "来自孙组件的数据",
};
},
methods: {
sendToZu() {
// 孙组件能够触发爷组件的fromSun方法的原因还是因为父组件中有一个$listeners作为中间人,去转发这个事件的触发
this.$emit("fromSun", this.data);
},
},
name: "DemoSun",
};
</script>
4.provide和inject (用于爷向子孙传值)
作用:用于父组件向子孙组件传递数据
使用方法:provide在父组件中返回要传给下级的数据,inject在需要使用这个数据的子辈组件或者孙辈等下级组件中注入数据。
使用场景:由于vue有$parent属性可以让子组件访问父组件。但孙组件想要访问祖先组件就比较困难。通过provide/inject可以轻松实现跨级访问父组件的数据
在爷组件中使用provide来发送
<template>
<div class="yeClass">
<child></child>
</div>
</template>
<script>
export default {
name: "yeClass",
data() {
return {
data: "来自爷组件的数据",
};
},
components: {
child
},
provide: {
for: 'demo'
}
};
</script>
在子组件或者孙组件中使用inject来接收
<template>
<div class="sunClass">
</div>
</template>
<script>
export default {
name: "sunClass",
inject: ['for'],
data() {
return {
demo:'this.for'
};
};
</script>
总结:
8种vue组件间通信方式:
父子之间:props、$emit;$parent和$children、v-model
祖先与后代之间:provide和inject、$attrs和$listeners
任意组件之间:vuex、事件总线BUS
补充:使用$refs获取组件实例,进而获取数据
6.vue的传值方式之一—vuex
state:定义初始状态getter:从store从取数据mutation:更改store中状态,只能同步操作action:用于提交mutation,而不直接更改状态,可异步操作module:store的模块拆分
7.vue的优缺点?和react的区别?
优点:
-
渐进式框架
意思是框架的使用者的要求很弱,需要什么功能就用什么。不像Angular,使用它就必须使用全套它的东西;也不像React,但仍有一些强制性要求,即自己的主张,如要求函数式编程等。vue有组件化开发、大规模状态管理vuex、构建工具vite、vue-resource不用推荐用axios,这些功能想用就用,不想要就不要,这也是vue的设计理念-渐进式的框架。
-
虚拟dom
组件刚被创建和数据更新时会重新调用render函数渲染视图,如果直接使用真实DOM,会浪费极大的性能,降低渲染效率;使用虚拟DOM和数据的双向绑定,进行真实和虚拟DOM的对比,具体的是内部的diff算法中的patch函数来进行节点的对比。(vue的diff算法:传统的diff算法时间复杂度为o(n3),而它只需要进行同层对比就行,复杂度为o(n)
-
组件化开发
-
响应式数据
-
单页面开发
不需要跳转页面和重新渲染就可以切换不同的页面,用户体验好,服务器压力也小;当然也有缺点:首屏加载慢、搜索引擎优化SEO难(所有页面都在一个页面中动态显示)
-
数据和视图分开
缺点:
- 单页面应用很难去做搜索引擎优化SEO
- 首屏加载太慢
- 不兼容IE
和react作比较
相同点:
- 都是单向数据流
- 都使用虚拟dom的技术
- 都支持服务端优化ssr
- 都是组件化开发
不同点:
数据的变化,vue是响应式的,react用的是手动setState
vue实现的是双向绑定,react是单向
vue状态管理工具vuex,react是Redux、Modx
8.SPA的实现
单页面应用:不需要重新加载整个页面来实现页面之间的切换
原理:
1.监听地址栏中hash变化来驱动界面变化
2.用pushsate记录浏览器的历史,驱动界面发生变化
实现路由跳转从而实现SPA的两种模式:
1.hash模式
核心是通过监听url中的hash来进行路由跳转
2.history模式
核心借用了HTML5 history 的api,如:
history.pushState浏览器历史纪录添加记录history.replaceState修改浏览器历史纪录中当前纪录history.popState当history发生变化时触发
如何给SPA做SEO---vue2缺点一
1.SSR服务端渲染
将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js
2.静态化
目前主流的静态化主要有两种:
(1)一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中
(2)另外一种是通过WEB服务器的 URL Rewrite的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果
3.使用phantomjs针对爬虫处理
原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。
SPA首屏加载优化方案---vue2缺点二
常见的几种SPA首屏优化方式
- 减小入口文件积--懒加载
- 静态资源本地缓存--合理运用localstorage
- UI框架按需加载
- 图片资源的压缩
- 组件重复打包
- 开启GZip压缩
- 使用SSR 服务端优化
9.vue路由守卫的作用和应用场景
路由守卫:路由跳转前做一些验证,如登陆验证
全局路由守卫: 指路由实例上直接操作的钩子函数,特点是所有的路由配置都会触发这些钩子函数。包括:
beforeEach:在路由跳转前触发,传入三个参数,包括to、from、next,这个钩子的作用是在路由跳转前进行验证,用的最多的路由守卫钩子。
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
beforResolve:和上面的一样,区别为在 beforeEach 和 组件内beforeRouteEnter 之后,afterEach之前调用。基本不用。
afterEach:和beforeEach相反,路由跳转完成后触发,传入两个参数,包括to、from。发生在beforeEach和beforeResolve之后,beforeRouteEnter(组件内守卫)之前。
路由独享守卫: 单个路由配置的时候也可以设置的钩子函数,
beforeEnter:和beforeEach完全相同,如果都设置则在beforeEach之后紧随执行,参数to、from、next
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
组件内路由守卫: 是指在组件内执行的钩子函数,类似于组件内的生命周期,相当于为配置路由的组件添加的生命周期钩子函数。
钩子函数按执行顺序包括beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave三个,执行位置如下
export default{
data(){
//...
},
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
10.说说你对slot的理解?slot使用场景有哪些?
当我们想要复用组件时,但是想对组件中少量的地方进行更改,不进行重写,而是通过slot插槽的方式向组件内部想改的位置传递内容,完成这个复用组件在不同场景的应用。
如:布局组件、表格列、下拉选项、弹框显示内容等
插槽包括:
- 默认插槽
- 具名插槽
- 作用域插槽
默认插槽就是普通的插槽。
具名插槽:
在子组件中用name属性来表示插槽的名字,不传的时候就是默认插槽。
父组件在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值
作用域插槽:
子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上
父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用
子组件Child.vue
<template>
<slot name="footer" testProps="子组件的值">
<h3>没传footer插槽</h3>
</slot>
</template>
父组件
<child>
<!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
<template v-slot:default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
</template>
<template #default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
</template>
</child>
总结:
v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使用- 默认插槽名为
default,可以省略default直接写v-slot - 缩写为
#时不能不写参数,写成#default - 可以通过解构获取
v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"
?11.虚拟dom和diff算法?
虚拟dom是对数据通过Object.defineProperty进行数据劫持的同时新建一个Wacther对象,该对象会调用render函数来渲染生成虚拟dom,也就是组件刚被创建时,数据更新时会对新旧虚拟dom对比生成真实dom,而diff算法就是在这个过程中进行优化的。
而虚拟dom存在的作用就是避免多次dom操作而对页面的重排,虚拟dom不会进行重排操作,只会通过新旧dom对比从而生成真实dom,一次性地进行重排。
diff 算法是一种通过同层的树节点进行比较的高效算法,组件创建或更新时会执行vue内部的update函数,该函数运行render函数得到虚拟dom树,将他和旧的虚拟dom树对比,完成真实dom的更新,渲染页面。
两棵树进行对比更新差异就是diff算法,因为diff算法是通过patch函数完成的,所以也叫patch算法。
特点:
- 比较只会在同层级进行, 不会跨层级比较
- 在diff比较的过程中,循环从两边向中间比较(这一点相比react更高效)
- 对比过程中发现新节点不存在时,就直接删掉旧节点;新节点存在,旧节点不存在,就直接新建
- 这样相比传统的diff算法(新旧节点互相对比o(n^2)、对比完进行dom操作还需要遍历一次,这样就o(n^3))时间复杂度变为o(n)只需要所有节点同级比较,只会遍历一次。
diff 算法在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较
具体步骤:
-
第一步:调用patch方法,传入新旧虚拟dom,开始同层对比
-
第二步:调用isSameNode方法,对比新旧节点是否未同类型节点
-
第三步:如果不同,新节点直接替换旧节点
-
第四步,如果相同,调用patchNode进行节点对比
- 如果旧节点和新节点都是文本节点,则新文本代替旧文本
- 如果旧节点有子节点,新节点没,则删除旧节点的子节点
- 如果旧节点没有子节点,新节点有,则把子节点新增上去
- 如果都有子节点,则调用
updateChildren方法进行新旧子节点的对比 - 子节点对比为首尾对比法
?12.vue中的axios封装
axios 是一个轻量的 HTTP客户端,基于ajax和promise的
基于 XMLHttpRequest 服务来执行 HTTP 请求,支持丰富的配置,支持 Promise,支持浏览器端和 Node.js 端。自Vue2.0起,尤大宣布取消对 vue-resource 的官方推荐,转而推荐 axios。
特性
- 从浏览器中创建
XMLHttpRequests - 从
node.js创建http请求 - 支持
PromiseAPI - 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换
JSON数据 - 客户端支持防御
XSRF
13.说说对nextTick的理解
vue中对于dom元素的修改是异步的,不能马上得到最新的数据变化,在nextTick中的回调函数可以得到最新的数据变化,
new Vue({
el: '.app',
data: {
msg: 'Hello Vue.',
msg1: '',
msg2: '',
msg3: ''
},
templete: {
<div class="app">
<div ref="msgDiv">{{msg}}</div>
<div v-if="msg1">Message got outside $nextTick: {{msg1}}</div>
<div v-if="msg2">Message got inside $nextTick: {{msg2}}</div>
<div v-if="msg3">Message got outside $nextTick: {{msg3}}</div>
<button @click="changeMsg">
Change the Message
</button>
</div>
},
methods: {
changeMsg() {
this.msg = "Hello world."
this.msg1 = this.$refs.msgDiv.innerHTML
this.$nextTick(() => {
this.msg2 = this.$refs.msgDiv.innerHTML //结果只有msg2变成了hello world
})
this.msg3 = this.$refs.msgDiv.innerHTML
}
}
})
因为vue中dom元素的更新是异步的,当检查到数据变化,vue将会开启一个队列(queueWatcher(this)),并将多个数据改变放到同一个事件循环中?批量更新watcher。而且同一个watcher如果被触发了多次,只会被推入到队列中一次,这样的话可以去除重复数据对于避免不必要的计算和dom操作非常重要。
应用场景:
- 在Vue生命周期的
created()钩子函数进行的dom操作一定要放在vue.nextTcik()的回调函数中,因为此时还未对dom结构进行渲染。 - 当数据变化后想要执行某些操作,而这个操作需要随着数据改变而立即改变dom结构的时候,这个操作需要放到
vue.nextTcik()的回调函数中。
14.MVVM模型的理解
MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View和ViewModel的双向数据绑定,这使得ViewModel 的状态改变可以自动传递给View,即所谓的数据双向绑定。
MVVM是MVC的升级版,MVC 即Model-View-Controller 的缩写。
View :视图-把数据以某种方式呈现给用户
Model:模型-管理数据
Controller :响应用户操作,并将Model 更新到View 上。
随着技术的更新,大量调用相同API,操作复杂、冗余,影响页面性能、用户体验,Model和View之间的数据同步及更新越来越多、复杂。
MVVM出现解决了以上的问题,
Model-数据模型
View-视图
ViewModel-模型视图
在MVVM架构下,View和Model之间并没有直接的联系,而是通过ViewModel进行交互, 在Model更新时,ViewModel通过绑定器将数据更新到View,在View触发指令时,会通过ViewModel传递消息到Model。
ViewModel 通过双向数据绑定把 View层和 Model层连接了起来,实现数据的同步,不需要手动更新。
优点:
1、主要目的是为了分离视图和模型,各自独立开发。
2、降低代码耦合,提高代码和逻辑的可读性。--视图view可以独立于Model的变化和修改,View和Model之间并没有直接的联系
3、方便测试,ViewModel可以方便代码的测试。
4、具有可复用性,可以把一些试图逻辑放在一个ViewModel中,让很多的View复用这段视图逻辑。
15.vue的生命周期
beforeCreate:未初始化组件和响应式数据(未添加setter、gettercreated:初始化和响应式数据,可以访问数据beforeMount:调用render函数,生成虚拟dommounted:真实dom挂载完成beforeUpdate:数据更新,生成新的虚拟domupdated:新旧虚拟dom比较,打补丁,进行真实dom的更新beforedestory:实例销毁前,仍可以访问数据destory:实例销毁、子实例销毁,解绑指令,解绑实例的事件activated:keep-alive所缓存的组件被激活时调用deactivated:keep-alive所缓存的组件被停用时调用errorCaptured:子孙组件的错误捕获,此函数可返回false阻止继续向上传播
16.vue3与vue2的区别?vue3有哪些亮点?
-
更小:移除了一些不常用的API,通过webpack的
tree-shaking,选择性打包,使文件体积更小了。 -
更快:
-
diff算法优化
- vue3在diff算法中添加了flag标记,进一步提高性能
-
静态提升(静态标记)
- 不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接使用,虚拟DOM按需更新。
-
事件监听缓存
- 绑定事件不会每次都去监听它的变化
-
SSR优化
- 如果存在很多静态属性时,会自动生成一个静态node,不需要创建对象,直接根据对象渲染。
-
-
更友好:
- Vue3是基于TS编写的,类型概念更强,更严谨。
- 兼顾
options API的基础上推出了composition API,提升了代码的逻辑性和复用性。即实现一个功能的所有API会放到一起。适合项目大、逻辑复杂,实现高内聚、低耦合、高复用、逻辑性更强、更易维护。
亮点:
- 源码管理?
- 基于TS编写
- 体积优化
- 数据劫持优化:
Object.defineProperty不能监测对象属性的增加和删除,虽然有set和delete可以用,但是Vue3利用Proxy监听整个对象,同时Proxy并不能监听到内部深层次的对象变化,而Vue3的处理方式是在getter中去递归响应式? composition API提升了代码的逻辑性和复用性。
17.为什么要用 Proxy API 替代 defineProperty API ?
vue2:
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
- 检测不到对象属性的添加和删除
- 数组
API方法无法监听到 - 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题
vue3:
Object.defineProperty只能遍历对象属性进行劫持
Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的
对象属性的添加和删除、数组方法都能正常监听到
只有在getter时才对深层对象进行监听,优化了性能
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`设置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)
return res
}
})
return observed
}
\