1、如何理解 MVVM
原理
M
:Model
,数据层对数据的处理;V
:View
,视图层,是 HMTL 显示页面;VM
:ViewModel
:业务逻辑层(一切JS
可视为业务逻辑,比如表单按钮提交,自定义事件的注册和处理逻辑都在ViewModel
里面负责监听两边的数据)。
2、响应式数据的原理是什么
响应式的基本机制:
- 通过
Object.defineProperty()
替换配置对象属性的set
、get
方法。 watcher
在执行getter
函数是触发get
方法,从而建立依赖关系。- 写入数据时触发
set
方法,从而借助dep
发布通知,进而getter
进行更新。
3、Vue
中是如何检测数组和对象变化
由于 JavaScript
的限制,Vue
不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
对于对象
- 使用
Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式property
。
Vue.set(someObject, "b", 2);
- 使用
Vue.$set(this.someObject, propeytyName, value)
实例方法,这也是全局Vue.set()
的别名。
Vue.$set(someObject, "b", 2);
- 为已有对象赋值多个新的
property
:
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 });
对于数组
Vue
不能检测以下数组的变动:
- 当你利用索引值直接设置一个数组项时:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时:
vm.item.length = newLength
解决第一类问题:
Vue.set(vm.items, indexOfItems, newValue);
vm.items.splice(indexOfItems, 1, newValue);
解决第二类问题:
vm.items.splice(newLength);
4、为何Vue
采用异步渲染
如果不采用异步渲染,那么每次数据更新都会对当前组件进行重写渲染,所以为了性能考虑,会在本轮数据更新后,再去异步更新视图。
5、nextTick
实现原理
源码解析nextTick
方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用 nextTick
会将方法存入队列中,通过这个异步方法清空当前队列。所以这个 nextTick
方法是异步方法。
6、Vue
组件的生命周期
7、Ajax
请求放在那个生命周期中
在 mounted
生命周期中请求 Ajax
,此时 html
已经渲染出来了,可以直接操作 dom
节点。
8、何时需要使用beforeDestroy
实例销毁之前调用。在这一步,实例仍然完全可用。该钩子在啊服务器渲染期间不被调用。
9、Vue
父子组件生命周期调用顺序
- 加载渲染过程
父beforeCreate -> 父create -> 父beforeMounted -> 子beforeCreate -> 子create -> 子mounted -> 父mounted
- 子组件更新过程
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
- 父组件更新过程
父beforeUpdate -> 父updated
销毁过程
父beforeDestory -> 子beforeDestory -> 子destoryed -> 父destoryed
10、Vue
中computed
中的特点
特点
- 监控自己定义的变量,不用再 `data` 里面声明,函数名就是变量名。
- 适合多个变量会对象处理后返回一个值。若这个多个变量只要有一个发生变化,结果都回变化。
- 计算的结果具有缓存、依赖响应式属性变化,响应式属性没有变化,直接从缓存中读取结果。
- 在函数内部调用的时候不用加 `()`。
- 必须用 `return` 返回。
- 不要在 `computed` 中对 `data` 中的数据进行赋值操作,这回形成一个死循环。
应用场景
- 一个需要的结果受多个数据影响的时候,比如购物车结算的金额。
- 操作某个属性,执行一些复杂的逻辑,并在多处使用这个结果。
- 内部函数中多处要使用到这个结果的。
11、watch
的用法及deep: true
是如何实现的
watch
是一个侦听的动作,用来观察和响应 Vue
实例上的数据变动。
handler
方法和 immediate
属性
<div id="demo">{{ fullName }}</div>
var vm = new Vue({
el: "#demo",
data: {
firstName: "Foo",
lastName: "Bar",
fullName: "Foo Bar",
},
watch: {
firstName: (val) => {
console.log("第一次没有执行");
this.fullName = val + this.lastName;
},
},
});
初始化的时候,watch
是不会执行的。只要当 firstName
的值改变的时候才会执行监听计算。如果想再第一次被绑定的时候就执行,则需要这样修改:
var vm = new Vue({
el: "#demo",
data: {
firstName: "Foo",
lastName: "Bar",
fullName: "Foo Bar",
},
watch: {
firstName: (val) => {
console.log("第一次执行了");
this.fullName = val + this.lastName;
},
// 代表在 watch 里声明了 firstName 这个方法后,先去立即执行 handler 方法
immediate: true,
},
});
deep 属性
deep
属性的意思是深度监听,会在对象一层层往下遍历,在每一层上都加上监听器。默认为 false
。
<div id="app">
<div>obj.a: {{ obj.a }}</div>
<input type="text" v-model="obj.a" />
</div>
var vm = new Vue({
el: "#app",
data: {
obj: {
a: 1,
},
},
watch: {
obj: {
handler(val) {
console.log("obj,a changed");
},
immediate: true,
},
},
});
当我们在 input
输入框中输入数据改变 obj.a
的值时,我们发现在控制台没有打印出 obj.a changed
。受现代 Javascript
的限制(以及废弃 Object.observe
), Vue
不能检测到对象属性的添加或删除。 由于 Vue
会在初始化实例时对属性执行 getter/setter
转化过程,所以属性必须在 data
对象上存在才能让 Vue 转换它,才能让它是响应式的。
默认情况下, 在 handler
方法中 只监听 obj
这个属性它的引用的变化,我们只有给 obj
赋值的时候它才会监听到,比如我们在 mounted 事件钩子函数中对 obj
进行重新赋值:
mounted() {
this.obj = {
a: '123
}
}
这样 handler
就会执行了,且打印出了 obj.a changed
。
但是如果需要监听 obj
里的属性的值呢?这时候, deep
属性就派上用场了。我们只需要加上 deep: true
,就能深度监听 obj
里属性的值。
watch: {
obj: {
handler(val) {
console.log("obj.a changded");
},
immediate: true,
deep: true
}
}
使用 deep
属性会给每一层都加上监听器,性能开销可能就会非常大了。可以使用字符串的形式来优化:
watch: {
'obj.a': {
handler(val) {
console.log("obj.a changde");
},
immediate: true
// deep: true
}
}
直到遇到 obj.a
属性,才会给该属性设置监听 d 函数,提高性能。
12、Vue
中事件绑定的原理
13、Vue
中v-html
回导致哪些问题
v-html
更新的元素是innerHTML
,内容按普通HTML
插入,不会作为Vue
模版进行编译。在网站上动态渲染HTML
是非常危险的,因为容易导致XSS
攻击。只在可信的内容上使用v-html
,不用在用户提交的的内容上。
- 在单文件组件里,
scoped
的样式不会应用在v-html
内部,因为那部分HTML
没有被Vue
的模板编译器处理。如果你希望针对v-html
的内容设置带作用域的CSS
,你可以替换为CSS Modules
或用一个额外的全局。
第二个问题的解决方案:
第一种解决方案,照样使用 scoped
,但是我们可以使用深度选择器 (>>>
),示例如下:
<style>
.a >>> .b {
/*
...
*/
}
</style>
以上代码最终会被编译为:
.a[data-v-f3f3eg9] .b { /* ... */ }
如果你的 vue
项目使用 less
或 sass
的时候,>>>
可能会失效,我们可以用 /deep/
来代替,代码如下:
.a {
/deep/ .b {
/*
...
*/
}
}
第二种解决方案,单文件组件 style
标签可以使用多次,可以一个 style
标签带 scoped
属性针对当前组件,另外一个 style
标签针对全局生效,但是内部我们采用特殊的命名规则即可,例如 BEM
规则。
14、Vue
中v-if
和v-show
的区别
`v-if` 是真正的条件渲染,因为它会确保在切换的过程中,条件块中的事件监听器和子组件适当的被销毁和重建。
`v-if` 也是惰性的:如果在初始渲染条件为假,则直到第一次变为真时,才会开始渲染条件块。
`v-show` 不管初始条件是什么,元素总是回被渲染,并且只是简单基于 `CSS` 进行切换。
如果需要非常频繁的切换,用`v-show` 较好;如果在运行时条件不太可能改变,则使用`v-if`较好。
15、为什么v-for
和v-if
不能连用
v-for
和 v-if
不应该一起使用,必要情况下应该替换成 computed
属性。
原因:v-for
比 v-if
优先,如果每一次都要遍历整个数组,将会影响速度。尤其是当之需要渲染一小部分的时候。
<ul>
<li v-for="user in users" v-if="user.isActive" :key="user.id">
{{ user.name }}
</li>
</ul>
如上情况,即使 100 个user
中只需要使用一个数据,也会循环整个数组。
正确的做法:
computed: {
activeUsers: () => {
return this.users.filter((user) => {
return user.isActive;
});
};
}
<ul>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</ul>
16、v-model
中的实现原理及如何自定义v-model
17、组件中的data
为什么是一个函数
当一个组件被定义,data
必须声明返回为一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data
是一个纯粹的对象,则所引用的实例将共享引用同一个数据对象。通过提供 data
函数,每次创建一个新实例后,我们能够调用 data
函数,从而返回初始数据的一个全新副本数据对象。
我们假设 data
是一个对象,因为组件是可以被复用的,注册了一个组件本质上就是创建了一个组件构造器的引用,而真正当我们使用组件的时候才会去将组件实例化。
const Component = function () {};
Component.prototype.data = {
a: 1,
b: 2,
};
// 使用组件
const componentA = new Component();
const componentB = new Component();
componentA.data.b = 3;
conponentB.data.b; // 3
当修改一个属性的时候, data
也会发生改变,这明显不是我们想要的结果。
const component = function () {};
Component.prototype.data = function () {
return {
a: 1,
b: 2,
};
};
const componentA = new Component();
const componentB = new Component();
componentA.data.b = 3;
componentB.data.b; // 2
当 data
是一个函数的时候,每一个实例的 data
属性都是独立的,不会相互影响。 JavaScript
本身的面向对象编程也是基于原型链和构造函数,应该会注意到原型链上添加一般都是一个函数方法而不是一个对象了。
18、Vue
组件如何让通信(常见)
方法一、 props
/ $emit
1、父组件向子组件传值
我们提供一个例子,来说明父组件如何向子组件传值:在子组件 User.vue
中如何获取父组件 App.vue
中的数据:users: ["Henry", "Bucky", "Emily"]
// App.vue 父组件
<template>
<div id="app">
<users :users="users"></users> // 前者自定义名称便于子组件调用,后者要传递数据名
</div>
</template>
<script>
import Users from "./components/Users";
export default {
name: "App",
data() {
return {
users: ["Henry", "Bucky", "Emily"]
}
},
components: {
"users": Users
}
}
</script>
// 子组件 Users.vue
<template>
<div class="users">
<ul>
<li v-for="user in users" >{{ user }}</li> // 遍历传递过来的值,然后呈现到页面
</ul>
</div>
</template>
<script>
export default {
name: "Users",
props: {
users: {
type: Array,
required: true,
dafault: () => []
}
}
}
</script>
父组件通过 props
向下传递数据给子组件。注:组件中的数据共有三种数据形式: data
、 props
、 computed
。
2、子组件向父组件传值
// 子组件
<template>
<header>
<button @click="changeTitle" >{{title}}</button>
</header>
</template>
<script>
export default {
nanne: "app-header",
data() {
return {
title: "Vue.js Demo"
}
},
methods: {
changeTitle() {
this.$emit("titleChanged", "子向父组件传值"); // 自定义事件 传递值:子向父组件传值
}
}
}
</script>
// 父组件
<template>
<div id="app">
<app-header @titleChanged="updateTitle" ></app-header> // 与子组件 titleChanged 自定义事件保持一致
// updateTitle($event) 接受传递过来的文字
<h2>{{ title }}</h2>
</div>
</template>
<script>
import Header from './components/header';
export default {
name: 'App',
data() {
return {
title: '传递的是一个值'
};
},
components: {
"app-header": Header
},
methods: {
updateTitle(e) {
this.title = e;
}
}
}
</script>
总之,子组件通过 event
给父组件发送消息,实际上就是子组件把自己的数据发送到父组件。
方法二、 $emit
/ $on
通过一个空的 Vue
实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量的实现了任何组件间的通信。包括父子、兄弟、跨级。当我们的项目比较大时,可以选择更好的状态管理解决方案 vuex
。
1、具体实现方式
const GlobalBusEvent = new Vue();
GlobalBusEvent.$emit(事件名, 数据);
GlobalBusEvent.$on(事件名, (data) => {});
2、具体例子
假设兄弟组件有三个,分别是 A
, B
, C
组件, C
组件如何获取 A
或者 B
组件的数据。
<div id="itany">
<my-b><my-a>
<my-b><my-b>
<my-c><my-c>
</div>
<template id ="a">
<div>
<h3>A组件: {{ name }}</h3>
<button @click="send" >A组件 将数据发送给C组件</button>
</div>
</template>
<template id ="b">
<div>
<h3>A组件: {{ age }}</h3>
<button @click="send" >B组件 将数据发送给C组件</button>
</div>
</template>
<template id="c">
<div>
<h3>C组件:{{ name }}, {{ age }}</h3>
</div>
</template>
<script>
const Event = new Vue(); // 定义一个空的 Vue 实例
const A = {
template: "#a",
data() {
return {
name: "tom"
};
},
methods: {
send() {
Event.$emit("data-a", this.name);
}
}
};
const B = {
template: "#b",
data() {
return {
age: 20
};
},
methods: {
send() {
Event.$emit("data-b", this.ge);
}
}
};
const C = {
template: "#c",
data() {
return {
name: "",
age: ""
};
},
mounted() { // 在模版编译完成后执行
Event.$on("data-a", name => {
this.name = name; // 箭头函数内部不会产生l新的 this,这边如果不用 =>,this 指代 Event
});
Event.$on("data-b", age => {
this.age = age;
});
}
};
const vm = new Vue({
el: "#itany",
components: {
"my-a": A,
"my-b": B,
"my-c": C
}ßß
});
</script>
$on
监听了自定义事件 data-a
、data-b
,因为有时不确定何时会触发事件,一般会在 mounted
或 created
钩子中来监听。
方法三、vuex
1.简要介绍 Vuex
原理
Vuex
实现了一个单项数据流,在全局拥有一个 State
存放数据,当组件要更改 State
中当数据时,必须通过 Mutation
进行, Mutation
同时提供了订阅者模式供外部插件调用获取 State
数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走 Action
,但 Action
也是无法直接修改 State
的,还是需要提供 Mutation
来修改 State
的数据。最后,根据 State
的变化,渲染到视图上。
2. 简要介绍各模块在流程中的功能
Vue Components
:Vue
组件。HTML
页面上,负责收集用户操作等交互行为,执行dispatch
方法触发对应action
进行回应。dispatch
:操作行为触发方法,是唯一能执行action
的行为。actions
:操作行为处理模块,由组件中的$store.dispatch('action名称', detail)
来触发。然后由commit()
来触发mutation
的调用,间接更新state
。负责处理Vue Components
接收到到所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API
请求的操作就在这个模块中进行,包括触发其他action
以及提交mutation
的操作。该模块提供了Promise
的封装,以支持action
的链式触发。commit
:状态改变提交操作方法,对mutation
进行提交,是唯一能执行mutation
的方法。mutations
:状态改变操作方法,由actions
中的commit('mutation 名称')
来触发。是Vuex
修改state
的唯一推荐方法。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook
暴露出来,以进行state
的监控等。state
:页面状态管理容器对象。集中存储Vue components
中data
对象的零散数据,全局唯一,以进行统一的状态管理。 页面显示所需的数据从该对象中进行读取,利用Vue
的细粒度数据响应机制来进行高效的状态更新。getters
:state
对象的读取方法。Vue Components
通过该方法读取全局state
对象。
3. Vuex
与 localStorage
vuex
是 vue
的状态管理工具,存储的数据是响应式的。但是并不会保存起来,刷新之后就回到了初始状态,具体做法应该在 vuex
里数据改变的时候把数据拷贝一份保存到 localStorage
里面,刷新之后,如果 localStorage
里有保存的数据,取出来再替换 store
里的 state
。
let defaultCity = "上海";
try {
if (!defaultCity) {
defaultCity = JSON.parse(window.localStorage.getItem("defaultCity"));
}
} catch (e) {
export default new Vuex.Store({
state: {
city: defaultCity,
},
mutations: {
changeCity(state, city) {
state.city = city;
try {
window.localStorage.setItem(
"defaultCity",
JSON.stringify(state.city)
);
} catch (e) {}
},
},
});
}
这里需要注意的是:由于 vuex
里,我们保存的状态都是数组,而 localStorage
只支持字符串,所以需要用 JSON
转换:
JSON.stringify(state.subscribeList); // array -> string
JSON.parse(window.localStorage.getItem("subscribeList"));
19、什么是作用域插槽
20、用vnode
来描述一个数据结构
21、diff
算法的时间复杂度
22、简述Vue
中diff
算法原理
23、v-for
中为什么要用key
key
的特殊 attribute
主要用在 Vue
的虚拟DOM
算法,在新旧 nodes
对比时辨识 VNodes
。如果不使用 key
,Vue
会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key
时,它会基于 key
的变化重新排列元素顺序,并且会移除 key
不存在的元素。
有相同父元素的子元素必须有独特的 key
。重复的 key
会造成渲染错误。