关于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双向绑定的原理图。

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

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

   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数据绑定:

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>



一开始的时候,

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

我们来总结一下,

    最本质的区别,computed为计算属性,watch为监听属性。
    watch就是单纯的监听某个数据的变化,支持深度监听。computed是计算属性,是依赖于某个或者某些属性值,当依赖值发生变化时,也会发生变化。
    计算属性不在data中,计算属性依赖值在data中。watch监听的数据在data中。(不一定在只是data,也可能是props)
    watch用于观察和监听页面上的vue实例,当你需要在数据变化响应时,执行异步操作,或高性能消耗的操作,那么watch为最佳选择。computed可以关联多个实时计算的对象,当这些对象中的其中一个改变时都会触发这个属性,具有缓存能力,所以只有当数据再次改变时才会重新渲染,否则就会直接拿取缓存中的数据。
    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有以下两个优点:

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

摒弃 Object.defineProperty,基于Proxy的观察者机制探索
5、为什么Vuex的mutation不能做异步操作?

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

实质是一个惰性的wather,在取值操作时根据自身标记dirty属性返回上一次计算结果或重新计算值在创建时就进行一次取值操作,收集依赖变动的对象或属性(将自身压入dep中),在依赖的对象或属性变动时,仅将自身标记dirty致为true。
7、Vue的父组件和子组件的生命周期钩子函数执行顺序是什么?

    加载渲染过程
    (父)beforeCreate → (父)created → (父)beforeMount → (子)beforeCreate → (子)created → (子)beforeMount → (子)mounted → (父)mounted
    子组件更新过程
    (父)beforeUpdate → (子)beforeUpdate → (子)Updated → (父)Updated
    父组件更新过程
    (父)beforeUpdate → (父)Updated
    销魂过程
    (父)beforeDestroy → (子)beforeDestory → (子)destroyed → (父)destroyed

作者:Vam的金豆之路

主要领域:前端开发

我的微信:maomin9761

微信公众号:前端历劫之路