[TOC]
Vue
watch与computed的区别 vue生命周期及对应的行为 vue父子组件生命周期执行顺序 组件间通讯方法 如何实现一个指令 如何做到的双向绑定 虚拟dom为什么快 如何设计一个组件
1. 谈谈对MVC、MVP和MVVM的理解
MVC
MVC即Model--View--Controller
,简单来说就是通过controller的控制去操作model层的数据,并且返回给view层展示。
前端MVC与后端类似,具备Model、View和Controller
- Model:负责保存应用数据,与后端数据进行同步
- View:负责视图展示,将model中的数据可视化出来
- Controller:负责业务逻辑,根据用户行为对model数据进行修改
- View 接受用户交互请求
- View 将请求转交给Controller处理
- Controller 操作Model进行数据更新保存
- 数据更新保存之后,Model会通知View更新
- View更新变化数据使用户得到反馈
MVP
MVVM
MVVM即Model-View-ViewModel,将其中的 View 的状态和行为抽象化,让我们可以将UI和业务逻辑分开。MVVM的优点是低耦合、可重用性、独立开发。
- View接收用户交互请求
- View将请求转交给ViewModel
- ViewModel操作Model数据更新
- Model更新完数据,通知ViewModel数据发生变化
- ViewModel更新View数据 MVVM模式和MVC有些类似,但有以下不同:
- ViewModel替换了Controller,在UI层之下
- ViewModel向View暴露它所需要的数据和指令对象
- ViewModel接收来自Model的数据
概括起来,MVVM是由MVC发展而来,通过在Model之上而在View之下增加一个非视觉的组件将来自Model的数据映射到View中。
Model–View–ViewModel(MVVM)是一个软件架构设计模式,由微软 WPF 和 Silverlight 的架构师 Ken Cooper 和 Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。由John Gossman(同样也是 WPF 和 Silverlight 的架构师)于2005年在他的博客上发表。
MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。
MVVM源自于经典的Model–View–Controller(MVC)模式 ,MVVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率,MVVM 的核心是 ViewModel 层,它就像是一个中转站(value converter),负责转换 Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与 Model 层通过接口请求进行数据交互,起呈上启下作用。如下图所示:
MVVM是Model-View-ViewModel的缩写。
- Model:代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。我们可以把Model称为数据层,因为它仅仅关注数据本身,不关心任何行为。
- View:用户操作界面。当ViewModel对Model进行更新的时候,会通过数据绑定更新到View。
- ViewModel:业务逻辑层,View需要什么数据,ViewModel要提供这个数据;View有某些操作,ViewModel就要响应这些操作,所以可以说它是Model for View。
View层
View是视图层,也就是用户界面。前端主要由HTML和CSS来构建。
Model层
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的api接口。
ViewModel层
- ViewModel通过实现一套数据响应式机制自动响应Model中数据变化;
- 同时ViewModel会实现一套更新策略自动将数据变化转换为视图更新;
- 通过事件监听响应View中用户交互修改Model中数据。
- 这样在ViewModel中就减少了大量的DOM操作代码
- MVVM在保持View和Model松耦合的同时,还减少了维护它们关系的代码,使用户专注于业务逻辑,兼顾开发效率和可维护性。
ViewModel是由前端开发人员组织生成和维护的视图数据层。在这一层,前端开发者对从后端获取的 Model 数据进行转换处理,做二次封装,以生成符合 View 层使用预期的视图数据模型。需要注意的是 ViewModel 所封装出来的数据模型包括视图的状态和行为两部分,而 Model 层的数据模型是只包含状态的,比如页面的这一块展示什么,而页面加载进来时发生什么,点击这一块发生什么,这一块滚动时发生什么这些都属于视图行为(交互),视图状态和行为都封装在了 ViewModel 里。这样的封装使得 ViewModel 可以完整地去描述 View 层。
MVVM框架实现了双向绑定,这样 ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦地通过操纵 DOM 去更新视图,MVVM 框架已经把最脏最累的一块做好了,我们开发者只需要处理和维护 ViewModel,更新数据视图就会自动得到相应更新。这样 View 层展现的不是 Model 层的数据,而是 ViewModel 的数据,由 ViewModel 负责与 Model 层交互,这就完全解耦了 View 层和 Model 层,这个解耦是至关重要的,它是前后端分离方案实施的重要一环。
我们以下通过一个Vue实例来说明MVVM的具体实现,有Vue开发经验的同学应该一目了然: View层
<div id="app">
<p>{{message}}</p>
<button v-on:click="showMessage()">Click me</button>
</div>
2
3
4
ViewModel层
var app = new Vue({
el: '#app',
data: { // 用于描述视图状态
message: 'Hello Vue!',
},
methods: { // 用于描述视图行为
showMessage(){
let vm = this;
alert(vm.message);
}
},
created(){
let vm = this;
// Ajax 获取 Model 层的数据
ajax({
url: '/your/server/data/api',
success(res){
vm.message = res;
}
});
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Model层
{
"url": "/your/server/data/api",
"res": {
"success": true,
"name": "IoveC",
"domain": "www.cnblogs.com"
}
}
2
3
4
5
6
7
8
总结:MVVM模式简化了界面与业务的依赖,解决了数据频繁更新。MVVM 在使用当中,利用双向绑定技术,使得 Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。
MVVM和MVC的区别?
MVC:MVC模式可以这样理解,将html看成view;js看成controller,处理用户与应用的交互,响应对view的操作(对事件的监听),调用Model对数据进行操作,完成model与view的同步(根据model的改变,通过选择器对view进行操作);将js的ajax当做Model,从服务器获取数据,MVC是单向的。 MVVM:它实现了View和Model的自动同步,也就是当Model的属性改变时,我们不用再自己手动操作Dom元素,来改变View的显示,而是改变属性后该属性对应View层显示会自动改变,MVVM是双向的。
谈谈对vue的理解
vue是一个渐进式的JS框架。他易用,灵活,高效; 可以把一个页面分隔成多个组件;当其他页面有类似功能时,直接让封装的组件进行复用; 他是构建用户界面的声明式框架,只关心图层;不关心具体是如何实现的。
VUE和REACT有什么区别?
react整体是函数式的思想,把组件设计成纯组件,状态和逻辑通过参数传入,所以在react中,是单向数据流;
vue的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立Watcher来监听,当属性变化的时候,响应式的更新对应的虚拟dom。
2. 谈一谈Vue的虚拟DOM和diff算法
Vue.js通过编译将模版转换成渲染函数(render),执行渲染函数就可以得到一个虚拟DOM 简单点讲,在Vue的实现上,Vue讲模版编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应用到DOM操作上。
Vue diff算法:
- 源码分析1:必要性,lifecycle.js - mountComponent()
- 源码分析2:执行方式,patch.js - patchVnode()
- patchVnode是diff发生的地方,整体策略:深度优先,同层比较
- 源码分析3:高效性,patch.js - updateChildren()
- diff算法是虚拟DOM技术的必然产物:通过新旧虚拟DOM做对比(即diff),将变化的部分更新在真实DOM上,另外,也需要diff高效的执行对比过程,从而降低时间复杂度为O(n)。
- Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,只有引入diff才能精确找到发生变化的地方。
- Vue中diff执行的时刻是组件实例执行其更新函数时,它会比对上一次渲染结果oldVnode和新的渲染结果newVnode,此过程称为patch。
- diff过程整体遵循深度优先、同层比较的策略。两个节点之间比较会根据它们是否拥有子节点或者文本节点不同操作;比较两组子节点是算法的重点,首先假设头尾节点可能相同做4次对比尝试,如果没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点;借助key通常可以非常精确找到相同节点,因此整个patch过程非常高效。
3. Vue响应式原理(2.x)
Vue在初始化数据时,会使用Object.defineProperty重新定义data中的所有属性,当页面使用对应属性时,首先会进行依赖收集(收集当前组件的watcher)如果属性发生变化会通知相关依赖进行更新操作(发布订阅)。
Vue采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty劫持data属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
实现mvvm的双向绑定,是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Vue数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
即:
- 输入框内容变化时,Data 中的数据同步变化。即 View => Data 的变化。
- Data 中的数据变化时,文本节点的内容同步变化。即 Data => View 的变化。
其中,View 变化更新 Data ,可以通过事件监听的方式来实现,所以 Vue 的数据双向绑定的工作主要是如何根据 Data 变化更新 View。
Vue主要通过以下4个步骤来实现数据双向绑定的:
- 实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
- 实现一个解析器 Compile:解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
- 实现一个订阅者 Watcher:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
- 实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
Vue是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()
来劫持各个属性的setter和getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
具体实现步骤,感兴趣的可以看看:
当把一个普通Javascript对象传给Vue实例来作为它的data选项时,Vue将遍历它的属性,用Object.defineProperty
都加上 setter和getter。这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
Vue实现数据双向绑定的原理就是用Object.defineproperty重新定义(set方法)对象设置属性值和(get方法)获取属性值的操纵来实现的。 Object.property()方法的解释:Object.property(参数1,参数2,参数3) 返回值为该对象obj 其中参数1为该对象(obj),参数2为要定义或修改的对象的属性名,参数3为属性描述符,属性描述符是一个对象,主要有两种形式:数据描述符和存取描述符。这两种对象只能选择一种使用,不能混合使用。而get和set属于存取描述符对象的属性。
这个方法会直接在一个对象上定义一个新属性或者修改对象上的现有属性,并返回该对象。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="myapp">
<input v-model="message" /><br>
<span v-bind="message"></span>
</div>
<script type="text/javascript">
var model = {
message: ""
};
var models = myapp.querySelectorAll("[v-model=message]");
for (var i = 0; i < models.length; i++) {
models[i].onkeyup = function() {
model[this.getAttribute("v-model")] = this.value;
}
}
// 观察者模式 / 钩子函数
// defineProperty 来定义一个对象的某个属性
Object.defineProperty(model, "message", {
set: function(newValue) {
var binds = myapp.querySelectorAll("[v-bind=message]");
for (var i = 0; i < binds.length; i++) {
binds[i].innerHTML = newValue;
};
var models = myapp.querySelectorAll("[v-model=message]");
for (var i = 0; i < models.length; i++) {
models[i].value = newValue;
};
this.value = newValue;
},
get: function() {
return this.value;
}
})
</script>
</body>
</html>
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
32
33
34
35
36
37
38
39
40
41
Vue3.x响应式数据原理
Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?(很简单啊)
判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理,这样就实现了深度观测。
监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?
我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。
4. Vue 2.x如何监测对象和数组的变化
如果被问到Vue怎么实现数据双向绑定,大家肯定都会回答通过Object.defineProperty对数据进行劫持,但是Object.defineProperty只能对属性进行数据劫持,不能对整个对象进行劫持,同理无法对数组进行劫持,但是我们在使用 Vue 框架中都知道,Vue 能检测到对象和数组(部分方法的操作)的变化,那它是怎么实现的呢?我们查看相关代码如下:
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]) // observe 功能为监测数据的变化
}
}
/**
* 对属性进行递归遍历
*/
let childOb = !shallow && observe(val) // observe 功能为监测数据的变化
2
3
4
5
6
7
8
9
10
11
12
通过以上Vue源码部分查看,我们就能知道Vue是通过遍历数组和递归遍历对象,从而达到利用Object.defineProperty也能对对象和数组(部分方法的操作)进行监听。
使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。
5. Proxy相对于Object.defineProperty的优势
- 数组变化也能监听到
- 不需要深度遍历监听
基于Object.defineProperty可以劫持对象的访问器,在对象属性值发生变化时我们可以获取变化后的值,从而进行进一步操作。
Proxy的优势如下:
- Proxy可以直接监听对象而非属性;
- Proxy可以直接监听数组的变化;
- Proxy有多达13种拦截方法,不限于 apply、ownKeys、deleteProperty、has等等是 Object.defineProperty 不具备的;
- Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改; Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
Object.defineProperty的优势:兼容性好,支持IE9,而Proxy的存在浏览器兼容性问题(ie11都不支持),而且无法用 polyfill磨平,因此,Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写。
6. Vue中的key的作用和工作原理
结论:
- key的作用主要是为了高效的更新虚拟DOM,其原理是Vue在patch过程中通过key可以精准判断两个节点是否是是同一个,从而避免频繁更新不同元素,使得整个patch过程更加高效,减少DOM操作量,提高性能。
- 另外,若不设置key还可能在列表更新时引发一些隐蔽的bug
- Vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让Vue可以区分它们,否则Vue只会替换其内部属性而不会触发过渡效果。
如果不使用key:需要进行3次节点更新操作(C->F、D->C、E->D)和一次节点插入(E重新创建插入)操作。
如果使用key:只进行一次节点插入操作,通过5次patch比较,找出F节点的插入位置,创建F节点并插入。相对于不使用key节省了多次节点更新操作。
- 让Vue精准的追踪到每一个元素,高效的更新虚拟DOM。
- 触发过渡
当text改变时,这个元素的key属性就发生了改变,在渲染更新时,Vue会认为这里新产生了一个元素,而老的元素由于key不存在了,所以会被删除,从而触发了过渡。
<transition>
<span :key="text">{{text}}</span>
</transition>、
2
3
强制替换元素,从而可以触发组件的生命周期钩子或者触发过渡。因为当key改变时,Vue认为一个新的元素产生了,从而会新插入一个元素来替换掉原有的元素。
这里如果text发生改变,整个<span>
元素会发生更新,因为当text改变时,这个元素的key属性就发生了改变,在渲染更新时,Vue会认为这里新产生了一个元素,而老的元素由于key不存在了,所以会被删除,从而触发了过渡。
同理,key属性被用在组件上时,当key改变时会引起新组件的创建和原有组件的删除,此时组件的生命周期钩子就会被触发。
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。Vue的diff过程可以概括为:oldCh和newCh各有两个头尾的变量oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,它们会新节点和旧节点会进行两两对比,即一共有4种比较方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,如果以上 4 种比较都没匹配,如果设置了key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。具体有无 key 的 diff 过程,可以查看作者写的另一篇详解虚拟 DOM 的文章《深入剖析:Vue核心之虚拟DOM》。
所以,Vue中key的作用是:key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。
更准确:因为带key就不是就地复用了,在sameNode函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用key的唯一性生成map对象来获取对应节点,比遍历方式更快,源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
2
3
4
5
6
7
8
9
当 Vue.js 用v-for正在更新已渲染过的元素列表时,它默认用“就地复用”策略。 如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。
key的作用主要是为了高效的更新虚拟DOM。
7. Vue怎么用vm.$set()解决对象新增属性不能响应的问题?
Vue中$set有什么用?
在我们使用vue进行开发的过程中,可能会遇到一种情况:当生成vue实例后,当再次给数据赋值时,有时候并不会自动更新到视图上去,因此可以使用$set。
initTableData() {
this.tableData.forEach(element => {
this.$set(element, 'edit', false)
})
}
2
3
4
5
受现代JavaScript的限制 ,Vue无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性,那框架本身是如何实现的呢?
我们查看对应的 Vue 源码:vue/src/core/instance/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
我们阅读以上源码可知,vm.$set的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
8. Vue组件的data为什么必须是函数而Vue的根实例则没有这个限制?
Vue组件可能存在多个实例,如果使用对象形式定义data,则会导致多个实例共用一个data对象,那么状态变更将会影响所有组件实例,这是有问题的;采用函数形式定义,在initData时会将其作为工厂函数返回全新data对象,有效规避多实例之间状态污染问题。而在Vue根实例创建过程中则不存在该限制,也是因为根实例只能有一个,不需要考虑多实例数据污染的情况。
因为JS本身的特性带来的,如果data是一个对象,那么由于对象本身属于引用类型,当我们修改其中的一个属性时,会影响到所有Vue示例的数据。如果将data作为一个函数返回一个对象,那么每一个实例的data属性都是独立的,不会相互影响了。
组件中的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data。如果单纯的写成对象形式,就使得所有组件实例共用了一份data,造成了数据污染。
如果data是一个函数的话,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份data,就会造成一个变了全都会变的结果。
为什么组件中的data必须是一个函数,然后return一个对象,而new Vue实例里,data可以直接是一个对象?
// data
// 针对使用脚手架的情况
data() {
return {
message: "子组件",
childName:this.name
}
}
// new Vue
// 针对不用脚手架的情况,比如直接通过script标签的形式引入vue.js
new Vue({
el: '#app',
template: '<App/>',
data: {
name: 'lisi'
},
components: {App}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
因为组件是用来复用的,且JS里对象是引用关系,如果组件中data是一个对象,那么这样作用域没有隔离,子组件中的 data属性值会相互影响,如果组件中data选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的data属性值不会互相影响;
而new Vue的实例,是不会被复用的,因此不存在引用对象的问题。
9. computed的实现原理
computed本质上是一个惰性求值的观察者。
computed内部实现了一个惰性的watcher,也就是computed watcher,computed watcher不会立刻求值,同时持有一个dep实例。其内部通过this.dirty
属性标记计算属性是否需要重新求值。
当computed的依赖状态发生改变时,就会通知这个惰性的watcher,computed watcher通过this.dep.subs.length判断有没有订阅者,有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。) 没有的话,仅仅把this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备lazy(懒计算)特性。)
watch、computed和methods的区别
区别:
- computed:计算属性依赖于其他属性的值,并且computed的值会被缓存,只有它依赖的属性值发生改变,下一次获取computed的值时才会重新计算computed的值,主要当作属性来使用。
- watch:更多的是观察的作用,无缓存,主要用来监听某些特定数据的变化,每当监听的数据变化时都会执行回调(接收newVal和oldVal两个参数)进行某些具体的业务逻辑操作,可以看作是
computed
和methods
的结合体; - methods:表示一个具体的操作,主要书写业务逻辑。我们通常在这里面写入组件需要的方法,只要调用就会重新执行一次。
computed和methods区别是:
- computed是基于响应性依赖来进行缓存的。只有在响应式依赖发生改变时它们才会重新求值,也就是说,当msg属性值没有发生改变时,多次访问 reversedMsg计算属性会立即返回之前缓存的计算结果,而不会再次执行computed中的函数。
- 但是methods方法中是每次调用,都会执行函数的,methods它不是响应式的。
watch和computed的区别是:
- 相同点:他们两者都是观察页面数据变化的。
- 不同点:
- computed只有当依赖的数据变化时才会计算, 当数据没有变化时, 它会读取缓存数据。
- watch每次都需要执行函数。watch更适用于数据变化时的异步操作。
运用场景:
- 当需要进行数值计算,并且依赖于其它数据时,应该使用computed,因为可以利用computed的缓存特性,避免每次获取值时,都要重新计算。
- 当需要在数据变化时执行异步或开销较大的操作时,应该使用watch,使用watch允许我们执行异步操作(访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
10. Vue常用的指令有哪些
- v-model:一般用于表单输入,实现表单控件和数据的双向绑定;
- v-html:更新元素的
innerHTML
; - v-show:条件渲染
- v-if:条件渲染
- v-on:click:可以简写为@click,@绑定一个事件。如果事件触发了,就执行事件的回调函数;
- v-for:循环多次渲染元素或模板;
- v-bind:当表达式的值改变时,将其产生的连带影响响应式地作用于DOM。语法:
v-bind:title="msg"
,简写::title="msg"
。
v-if和v-show异同
相同点:
- v-if和v-show两个指令都能够实现元素的显示和隐藏。
- v-if通过创建和删除对应的dom节点来实现显示和隐藏
- v-show通过设置css样式来实现显示和隐藏(对应的dom节点一直存在)
不同点:
- v-if:在条件不满足的情况下(值为false),直接不渲染对应元素。而v-show在条件不满足的情况下,给元素添加
display:none
来隐藏元素,并没有将元素从dom树中移除。当v-if的值由true变为false时,直接将元素从dom树中移除。 - v-show:不管值为true还是false,对应的html元素都会存在,只是通过设置CSS中的display属性值(
display:none
)来控制元素的显示或隐藏。 - v-if是通过控制dom节点的存在与否来控制元素的显隐;v-show是通过设置DOM元素的display属性值来控制元素的显隐,block为显示,none为隐藏;
- v-if是
真正的条件渲染(其后面可以跟v-else等其它条件渲染语句,而v-show没有)
,因为它会确保在切换过程中,条件判断语句块中的事件监听器和子组件适当地被销毁和重建; - v-if是惰性的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时,才会开始渲染条件块。
- v-show则不同,不管初始条件是真是假,元素总是会被渲染,并且只是简单地基于CSS的display属性进行切换。
具体应用场景:
- v-if有更高的切换开销(因为每次切换都是通过创建和删除节点来实现显示和隐藏的),而v-show有更高的初始渲染开销。
- 因此,当需要隐藏和显示切换的比较频繁的时候,推荐使用v-show;需要隐藏和显示切换少的时候(不需要频繁切换显示和隐藏),推荐使用v-if。
Vue中如何自定义指令?
① 定义全局的自定义变量
// main.js
Vue.directive('color', {
inserted(el) {
// 注意:这里的el获取的是标签元素,说白了就是可以直接操作DOM
console.log(el);
el.style.color = "red";
}
})
<div>自定义指令</div>
<div v-color>自定义指令</div>
2
3
4
5
6
7
8
9
10
11
② 组件内指令-只有当前组件可以使用
// template
<div>自定义指令</div>
<div v-color>自定义指令</div>
// script
directives: {
color: {
inserted(el) {
el.style.color = 'red';
}
}
}
2
3
4
5
6
7
8
9
10
11
12
11. Class与Style如何动态绑定?
Class可以通过对象语法和数组语法进行动态绑定
对象语法:
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
data: {
isActive: true,
hasError: false
}
2
3
4
5
数组语法:
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
data: {
activeClass: 'active',
errorClass: 'text-danger'
}
2
3
4
5
6
Style 也可以通过对象语法和数组语法进行动态绑定
对象语法:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
activeColor: 'red',
fontSize: 30
}
2
3
4
5
6
数组语法:
<div v-bind:style="[styleColor, styleSize]"></div>
data: {
styleColor: {
color: 'red'
},
styleSize:{
fontSize:'23px'
}
}
2
3
4
5
6
7
8
9
10
12. 对Vue单向数据流的理解
所有的prop都使得其父子 prop 之间形成了一个单向下行绑定 :父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。
有两种常见的试图改变一个 prop 的情形:
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值:
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
2
3
4
5
6
这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
2
3
4
5
6
13. 直接给一个数组项赋值,Vue能检测到变化吗?
由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:
当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue 当你修改数组的长度时,例如:vm.items.length = newLength 为了解决第一个问题,Vue 提供了以下操作方法:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
2
3
4
5
6
为了解决第二个问题,Vue 提供了以下操作方法:
// Array.prototype.splice
vm.items.splice(newLength)
2
14. 谈一谈对Vue组件化的理解以及如何实现一个自定义组件?
回答思路: 组件化定义、优点、使用场景和注意事项等方面展开陈述,同时要强调Vue组件化的一些特点。 源码分析1:组件定义
// 组件定义
Vue.component('comp', {
template: '<div>this is a component</div>'
})
2
3
4
15. 说说对SPA单页面的理解,它的优缺点分别是什么?
SPA(single-page application仅在Web页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。 优点:
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
- 基于上面一点,SPA 相对对服务器压力小;
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理; 缺点:
- 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
- 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
- SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
16. Vue解决了什么问题?谈一谈对Vue设计原则的理解?
Vue定义和特点:
- 渐进式javascrip框架
- 易用、灵活和高效
17. Vue与Angular以及React的区别
Vue与AngularJS的区别
- Angular采用TypeScript开发, 而Vue可以使用javascript也可以使用TypeScript
- AngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。
- AngularJS社区完善,Vue的学习成本较小
Vue与React的区别
- vue组件分为全局注册和局部注册,在react中都是通过import相应组件,然后模版中引用;
- props是可以动态变化的,子组件也实时更新,在react中官方建议props要像纯函数那样,输入输出一致对应,而且不太建议通过props来更改视图;
- 子组件一般要显示地调用props选项来声明它期待获得的数据。而在react中不必需,另两者都有props校验机制;
- 每个Vue实例都实现了事件接口,方便父子组件通信,小型项目中不需要引入状态管理机制,而react必需自己实现;
- 使用插槽分发内容,使得可以混合父组件的内容与子组件自己的模板;
- 多了指令系统,让模版可以实现更丰富的功能,而React只能使用JSX语法;
- Vue增加的语法糖computed和watch,而在React中需要自己写一套逻辑来实现;
- react的思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等;而 vue是把html,css,js组合到一起,用各自的处理方式,vue有单文件组件,可以把html、css、js写到一个文件中,html提供了模板引擎来处理。
- react做的事情很少,很多都交给社区去做,vue很多东西都是内置的,写起来确实方便一些, 比如 redux的combineReducer就对应vuex的modules, 比如reselect就对应vuex的getter和vue组件的computed, vuex的mutation是直接改变的原始数据,而redux的reducer是返回一个全新的state,所以redux结合immutable来优化性能,vue不需要。
- react是整体的思路的就是函数式,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以做到,比如结合redux-form,组件的横向拆分一般是通过高阶组件。而vue是数据可变的,双向绑定,声明式的写法,vue组件的横向拆分很多情况下用mixin。
18. v-model的原理
Vue的双向数据绑定是由数据劫持结合发布者订阅者实现的。 数据劫持是通过Object.defineProperty()来劫持对象数据的setter和getter操作。 在数据变动时作你想做的事
原理:通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化->视图更新 在初始化vue实例时,遍历data这个对象,给每一个键值对利用Object.definedProperty对data的键值对新增get和set方法,利用了事件监听DOM的机制,让视图去改变数据。
在Vue中,主要使用v-model指令在表单input、textarea、select等元素上实现数据的双向绑定。我们知道v-model本质上是语法糖,v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用 value 属性和 input 事件;
- checkbox 和 radio 使用 checked 属性和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件;
以input表单元素为例:
<input v-model='something'>
// 相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
2
3
这个语法糖必须是固定的,也就是说属性必须为value,方法名必须为:input
。
知道了v-model的原理,我们可以在自定义组件上实现v-model。
// Parent
<template>
{{num}}
<Child v-model="num" />
</template>
export default {
data(){
return {
num: 0
}
}
}
2
3
4
5
6
7
8
9
10
11
12
// Child
<template>
<div @click="add">Add</div>
</template>
export default {
props: ['value'],
methods:{
add(){
this.$emit('input', this.value + 1)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
如果在自定义组件中,v-model默认会利用名为value的prop和名为input的事件,如下所示:
// 父组件:
<ModelChild v-model="message"></ModelChild>
// 子组件:
<div>{{value}}</div>
props:{
value: String
},
methods: {
test1(){
this.$emit('input', '小红')
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
19. 前端路由实现原理
前端路由实现起来其实很简单,本质就是监听URL的变化,然后匹配路由规则,显示相应的页面,并且无须刷新。
vue-router的实现原理
mode
- hash
- history
跳转
- this.$router.push()
<router-link to=""></router-link>
占位
<router-view></router-view>
vue-router通过hash与History interface两种方式实现前端路由,更新视图但不重新请求页面是前端路由原理的核心之一。
- hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
- history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。
vue路由的两种模式
hash模式(默认模式)——即地址栏URL中的#符号(此hsah不是密码学里的散列运算)。hash虽然出现URL中,但不会被包含在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面。
比如这个URL:http://www.abc.com/#/hello
,hash的值为#/hello
。它的特点在于:hash虽然出现在URL中,但不会被包括在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面。
history模式:利用了HTML5 History Interface 中新增的pushState() 和replaceState() 方法。这两个方法应用于浏览器的历史记录站,在当前已有的back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的URL,但浏览器不会立即向后端发送请求。
因此,可以说hash模式和history模式都属于浏览器自身的特性,Vue-Router只是利用了这两个特性(通过调用浏览器提供的接口)来实现前端路由。
history与hash路由的区别
hash 模式相比于 history 模式的优点:
- 兼容性更好,可以兼容到IE8
- 无需服务端配合处理非单页的url地址
hash 模式相比于 history 模式的缺点:
- 看起来更丑。
- 会导致锚点功能失效。
- 相同 hash 值不会触发动作将记录加入到历史栈中,而 pushState 则可以。
一般场景下,hash 和 history 都可以,除非你更在意颜值,# 符号夹杂在 URL 里看起来确实有些不太美丽。如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成URL 跳转而无须重新加载页面。
调用 history.pushState() 相比于直接修改 hash,存在以下优势:
- pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
- pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
- pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
- pushState() 可额外设置 title 属性供后续使用。
history模式问题:在用户手动输入 URL 后回车,或者刷新(重启)浏览器的时候。
- hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.abc.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。
- history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.abc.com/book/id。如果后端缺少对 /book/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果URL匹配不到任何静态资源,则应该返回同一个index.html页面(nginx配置一个fallback:index.html),这个页面就是你app依赖的页面。
history问题
history在修改url 后,虽然页面并不会刷新,但在手动刷新,或通过 url 直接进入应用的时候,服务端是无法识别这个url的。因为我们是单页应用,只有一个html文件,服务端在处理其他路径的url的时候,就会出现404的情况。
所以,如果要应用 history 模式,需要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回单页应用的 html 文件。
popstate事件
当活动历史记录条目更改时,将触发popstate事件。如果被激活的历史记录条目是通过对history.pushState()的调用创建的,或者受到对history.replaceState()的调用的影响,popstate事件的state属性包含历史条目的状态对象的副本。
每当处于激活状态的历史记录条目发生变化时,popstate事件就会在对应window对象上触发. 如果当前处于激活状态的历史记录条目是由history.pushState()方法创建,或者由history.replaceState()方法修改过的, 则popstate事件对象的state属性包含了这个历史记录条目的state对象的一个拷贝。
调用history.pushState()或者history.replaceState()不会触发popstate事件。popstate事件只会在浏览器某些行为下触发,比如点击后退、前进按钮(或者在JavaScript中调用history.back()、history.forward()、history.go()方法)。
webpack-dev-server配置historyApiFallback
之前在使用webpack做spa(单页面应用)的时候出现了404错误,原因很简单是因为刷新页面时访问的资源在服务端找不到,因为vue-router设置的路径不是真实存在的路径。
// 无论是啥都匹配自己设置的首页
historyApiFallback: {
rewrites: [{
from: /.*/g,
to: '/page/index.html'
}]
}
2
3
4
5
6
7
20. Vue中$nexTick原理和应用
$nexTick
是在下一次DOM更新循环结束之后执行延迟回调,在修改数据之后使用$nexTick
,则可以在回调中获取到更新后的DOM。
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
2
3
4
5
6
7
8
9
10
11
12
应用场景:需要在视图更新之后,基于新的视图进行操作。
在Vue生命周期的created()钩子函数中进行DOM操作一定要放在Vue.nextTick()
的回调函数中。原因是什么呢?
原因在于:created()钩子函数执行的时候,DOM其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。
21. Vue生命周期的理解
Vue实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为Vue的生命周期。
生命周期中有多个事件钩子,如下:
- beforeCreate阶段(创建前):vue实例的挂载元素el和数据对象data都是undefined,还没有初始化。
- created阶段(创建后):创建完成,属性已经绑定,但还未生成真实dom。vue实例的数据对象data有了,可以访问里面的数据和方法,未挂载到DOM,el还没有。
- beforeMount阶段(载入前):vue实例的el和data都初始化了,但是挂载之前为虚拟的dom节点。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。
- mounted阶段(载入后-组件已挂载):vue实例挂载到真实DOM上,可以获取相关DOM节点。用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。
- beforeUpdate阶段(更新前):响应式数据更新时调用,发生在虚拟DOM打补丁之前,适合在更新之前访问现有的DOM,比如手动移除已添加的事件监听器。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
- updated阶段(更新后):虚拟DOM重新渲染和打补丁之后调用,组成新的DOM已经更新,避免在这个钩子函数中操作数据,防止死循环。该钩子在服务器端渲染期间不被调用。
- beforeDestroy阶段(销毁前):实例销毁前调用,实例还可以用,this能获取到实例,常用于销毁定时器,解绑事件
- destroyed阶段(销毁后):实例销毁后调用,调用后所有事件监听器会被移除,所有的子实例都会被销毁。该钩子在服务器端渲染期间不被调用。
哪个阶段发起ajax请求
一般在 created 里面就可以,如果涉及到需要页面加载完成之后的操作话就用 mounted;
- created 阶段的优势是:请求时间比较早,页面 loading 时间相对较短;
- mounted 阶段的优势是:页面已经渲染完成,如果想请求之后进行 DOM 操作的话,必须在 mounted 阶段发起请求;
可以在钩子函数created、beforeMount、mounted中进行调用,因为在这三个钩子函数中,data已经创建,可以将服务端返回的数据进行赋值。但是,推荐在created钩子函数中调用异步请求,因为在created钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面loading时间;
- ssr不支持beforeMount、mounted钩子函数,所以放在created中有助于一致性;
在哪个阶段才能访问和操作DOM
在钩子函数mounted被调用前,Vue已经将编译好的模板挂载到页面上,所以在mounted中可以访问操作DOM。
$route
和$router的区别?
22. $route是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。 $router是“路由实例”对象包括了路由的跳转方法,钩子函数等。
$router是VueRouter的一个对象,通过Vue.use(VueRouter)和VueRouter构造函数得到一个router的实例对象,这个对象中是一个全局的对象,他包含了所有的路由包含了许多关键的对象和属性。
$route对象表示当前的路由信息,包含了当前 URL 解析得到的信息
**1.$route.path** 字符串,对应当前路由的路径,总是解析为绝对路径,如 "/foo/bar"。
**2.$route.params** 一个 key/value 对象,包含了 动态片段 和 全匹配片段, 如果没有路由参数,就是一个空对象。
**3.$route.query** 一个 key/value 对象,表示 URL 查询参数。例如,对于路径 /foo?user=1,则有 $route.query.user == 1, 如果没有查询参数,则是个空对象。
**4.$route.hash** 当前路由的 hash 值 (不带 #) ,如果没有 hash 值,则为空字符串。锚点
**5.$route.fullPath** 完成解析后的 URL,包含查询参数和 hash 的完整路径。
**6.$route.matched** 数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象。
**7.$route.name 当前路径名字**
**8.$route.meta 路由元信息
2
3
4
5
6
7
8
23. 对keep-aerlive的了解?
keep-alive
是Vue内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
通过设置keep-alive
,可以简单理解为从页面1跳转到页面2后,然后后退到页面1,只会加载缓存中之前已经渲染好的页面1,而不会再次重新加载页面1,即不会再触发页面的created等类似的钩子函数,除非自己重新刷新该页面1。
24. vue中常用的路由钩子函数有哪一些?
全局守卫: router.beforeEach 全局前置守卫 进入路由之前 router.beforeResolve 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用 router.afterEach 全局后置钩子 进入路由之后
路由组件内的守卫: beforeRouteEnter 进入路由前 beforeRouteUpdate (2.2) 路由复用同一个组件时 beforeRouteLeave 离开当前路由时
25. vue中常用的修饰符?
- .stop:阻止点击事件冒泡
- .prevent:阻止事件的默认行为
- .capture:添加事件侦听器时使用事件捕获模式
- .self:只当事件在该元素本身时触发回调(在其子元素上不触发)
- .once:只触发一次事件
26. vuex中的成员?对应的作用?
- state => 基本数据
- getters => 从基本数据派生的数据
- mutations => 提交更改数据的方法,同步!
- actions => 像一个装饰器,包裹mutations,使之可以异步。
- modules => 模块化Vuex
vuex的流程
- 页面通过mapAction异步提交事件到action。
- action通过commit把对应参数同步提交到mutation。
- mutation会修改state中对于的值。
- 最后通过getter把对应值跑出去,在页面的计算属性中通过mapGetter来动态获取state中的值
vuex有哪几种状态和属性
state: 状态中心 mutations: 更改状态 actions: 异步更改状态 getters: 获取状态 modules: 将state分成多个modules,便于管理
- state中保存着共有数据,数据是响应式的
- getter可以对state进行计算操作,主要用来过滤一些数据,可以在多组件之间复用
- mutations定义的方法动态修改state中的数据,通过commit提交方法,方法必须是同步的
- actions将mutations里面处理数据的方法变成异步的,就是异步操作数据,通过store.dispatch来分发actions,把异步的方法写在actions中,通过commit提交mutations,进行修改数据。
- modules:模块化vuex
27. vue给对象新增属性页面没有响应
由于Vue会在初始化实例时对属性执行getter/setter转化,所以属性必须在data对象上存在才能让Vue将它转换为响应式的。Vue提供了$set方法用来触发视图更新。
export default {
data(){
return {
obj: {
name: 'fei'
}
}
},
mounted(){
this.$set(this.obj, 'sex', 'man')
}
}
2
3
4
5
6
7
8
9
10
11
12
13
28. 如何让CSS只在当前组件中起作用(scoped属性作用)?
在Vue文件中的style标签上有一个特殊的属性,scoped。当一个style标签拥有scoped属性时候(即<style lang="scss" scoped>
),它的css样式只能用于当前的Vue组件,可以使组件的样式不相互污染。如果一个项目的所有style标签都加上了scoped属性,相当于实现了样式的模块化。
scoped属性的实现原理是:给每一个dom元素添加了一个独一无二的动态属性,给css选择器额外添加一个对应的属性选择器,来选择组件中的dom。
<template>
<div class="box">dom</div>
</template>
<style lang="scss" scoped>
.box {
background:red;
}
</style>
2
3
4
5
6
7
8
vue将代码转译成如下:
.box[data-v-11c6864c]{
background:red;
}
<template>
<div class="box" data-v-11c6864c>dom</div>
</template>
2
3
4
5
6
29. 实现scoped样式穿透
scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性。即设置了scoped属性的style标签是无法进行全局样式的修改的。
- scss样式穿透:
父元素 /deep/ 子元素
; - stylus样式穿透:
- 方式1:
父元素 /deep/ 子元素
; - 方式2:
父元素 >>> 子元素
;
- 方式1:
- 使用
/deep/
// Parent
<template>
<div class="wrap">
<Child />
</div>
</template>
<style lang="scss" scoped>
.wrap /deep/ .box{
background: red;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
// Child
<template>
<div class="box"></div>
</template>
2
3
4
- 使用两个style标签
// Parent
<template>
<div class="wrap">
<Child />
</div>
</template>
<style lang="scss" scoped>
// 其他样式
</style>
<style lang="scss">
.wrap .box {
background: red;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Child
<template>
<div class="box"></div>
</template>
2
3
4
30. ref的作用
- 获取dom元素this.$refs.box
- 获取子组件中的data:this.$refs.box.msg
- 调用子组件中的方法this.$refs.box.open()
31. vue-loader的作用
vue-loader是解析.vue
结尾文件的加载器,将template/js/style等组成的.vue
文件转换为js模块。
32. v-if和v-for哪个优先级更高?如果两个同时出现,应该如何优化得到更好的性能?
结论:
- v-for优先于v-if(源码中也是这样判断的)
- 如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费性能
- 可以通过在循环外层嵌套template,在这一层进行v-if判断,然后在内部进行v-for循环
源码中找对应的答案:
compiler/codegen/index.js
。
情况1:当v-if和v-for同时出现在一个标签中时
<p v-for="child in childList" v-if="isShow">{{child.name}}</p>
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
{ staticClass: "about" },
[
_c("h1", [_vm._v("This is an about page")]),
_vm._l(_vm.childList, function(child) {
return _vm.isShow ? _c("p", [_vm._v(_vm._s(child.name))]) : _vm._e()
})
],
2
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
上述代码中:_vm._l
就是v-for循环,_vm.isShow
是在循环内部的,因此,当v-if和v-for同时出现在一个标签中时,v-for的优先级是高于v-if的。
情况2:当v-if所在标签包括v-for所在标签时
<template v-if="isShow">
<p v-for="child in childList">{{child.name}}</p>
</template>
2
3
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
{ staticClass: "about" },
[
_c("h1", [_vm._v("This is an about page")]),
_vm.isShow
? _vm._l(_vm.childList, function(child) {
return _c("p", [_vm._v(_vm._s(child.name))])
})
: _vm._e()
],
2
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
从上述代码可以看出,先进行_vm.isShow
判断,然后根据判断结果是否执行_vm._l
。
源码如下:
<template>
<div class="about">
<h1>This is an about page</h1>
<!-- <p v-for="child in childList" v-if="isShow">{{child.name}}</p> -->
<template v-if="isShow">
<p v-for="child in childList">{{child.name}}</p>
</template>
</div>
</template>
<script>
export default {
name: 'about',
data() {
return {
childList: [
{name: 'lisi'},
{name: 'wangwu'}
]
}
},
computed: {
isShow() {
return this.childList && this.childList.length > 0
}
},
mounted() {
console.log(this.$options.render); // 打印渲染函数
}
}
</script>
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
33. 了解哪些Vue性能优化方法?
- 路由懒加载
- keep-alive缓存页面
- 使用v-show复用DOM
- v-for遍历避免同时使用v-if
- 长列表性能优化
- 事件的销毁
- 图片懒加载(使用vue-lazyload库)
- 第三方插件按需引入
- 无状态的组件标记为函数式组件
- 子组件分割
- 变量本地化
- SSR
34. 对Vue3.0的新特性有没有了解
- 更快
- 虚拟DOM重写
- 优化slots的生成
- 静态树提升
- 静态属性提升
- 基于Proxy的响应式系统
- 更小:通过摇树优化核心库体积
- 更容易维护:TypeScript + 模块化
- 更加友好
- 跨平台:编译器核心和运行时核心与平台无关,使得Vue更容易与任何平台(Web、Android、IOS)一起使用
- 更容易使用
- 改进的TypeScript支持,编辑器能提供强有力的类型检查和错误及警告
- 更好的调试支持
- 独立的响应化模块
- Composition API
35. vue-router守卫
导航守卫 router.beforeEach 全局前置守卫
- to: Route: 即将要进入的目标(路由对象)
- from: Route: 当前导航正要离开的路由
- next: Function: 一定要调用该方法来 resolve 这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截) 执行效果依赖 next 方法的调用参数。
- next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
- next(false): 取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)。
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => {
next();
});
router.beforeResolve((to, from, next) => {
next();
});
router.afterEach((to, from) => {
console.log('afterEach 全局后置钩子');
});
2
3
4
5
6
7
8
9
10
11
路由独享的守卫 你可以在路由配置上直接定义 beforeEnter 守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
2
3
4
5
6
7
8
9
10
11
组件内的守卫 你可以在路由组件内直接定义以下路由导航守卫
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用,我们用它来禁止用户离开
// 可以访问组件实例 `this`
// 比如还未保存草稿,或者在用户离开前,
将setInterval销毁,防止离开之后,定时器还在调用。
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
36. vue项目中的性能优化
- 不要在模板里面写过多表达式
- 循环调用子组件时添加key
- 频繁切换的使用v-show,不频繁切换的使用v-if
- 尽量少用float,可以用flex
- 按需加载,可以用require或者import()按需加载需要的组件
- 路由懒加载
37. vue.extend和vue.component
extend 是构造一个组件的语法器。 然后这个组件你可以作用到Vue.component这个全局注册方法里 还可以在任意vue模板里使用组件。 也可以作用到vue实例或者某个组件中的components属性中并在内部使用apple组件。 Vue.component 你可以创建 ,也可以取组件。
38. SPA项目如何优化加载速度
- 减少入口文件体积
- 静态资源本地缓存
- 开启Gzip压缩
- 使用SSR,nuxt.js
React
React和Vue的区别
=> 相同点:
1.数据驱动页面,提供响应式的试图组件
2.都有virtual DOM,组件化的开发,通过props参数进行父子之间组件传递数据,都实现了webComponents规范
3.数据流动单向,都支持服务器的渲染SSR
4.都有支持native的方法,react有React native, vue有weex
=> 不同点:
1.数据绑定:Vue实现了双向的数据绑定,react数据流动是单向的
2.数据渲染:大规模的数据渲染,react更快
3.使用场景:React配合Redux架构适合大规模多人协作复杂项目,Vue适合小快的项目
4.开发风格:react推荐做法jsx + inline style把html和css都写在js了
vue是采用webpack + vue-loader单文件组件格式,html, js, css同一个文件
2
3
4
5
6
7
8
9
10
11
相同点:都支持 ssr,都有 vdom,组件化开发,实现 webComponents 规范,数据驱动等 不同点:vue 是双向数据流(当然为了实现单数据流方便管理组件状态,vuex 便出现了),react 是单向数据流。vue 的 vdom 是追踪每个组件的依赖关系,不会渲染整个组件树,react 每当应该状态被改变时,全部子组件都会 re-render。
setState
setState通过一个队列机制实现state更新,当执行setState时,会将需要更新的state很后放入状态队列
而不会立即更新this.state,队列机制可以高效地批量更新state。如果不通过setState而直接修改this.state的值
那么该state将不会被放入状态队列中。当下次调用setState并对状态队列进行合并时,就会忽略之前修改的state,造成不可预知的错误
同时,也利用了队列机制实现了setState的异步更新,避免了频繁的重复更新state
同步更新state:
setState 函数并不会阻塞等待状态更新完毕,因此 setNetworkActivityIndicatorVisible 有可能先于数据渲染完毕就执行。
第二个参数是一个回调函数,在setState的异步操作结束并且组件已经重新渲染的时候执行
也就是说,我们可以通过这个回调来拿到更新的state的值,实现代码的同步
例子:componentDidMount() {
fetch('https://test.com')
.then((res) => res.json())
.then(
(data) => {
this.setState({ data:data });
StatusBar.setNetworkActivityIndicatorVisible(false);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
diff算法
三个大步骤:
- 虚拟节点类,将真实DOM节点用js 对象的形式进行展示,并提供render方法,将虚拟节点渲染成真实 DOM
- 节点 diff 比较:对虚拟节点进行 js 层面的计算,并将不同的操作都记录到 patch 对象
- re-render:解析 patch 对象,进行 re-render
1.把树形结构按照层级分解,只比较同级元素 2.给列表结构的每个单元添加key属性,方便比较。在实际代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个标记 3.在深度优先遍历的时候,每遍历到一个节点就把该节点和新的树进行对比。如果有差异的话就记录到一个对象里面 Vritual DOM 算法主要实现上面步骤的三个函数:element, diff, patch。然后就可以实际的进行使用 react只会匹配相同的class的component(这里的class指的是组件的名字) 合并操作,条用component的setState方法的时候,React将其标记为dirty.到每一个时间循环借宿,React检查所有标记dirty的component重新绘制 4.选择性子树渲染。可以重写shouldComponentUpdate提高diff的性能
VDOM 的必要性?
在进行diff算法之前,会先将真实DOM节点转换为虚拟DOM。
- 创建真实DOM的代价高:真实的 DOM 节点 node 实现的属性很多,而 vnode 仅仅实现一些必要的属性,相比起来,创建一个 vnode 的成本比较低。
- 触发多次浏览器重绘及回流:使用vnode,相当于加了一个缓冲,让一次数据变动所带来的所有 node 变化,先在 vnode 中进行修改,然后 diff 之后对所有产生差异的节点集中一次对 DOM tree 进行修改,以减少浏览器的重绘及回流。
为什么虚拟DOM会提高性能
虚拟DOM相当于在js和真实dom中间加了一个缓存,利用dom diff算法避免了没有必要的doom操作,从而提高性能 具体实现步骤:
- 用JavaScript对象结构表示DOM树的结构;然后用这个树构建一个真正的DOM树,插到文档中
- 当状态变更的时候,重新构造一棵树的对象树,然后用新的树和旧的树进行对比,记录两棵树差异
- 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了。