现在Vue几乎公司里都用,所以掌握Vue至关重要,这里我总结了几点,希望对大家有用


1、Vue项目中为什么要在列表组件中写key,作用是什么?

我们在业务组件中,会经常使用循环列表,当时用​​v-for​​​命令时,会在后面写上​​:key​​,那么为什么建议写呢?

​key​​的作用是更新组件时判断两个节点是否相同。相同则复用,不相同就删除旧的创建新的。正是因为带唯一​​key​​时每次更新都不能找到可复用的节点,不但要销毁和创建节点,在​​DOM​​中还要添加移除节点,对性能的影响更大。所以才说,当不带​​key​​时,性能可能会更好。

因为不带​​key​​时,节点会复用(复用是因为​​Vue​​使用了​​Diff​​算法),省去了销毁或创建节点的开销,同时只需要修改​​DOM​​文本内容而不是移除或添加节点。既然如此,为什么我们还要建议带​​key​​呢?因为这种不带​​key​​的模式只适合渲染简单的无状态的组件。对于大多数场景来说,列表都得必须有自己的状态。避免组件复用引起的错误。

带上​​key​​虽然会增加开销,但是对于用户来说基本感受不到差距,​为了保证组件状态正确,避免组件复用​,这就是为什么建议使用key。

2、Vue的双向绑定,Model如何改变View,View又是如何改变Model的?

我们先看一幅图,下面一幅图就是Vue双向绑定的原理图。

关于Vue在面试中常常被提到的几点(持续更新……)_数据

第一步,使数据对象变得“可观测”

我们要知道数据在什么时候被读或写了。

let person = {
'name': 'maomin',
'age': 23
}
let val = 'maomin';
Object.defineProperty(person, 'name', {
get() {
console.log('name属性被读取了')
return val
},
set(newVal) {
console.log('name属性被修改了')
val = newVal
}
})
// person.name
// name属性被读取了
// "maomin"
// person.name='xqm'
// name属性被修改了
// "xqm"

通过​​Object.defineProperty()​​​方法给​​person​​​定义了一个​​name​​​属性,并把这个属性的读和写分别使用​​get()​​​和​​set()​​​进行拦截,每当该属性进行读或写操作的时候就会触发​​get()​​​和​​set()​​。这样数据对象已经是“可观测”的了。

核心是利用​​es5​​​的​​Object.defineProperty​​,这也是Vue.js为什么不能兼容IE8及以下浏览器的原因。

​Object.defineProperty​​方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

Object.defineProperty(
obj, // 定义属性的对象
prop, // 要定义或修改的属性的名称
descriptor // 将要定义或修改属性的描述符【核心】
)

写一个简单的双向绑定:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<input type="text" id="input"/>
<div id="text"></div>
</body>
<script>
let input = document.getElementById('input');
let text = document.getElementById('text');
let data = {value:''};
Object.defineProperty(data,'value',{
set:function(val){
text.innerHTML = val;
input.value = val;
},
get:function(){
return input.value;
}
});
input.onkeyup = function(e){
data.value = e.target.value;
}
</script>
</html>
第二步,使数据对象的所有属性变得“可观测”

上面,我们只能观测​​person.name​​的变化,那么接下来我们要让所有的属性都变得可检测。

let person = observable({
'name': 'maomin',
'age': 23
})
/**
* 把一个对象的每一项都转化成可观测对象
* @param { Object } obj 对象
*/
function observable(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
let keys = Object.keys(obj); //返回一个表示给定对象的所有可枚举属性的字符串数组
keys.forEach((key) => {
defineReactive(obj, key, obj[key])
})
return obj;
}
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`${key}属性被读取了`);
return val;
},
set(newVal) {
console.log(`${key}属性被修改了`);
val = newVal;
}
})
}
// person.age
// age属性被读取了
// 23
// person.age=24
// age属性被修改了
// 24

我们通过​​Object.keys()​​将一个对象返回一个表示给定对象的所有可枚举属性的字符串数组,然后遍历它,使得所有对象可以被观测到。

第三步,依赖收集,制作一个订阅器

我们就可以在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,我们需要先将所有依赖收集起来,一旦数据发生变化,就统一通知更新。

创建一个依赖收集容器,也就是消息​​订阅器Dep​​,用来容纳所有的“订阅者”。​​订阅器Dep​​主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数。

设计了一个订阅器Dep类:

class Dep {
constructor(){
this.subs = []
},
//增加订阅者
addSub(sub){
this.subs.push(sub);
},
//判断是否增加订阅者
depend () {
if (Dep.target) {
this.addSub(Dep.target)
}
},
//通知订阅者更新
notify(){
this.subs.forEach((sub) =>{
sub.update()
})
}
}
Dep.target = null;

创建完订阅器,然后还要修改一下​​defineReactive​

function defineReactive (obj,key,val) {
let dep = new Dep();
Object.defineProperty(obj, key, {
get(){
dep.depend(); //判断是否增加订阅者
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
val = newVal;
console.log(`${key}属性被修改了`);
dep.notify() //数据变化通知所有订阅者
}
})
}

