關(guān)于Vue在面試中常常被提到的幾點(diǎn)(持續(xù)更新……)

1、Vue項(xiàng)目中為什么要在列表組件中寫key,作用是什么?

我們?cè)跇I(yè)務(wù)組件中,會(huì)經(jīng)常使用循環(huán)列表,當(dāng)時(shí)用v-for命令時(shí),會(huì)在后面寫上:key,那么為什么建議寫呢?

key的作用是更新組件時(shí)判斷兩個(gè)節(jié)點(diǎn)是否相同。相同則復(fù)用,不相同就刪除舊的創(chuàng)建新的。正是因?yàn)閹ㄒ籯ey時(shí)每次更新都不能找到可復(fù)用的節(jié)點(diǎn),不但要銷毀和創(chuàng)建節(jié)點(diǎn),在DOM中還要添加移除節(jié)點(diǎn),對(duì)性能的影響更大。所以才說,當(dāng)不帶key時(shí),性能可能會(huì)更好。
因?yàn)椴粠ey時(shí),節(jié)點(diǎn)會(huì)復(fù)用(復(fù)用是因?yàn)閂ue使用了Diff算法),省去了銷毀或創(chuàng)建節(jié)點(diǎn)的開銷,同時(shí)只需要修改DOM文本內(nèi)容而不是移除或添加節(jié)點(diǎn)。既然如此,為什么我們還要建議帶key呢?因?yàn)檫@種不帶key的模式只適合渲染簡單的無狀態(tài)的組件。對(duì)于大多數(shù)場(chǎng)景來說,列表都得必須有自己的狀態(tài)。避免組件復(fù)用引起的錯(cuò)誤。
帶上key雖然會(huì)增加開銷,但是對(duì)于用戶來說基本感受不到差距,為了保證組件狀態(tài)正確,避免組件復(fù)用,這就是為什么建議使用key。
2、Vue的雙向綁定,Model如何改變View,View又是如何改變Model的?

我們先看一幅圖,下面一幅圖就是Vue雙向綁定的原理圖。

第一步,使數(shù)據(jù)對(duì)象變得“可觀測(cè)”

我們要知道數(shù)據(jù)在什么時(shí)候被讀或?qū)懥恕?br>
   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定義了一個(gè)name屬性,并把這個(gè)屬性的讀和寫分別使用get()和set()進(jìn)行攔截,每當(dāng)該屬性進(jìn)行讀或?qū)懖僮鞯臅r(shí)候就會(huì)觸發(fā)get()和set()。這樣數(shù)據(jù)對(duì)象已經(jīng)是“可觀測(cè)”的了。

核心是利用es5的Object.defineProperty,這也是Vue.js為什么不能兼容IE8及以下瀏覽器的原因。

Object.defineProperty方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性,并返回這個(gè)對(duì)象。

    Object.defineProperty(
        obj, // 定義屬性的對(duì)象
        prop, // 要定義或修改的屬性的名稱
        descriptor // 將要定義或修改屬性的描述符【核心】
    )



寫一個(gè)簡單的雙向綁定:

<!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>



第二步,使數(shù)據(jù)對(duì)象的所有屬性變得“可觀測(cè)”