我们将​​订阅器Dep​​​添加订阅者的操作设计在​​get()​​里面,这是为了让订阅者初始化时进行触发,因此需要判断是否要添加订阅者。

第四步,订阅者Watcher

设计一个订阅者Watcher类:

class Watcher {
// 初始化
constructor(vm,exp,cb){
this.vm = vm; // 一个Vue的实例对象
this.exp = exp; // 是node节点的v-model或v-on:click等指令的属性值。如v-model="name",exp就是name;
this.cb = cb; // 是Watcher绑定的更新函数;
this.value = this.get(); // 将自己添加到订阅器的操作
},
// 更新
update(){
let value = this.vm.data[this.exp];
let oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
},
get(){
Dep.target = this; // 缓存自己
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
}

​订阅者Watcher​​​在初始化的时候需要将自己添加进​​订阅器Dep​​​中,如何添加呢?我们已经知道​​监听器Observer​​​是在​​get()​​​执行了添加​​订阅者Wather​​​的操作的,所以我们只要在​​订阅者Watcher​​​初始化的时候触发对应的​​get()​​​去执行添加订阅者操作即可。那要如何触发监听器​​get()​​,再简单不过了,只要获取对应的属性值就可以触发了。

订阅者Watcher运行时,首先进入初始化,就会执行它的 ​​this.get()​​ 方法,

执行​​Dep.target = this;​​,实际上就是把​​Dep.target​​ 赋值为当前的渲染 ​​Watcher​​ ,接着又执行了​​let value = this.vm.data[this.exp];​​。在这个过程中会对数据对象上的数据访问,其实就是为了触发数据对象的​​get()​​。

每个对象值的​​get()​​​都持有一个​​dep​​​,在触发 ​​get()​​​的时候会调用 ​​dep.depend()​​​方法,也就会执行​​this.addSub(Dep.target)​​​,即把当前的 ​​watcher​​​订阅到这个数据持有的​​dep.subs​​​中,这个目的是为后续数据变化时候能通知到哪些 ​​subs​​​ 做准备。完成依赖收集后,还需要把 ​​Dep.target​​​恢复成上一个状态​​Dep.target = null;​​​ 因为当前vm的数据依赖收集已经完成,那么对应的渲染​​Dep.target​​ 也需要改变。

而​​update()​​​是用来当数据发生变化时调用​​Watcher​​​自身的更新函数进行更新的操作。先通过​​let value = this.vm.data[this.exp];​​​获取到最新的数据,然后将其与之前​​get()​​​获得的旧数据进行比较,如果不一样,则调用更新函数​​cb​​进行更新。


总结:

实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个​​监听器Observer​​​,用来监听所有属性。如果属性发上变化了,就需要告诉​​订阅者Watcher​​​看是否需要更新。因为订阅者是有很多个,所以我们需要有一个​​消息订阅器Dep​​来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。

关于Vue在面试中常常被提到的几点(持续更新……)_监听器_02

实现一个Vue数据绑定:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1 id="name"></h1>
<input type="text">
<input type="button" value="改变data内容" onclick="changeInput()">
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
function myVue (data, el, exp) {
this.data = data;
observable(data); //将数据变的可观测
el.innerHTML = this.data[exp]; // 初始化模板数据的值
new Watcher(this, exp, function (value) {
el.innerHTML = value;
});
return this;
}
var ele = document.querySelector('#name');
var input = document.querySelector('input');

var myVue = new myVue({
name: 'hello world'
}, ele, 'name');

//改变输入框内容
input.oninput = function (e) {
myVue.data.name = e.target.value
}
//改变data内容
function changeInput(){
myVue.data.name = "改变后的data"
}
</script>
</body>
</html>

observer.js​(为了方便,这里将订阅器与监听器写在一块)

// 监听器
// 把一个对象的每一项都转化成可观测对象
// @param { Object } obj 对象

function observable (obj) {
if (!obj || typeof obj !== 'object') {
return;
}
let keys = Object.keys(obj);
keys.forEach((key) =>{
defineReactive(obj,key,obj[key])
})
return obj;
}
// 使一个对象转化成可观测对象
// @param { Object } obj 对象
// @param { String } key 对象的key
// @param { Any } val 对象的某个key的值

function defineReactive (obj,key,val) {
let dep = new Dep();
Object.defineProperty(obj, key, {
get(){
dep.depend();
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
val = newVal;
console.log(`${key}属性被修改了`);
dep.notify() //数据变化通知所有订阅者
}
})
}

// 订阅器Dep
class Dep {

constructor(){
this.subs = []
}
//增加订阅者
addSub(sub){
this.subs.push(sub);
}
//判断是否增加订阅者
depend () {
if (Dep.target) {
this.addSub(Dep.target)
}
}

//通知订阅者更新
notify(){
this.subs.forEach((sub) =>{
sub.update()
})
}

}
Dep.target = null;

watcher.js

class Watcher {
constructor(vm,exp,cb){
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
}
get(){
Dep.target = this; // 缓存自己
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
update(){
let value = this.vm.data[this.exp];
let oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
}
}

3、Vue的computed与watch的区别在哪里?

我们先看一个例子:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<p>{{a}}</p>
<p>{{b}}</p>
<p>{{c}}</p>
<button @click='change'>change</button>
</div>
</body>
<script src="vue.js"></script>
<script>
var vm =new Vue({
el:'#app',
data:{
a:1,
b:2
},
methods:{
change(){
this.a = 5;
}
},
watch:{
a(){
console.log('watch');
}
},
computed:{
c(){
console.log('computed');
return this.a + this.b;
}
}
})
</script>
</html>

一开始的时候,关于Vue在面试中常常被提到的几点(持续更新……)_数据_03

点击按钮时,

关于Vue在面试中常常被提到的几点(持续更新……)_监听器_04

我们可以看到一开始的时候,打印出了​​computed​​,当点击按钮时,​​data​​内的属性值​​a​​发生变化,打印出​​watch​​,接着我们不停点击按钮,并没有打印。(?查看总结4)

我们来总结一下,