上面,我們只能觀測(cè)person.name的變化,那么接下來我們要讓所有的屬性都變得可檢測(cè)。

    let person = observable({
        'name': 'maomin',
        'age': 23
    })
    /**
     * 把一個(gè)對(duì)象的每一項(xiàng)都轉(zhuǎn)化成可觀測(cè)對(duì)象
     * @param { Object } obj 對(duì)象
     */
    function observable(obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj); //返回一個(gè)表示給定對(duì)象的所有可枚舉屬性的字符串?dāng)?shù)組
        keys.forEach((key) => {
            defineReactive(obj, key, obj[key])
        })
        return obj;
    }
    /**
     * 使一個(gè)對(duì)象轉(zhuǎn)化成可觀測(cè)對(duì)象
     * @param { Object } obj 對(duì)象
     * @param { String } key 對(duì)象的key
     * @param { Any } val 對(duì)象的某個(gè)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()將一個(gè)對(duì)象返回一個(gè)表示給定對(duì)象的所有可枚舉屬性的字符串?dāng)?shù)組,然后遍歷它,使得所有對(duì)象可以被觀測(cè)到。
第三步,依賴收集,制作一個(gè)訂閱器

我們就可以在數(shù)據(jù)被讀或?qū)懙臅r(shí)候通知那些依賴該數(shù)據(jù)的視圖更新了,為了方便,我們需要先將所有依賴收集起來,一旦數(shù)據(jù)發(fā)生變化,就統(tǒng)一通知更新。
創(chuàng)建一個(gè)依賴收集容器,也就是消息訂閱器Dep,用來容納所有的“訂閱者”。訂閱器Dep主要負(fù)責(zé)收集訂閱者,然后當(dāng)數(shù)據(jù)變化的時(shí)候后執(zhí)行對(duì)應(yīng)訂閱者的更新函數(shù)。

設(shè)計(jì)了一個(gè)訂閱器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;



創(chuàng)建完訂閱器,然后還要修改一下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() //數(shù)據(jù)變化通知所有訂閱者
            }
        })
    }



我們將訂閱器Dep添加訂閱者的操作設(shè)計(jì)在get()里面,這是為了讓訂閱者初始化時(shí)進(jìn)行觸發(fā),因此需要判斷是否要添加訂閱者。
第四步,訂閱者Watcher

設(shè)計(jì)一個(gè)訂閱者Watcher類:

  class Watcher {
          // 初始化
        constructor(vm,exp,cb){
            this.vm = vm; // 一個(gè)Vue的實(shí)例對(duì)象
            this.exp = exp; // 是node節(jié)點(diǎn)的v-model或v-on:click等指令的屬性值。如v-model="name",exp就是name;
            this.cb = cb; // 是Watcher綁定的更新函數(shù);
            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]  // 強(qiáng)制執(zhí)行監(jiān)聽器里的get函數(shù)
            Dep.target = null;  // 釋放自己
            return value;
        }
    }



訂閱者Watcher在初始化的時(shí)候需要將自己添加進(jìn)訂閱器Dep中,如何添加呢?我們已經(jīng)知道監(jiān)聽器Observer是在get()執(zhí)行了添加訂閱者Wather的操作的,所以我們只要在訂閱者Watcher初始化的時(shí)候觸發(fā)對(duì)應(yīng)的get()去執(zhí)行添加訂閱者操作即可。那要如何觸發(fā)監(jiān)聽器get(),再簡單不過了,只要獲取對(duì)應(yīng)的屬性值就可以觸發(fā)了。

訂閱者Watcher運(yùn)行時(shí),首先進(jìn)入初始化,就會(huì)執(zhí)行它的 this.get() 方法,
執(zhí)行Dep.target = this;,實(shí)際上就是把Dep.target 賦值為當(dāng)前的渲染 Watcher ,接著又執(zhí)行了let value = this.vm.data[this.exp];。在這個(gè)過程中會(huì)對(duì)數(shù)據(jù)對(duì)象上的數(shù)據(jù)訪問,其實(shí)就是為了觸發(fā)數(shù)據(jù)對(duì)象的get()。

每個(gè)對(duì)象值的get()都持有一個(gè)dep,在觸發(fā) get()的時(shí)候會(huì)調(diào)用 dep.depend()方法,也就會(huì)執(zhí)行this.addSub(Dep.target),即把當(dāng)前的 watcher訂閱到這個(gè)數(shù)據(jù)持有的dep.subs中,這個(gè)目的是為后續(xù)數(shù)據(jù)變化時(shí)候能通知到哪些 subs 做準(zhǔn)備。完成依賴收集后,還需要把 Dep.target恢復(fù)成上一個(gè)狀態(tài)Dep.target = null; 因?yàn)楫?dāng)前vm的數(shù)據(jù)依賴收集已經(jīng)完成,那么對(duì)應(yīng)的渲染Dep.target 也需要改變。

而update()是用來當(dāng)數(shù)據(jù)發(fā)生變化時(shí)調(diào)用Watcher自身的更新函數(shù)進(jìn)行更新的操作。先通過let value = this.vm.data[this.exp];獲取到最新的數(shù)據(jù),然后將其與之前get()獲得的舊數(shù)據(jù)進(jìn)行比較,如果不一樣,則調(diào)用更新函數(shù)cb進(jìn)行更新。
總結(jié):

實(shí)現(xiàn)數(shù)據(jù)的雙向綁定,首先要對(duì)數(shù)據(jù)進(jìn)行劫持監(jiān)聽,所以我們需要設(shè)置一個(gè)監(jiān)聽器Observer,用來監(jiān)聽所有屬性。如果屬性發(fā)上變化了,就需要告訴訂閱者Watcher看是否需要更新。因?yàn)橛嗛喺呤怯泻芏鄠€(gè),所以我們需要有一個(gè)消息訂閱器Dep來專門收集這些訂閱者,然后在監(jiān)聽器Observer和訂閱者Watcher之間進(jìn)行統(tǒng)一管理的。


實(shí)現(xiàn)一個(gè)Vue數(shù)據(jù)綁定:

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內(nèi)容" onclick="changeInput()">
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
    function myVue (data, el, exp) {
        this.data = data;
        observable(data);                      //將數(shù)據(jù)變的可觀測(cè)
        el.innerHTML = this.data[exp];         // 初始化模板數(shù)據(jù)的值
        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');
    
    //改變輸入框內(nèi)容
    input.oninput = function (e) {
        myVue.data.name = e.target.value
    }
    //改變data內(nèi)容
    function changeInput(){
        myVue.data.name = "改變后的data"
    }
</script>
</body>
</html>


observer.js(為了方便,這里將訂閱器與監(jiān)聽器寫在一塊)

     // 監(jiān)聽器
     // 把一個(gè)對(duì)象的每一項(xiàng)都轉(zhuǎn)化成可觀測(cè)對(duì)象
     // @param { Object } obj 對(duì)象
     
    function observable (obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj);
        keys.forEach((key) =>{
            defineReactive(obj,key,obj[key])
        })
        return obj;
    }
     // 使一個(gè)對(duì)象轉(zhuǎn)化成可觀測(cè)對(duì)象
     // @param { Object } obj 對(duì)象
     // @param { String } key 對(duì)象的key
     // @param { Any } val 對(duì)象的某個(gè)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()                    //數(shù)據(jù)變化通知所有訂閱者
            }
        })
    }

    // 訂閱器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]  // 強(qiáng)制執(zhí)行監(jiān)聽器里的get函數(shù)
            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的區(qū)別在哪里?

我們先看一個(gè)例子:

<!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>{}</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>



一開始的時(shí)候,

點(diǎn)擊按鈕時(shí),
在這里插入圖片描述
我們可以看到一開始的時(shí)候,打印出了computed,當(dāng)點(diǎn)擊按鈕時(shí),data內(nèi)的屬性值a發(fā)生變化,打印出watch,接著我們不停點(diǎn)擊按鈕,并沒有打印。(?查看總結(jié)4)

我們來總結(jié)一下,

    最本質(zhì)的區(qū)別,computed為計(jì)算屬性,watch為監(jiān)聽屬性。
    watch就是單純的監(jiān)聽某個(gè)數(shù)據(jù)的變化,支持深度監(jiān)聽。computed是計(jì)算屬性,是依賴于某個(gè)或者某些屬性值,當(dāng)依賴值發(fā)生變化時(shí),也會(huì)發(fā)生變化。
    計(jì)算屬性不在data中,計(jì)算屬性依賴值在data中。watch監(jiān)聽的數(shù)據(jù)在data中。(不一定在只是data,也可能是props)
    watch用于觀察和監(jiān)聽頁面上的vue實(shí)例,當(dāng)你需要在數(shù)據(jù)變化響應(yīng)時(shí),執(zhí)行異步操作,或高性能消耗的操作,那么watch為最佳選擇。computed可以關(guān)聯(lián)多個(gè)實(shí)時(shí)計(jì)算的對(duì)象,當(dāng)這些對(duì)象中的其中一個(gè)改變時(shí)都會(huì)觸發(fā)這個(gè)屬性,具有緩存能力,所以只有當(dāng)數(shù)據(jù)再次改變時(shí)才會(huì)重新渲染,否則就會(huì)直接拿取緩存中的數(shù)據(jù)。
    computed是在Dep.update()執(zhí)行之后,數(shù)據(jù)更新之前,對(duì)數(shù)據(jù)重新改造。watch是在set剛開始發(fā)生的時(shí)候添加的回調(diào),可以監(jiān)聽數(shù)據(jù)的變化。

4、為什么在Vue3.0采用了Proxy,拋棄了Object.defineProperty?

Object.defineProperty無法監(jiān)控到數(shù)組下標(biāo)的變化,導(dǎo)致直接通過數(shù)組的下標(biāo)給數(shù)組設(shè)置值,不能實(shí)時(shí)響應(yīng)。為了解決這個(gè)問題,經(jīng)過Vue內(nèi)部處理后可以使用以下幾種方法來監(jiān)聽數(shù)組。

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

由于只針對(duì)以上八種方法進(jìn)行了hack處理,所以其他數(shù)組的屬性方法也是檢測(cè)不到的,還是具有一定的局限性。

這里我們舉個(gè)例子,可以看得更加明白:

<!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'; // 無法監(jiān)聽
                this.watchArr.length = 5; // 無法監(jiān)聽
            }
        },
        watch: {
            watchArr(newVal) {
                console.log('監(jiān)聽了');
            },
        }
    })
</script>
</html>



想必看到上面的例子我們會(huì)更加明白Object.defineProperty的局限性。接下來,我們接著說Object.defineProperty只能劫持對(duì)象的屬性,因此,我們需要對(duì)每個(gè)對(duì)象的每個(gè)屬性進(jìn)行遍歷。Vue2.0里,是通過遞歸+遍歷data對(duì)象來實(shí)現(xiàn)對(duì)數(shù)據(jù)的監(jiān)控的,如果屬性值也是對(duì)象的話,那么需要深度遍歷。顯然如果能夠劫持一個(gè)完整的對(duì)象才是更好的選擇。

那么Proxy有以下兩個(gè)優(yōu)點(diǎn):

    可以劫持整個(gè)對(duì)象,并返回一個(gè)新對(duì)象
    有13種劫持操作

摒棄 Object.defineProperty,基于Proxy的觀察者機(jī)制探索
5、為什么Vuex的mutation不能做異步操作?

因?yàn)楦膕tate的函數(shù)必須是純函數(shù),純函數(shù)既是統(tǒng)一輸入就會(huì)統(tǒng)一輸出,沒有任何副作用;如果是異步則會(huì)引起額外的副作用,導(dǎo)致更改后的state不可預(yù)測(cè)。
6、Vue中的computed是如何實(shí)現(xiàn)的?

實(shí)質(zhì)是一個(gè)惰性的wather,在取值操作時(shí)根據(jù)自身標(biāo)記dirty屬性返回上一次計(jì)算結(jié)果或重新計(jì)算值在創(chuàng)建時(shí)就進(jìn)行一次取值操作,收集依賴變動(dòng)的對(duì)象或?qū)傩裕▽⒆陨韷喝雂ep中),在依賴的對(duì)象或?qū)傩宰儎?dòng)時(shí),僅將自身標(biāo)記dirty致為true。
7、Vue的父組件和子組件的生命周期鉤子函數(shù)執(zhí)行順序是什么?

    加載渲染過程
    (父)beforeCreate → (父)created → (父)beforeMount → (子)beforeCreate → (子)created → (子)beforeMount → (子)mounted → (父)mounted
    子組件更新過程
    (父)beforeUpdate → (子)beforeUpdate → (子)Updated → (父)Updated
    父組件更新過程
    (父)beforeUpdate → (父)Updated
    銷魂過程
    (父)beforeDestroy → (子)beforeDestory → (子)destroyed → (父)destroyed

作者:Vam的金豆之路

主要領(lǐng)域:前端開發(fā)

我的微信:maomin9761

微信公眾號(hào):前端歷劫之路