  1. 最本质的区别,​​computed​​​为计算属性,​​watch​​为监听属性。
  2. ​watch​​​就是单纯的监听某个数据的变化,支持深度监听。​​computed​​是计算属性,是依赖于某个或者某些属性值,当依赖值发生变化时,也会发生变化。
  3. 计算属性不在​​data​​​中,计算属性依赖值在​​data​​​中。​​watch​​​监听的数据在​​data​​​中。(不一定在只是​​data​​​,也可能是​​props​​)
  4. ​watch​​​用于观察和监听页面上的vue实例,当你需要在数据变化响应时,执行异步操作,或高性能消耗的操作,那么​​watch​​​为最佳选择。​​computed​​可以关联多个实时计算的对象,当这些对象中的其中一个改变时都会触发这个属性,具有缓存能力,所以只有当数据再次改变时才会重新渲染,否则就会直接拿取缓存中的数据。
  5. ​computed​​​是在​​Dep.update()​​​执行之后,数据更新之前,对数据重新改造。​​watch​​​是在​​set​​刚开始发生的时候添加的回调,可以监听数据的变化。

4、为什么在Vue3.0采用了Proxy,抛弃了Object.defineProperty?

​Object.defineProperty​​无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。为了解决这个问题,经过Vue内部处理后可以使用以下几种方法来监听数组。


  • ​push()​
  • ​pop()​
  • ​shift()​
  • ​unshift()​
  • ​splice()​
  • ​sort()​
  • ​reverse()​

由于只针对以上八种方法进行了hack处理,所以其他数组的属性方法也是检测不到的,还是具有一定的局限性。

这里我们举个例子,可以看得更加明白:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<ul>
<li v-for='item in watchArr'>{{item.name}}</li>
</ul>
<button @click='change'>change</button>
</div>
</body>
<script src="vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
watchArr: [{
name: '1',
},{
name: '2',
}],
},
methods: {
change() {
this.watchArr =[{
name: '3',
}];
this.watchArr.splice(0, 1);
this.watchArr[0].name = 'xiaoyue'; // 无法监听
this.watchArr.length = 5; // 无法监听
}
},
watch: {
watchArr(newVal) {
console.log('监听了');
},
}
})
</script>
</html>

想必看到上面的例子我们会更加明白​​Object.defineProperty​​​的局限性。接下来,我们接着说​​Object.defineProperty​​只能劫持对象的属性,因此,我们需要对每个对象的每个属性进行遍历。Vue2.0里,是通过​递归+遍历data对象​来实现对数据的监控的,如果属性值也是对象的话,那么需要深度遍历。显然如果能够劫持一个完整的对象才是更好的选择。

那么Proxy有以下两个优点:


  1. 可以劫持整个对象,并返回一个新对象
  2. 有13种劫持操作

摒弃 ​​Object.defineProperty​​​,基于​​Proxy​​的观察者机制探索

5、为什么Vuex的mutation不能做异步操作?

因为更改state的函数必须是纯函数,纯函数既是统一输入就会统一输出,没有任何副作用;如果是异步则会引起额外的副作用,导致更改后的state不可预测。

6、Vue中的computed是如何实现的?

实质是一个惰性的​​wather​​​,在取值操作时根据自身标记dirty属性返回上一次计算结果或重新计算值在创建时就进行一次取值操作,收集依赖变动的对象或属性(将自身压入​​dep​​​中),在依赖的对象或属性变动时,仅将自身标记​​dirty​​​致为​​true​​。

7、Vue的父组件和子组件的生命周期钩子函数执行顺序是什么?


  1. 加载渲染过程
    (父)beforeCreate ​​ (父)created ​​ (父)beforeMount ​​ (子)beforeCreate ​​ (子)created ​​ (子)beforeMount ​​ (子)mounted ​​ (父)mounted
  2. 子组件更新过程
    (父)beforeUpdate ​​ (子)beforeUpdate ​​ (子)Updated ​​ (父)Updated
  3. 父组件更新过程
    (父)beforeUpdate ​​ (父)Updated
  4. 销魂过程
    (父)beforeDestroy ​​ (子)beforeDestory ​​ (子)destroyed ​​ (父)destroyed