vue---剖析vue響應式原理

1. 前言

在vue中,只要數據變化,頁面就會重新渲染,這個是怎麼做到的呢?
在創建vue實例時,vue會將data中的成員代理給vue實例,目的就是實現響應式,監控數據變化,然後執行某個事件函數。在vue2.0中使用的是Object.defineProperty來實現數據的劫持,配合發佈-訂閱者模式來實現。

2. Object.defineProperty

首先我們來看一下怎麼使用Object.defineProperty,其實使用方法很簡單。這個函數接收三個參數:
1.需要監控的對象
2.需要監控的對象的某個屬性
3.一個配置對象
前面兩個參數很好理解,一目瞭然,第三個配置對象有很多屬性,在這裏主要介紹他的兩個函數:set、get。
當我們給一個對象添加屬性的時候就會調用set函數,當我們讀取某個對象的屬性的時候就會調用get方法,請看下面這個小例子:

const data = {
    name:'小曹',
    age:18
}
Object.defineProperty(data, "name", {
    get(){
        console.log('讀取屬性');
        return 'xxx';
    },
    set(value){
        console.log('設置屬性:', value)
    }
})
console.log(data.name);//輸出結果:讀取屬性  xxx
data.name = "靚仔";//輸出結果:設置屬性:靚仔

上面例子的第一個console是獲取data的值,觸發了get方法。而且詭異的是輸出結果還並不是我們想象中的“小曹”,而是‘xxx’。這是因爲,Object.defineProperty的get方法返回值就是監控的對象的值,所以我們會打印出‘xxx’。
第二個console是我們重新賦值,這時候觸發了set方法,並且det方法接收一個參數,這個參數就是要設置的值,所以我們可以在set方法中對賦值進行控制。
使用這種方式只能監控一個屬性,顯然這並不符合我們的需求,所以可以使用循環遍歷來實現

3. 監控對象屬性

實現同時監控多個對象屬性,我們可以使用for-in遍歷,封裝成一個函數:

function defineReactive(data, key, value){
    Object.defineProperty(data, key, {
        get(){
            console.log('讀取屬性');
            return value;
        },
        set(val){
            console.log('設置屬性:', value)
            value = val;
        }
    })
}
function observer(data){
    for(let key in data){
        defineReactive(data, key, data[key]);
    }
}
observer(data);
console.log(data.age);//18
console.log(data.name);//小曹

這樣就可實現監控對象的讀和寫的操作。既然我們已經可以監控到對象的寫這個操作,那麼在vue中,每次重新賦值就會再次渲染頁面,達到響應式。那麼我們就可以在set函數裏執行渲染函數,我們給這個渲染函數起名爲render,render函數內部具體實現在這裏我就不多說了,在這裏就用一句話代替。渲染函數具體實現可以看這個篇文章https://blog.csdn.net/qq_44197554/article/details/105904564
在我們設置屬性值的時候,如果兩個值相等,那麼我們還有重新渲染的必要嗎?是不是就不需要渲染了,這個可以節省性能,所以我們在set函數中加個判斷

set(val){
   console.log('設置屬性:', value)
   //如果兩值相等,則直接結束
   if(value === val){
       return;
   }
   value = val;
   render();
}

4. 遞歸遍歷

現在又有一個問題,如果我們的data對象中的某個屬性值是一個對象,這樣還能監控到嗎,測試過後發現,是不能的,所以我們就需要用到遞歸了,請看下面代碼:

const data = {
    name:'小曹',
    age:18,
    obj:{
        a:1
    }
}
function defineReactive(data, key, value){
	//進行遞歸遍歷
    observer(value);
    Object.defineProperty(data, key, {
        get(){
            console.log('讀取屬性');
            return value;
        },
        set(val){
            console.log('設置屬性:', value)
            if(value === val){
                return;
            }
            value = val;
            render();
        }
    })
}
function observer(data){
	//判斷傳入的是否爲一個對象
    if(typeof data === 'object'){
        for(let key in data){
            defineReactive(data, key, data[key]);
        }
    }
}

經過上面的遞歸遍歷,不管data裏嵌套多少層對象,都會被監控到

5. 數組

vue響應式中 Object.defineProperty沒有觀察數組,原因是太消耗新能。官方說性能與用戶體驗不成正比
所以我們就需要在observer函數中判斷傳入的data是否爲一個數組

function observer(data){
	//判斷是否爲數組
    if(Array.isArray(data)){
        return;
    }
    if(typeof data === 'object'){
        for(let key in data){
            defineReactive(data, key, data[key]);
        }
    }
}

那麼在vue中操作數組是怎麼實現的呢?是因爲在vue中重寫了數組的方法,所以當我們在vue中使用數組的方法時,就執行了render函數

//保存數組原型
const arrayProto = Array.prototype;
//克隆一個原型對象
const arrayMethods = Object.create(arrayProto);
//重寫所有的函數
['push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse'].forEach(method => {
    arrayMethods[method] = function (){
        //改變重寫函數的this指向
        //展開傳入的值
        arrayProto[method].call(this, ...arguments);
        render();
    }
})

大家是不是有疑問,爲什麼要重新克隆一個數組的原型呢?
是因爲只需要在使用數組變異方法的時候執行這些重寫的方法,對於其他的數組不污染其原型。所以更改的是克隆出來的原型,而不是本來的原型。此時數組執行的還是本來原型上的方法,所以需要在observer函數中修改數組的原型指向

function observer(data){
    if(Array.isArray(data)){
        //改變數組原型指向
        data.__proto__ = arrayMethods;
        return;
    }
    if(typeof data === 'object'){
        for(let key in data){
            defineReactive(data, key, data[key]);
        }
    }
}
data.arr.push(100);
console.log(data.arr);//[1, 2, 3, 100]

(1)實現$set

$set方法的返回值就是要修改的值,該方法有三個參數:
1.data:要修改的對象或數組
2.key:要修改哪個屬性
3.value:修改成什麼

//實現$set方法
function $set(data, key, value){
    //修改數組
    if(Array.isArray(data)){
        data.splice(key, 1, value);
        return value;
    }
    //修改對象
    defineReactive(data, key, value);
    render();
    return value;
}

(2)實現$delete

該函數接收兩個參數:
1.data:要刪除哪個對象的屬性
2.key:刪除哪個屬性

//實現$delete
function $delete(data, key){
    if(Array.isArray(data)){
        data.splice(key, 1);
        return;
    }
    delete data[key];
    render();
}

6.劣勢

因此使用Object.defineProperty實現響應式有幾個劣勢
1.天生就需要進行遞歸
2.監聽不到數組不存在的索引的改變
3.監聽不到數組長度的改變
4.監聽不到對象的增刪
在vue3.0中解決了遞歸觀察的問題,使用proxy代理

7.所有源碼

const data = {
    name:'小曹',
    age:18,
    obj:{
        a:1
    },
    arr:[1, 2, 3]
}

//保存數組原型
const arrayProto = Array.prototype;
//重新創建一個原型對象
const arrayMethods = Object.create(arrayProto);
//重寫所有的函數
['push', 'pop', 'shift', 'unshift', 'sort', 'splice', 'reverse'].forEach(method => {
    arrayMethods[method] = function (){
        //改變重寫函數的this指向
        //展開傳入的值
        arrayProto[method].call(this, ...arguments);
        render();
    }
})

function defineReactive(data, key, value){
    observer(value);
    Object.defineProperty(data, key, {
        get(){
            console.log('讀取屬性');
            return value;
        },
        set(val){
            console.log('設置屬性:', value)
            if(value === val){
                return;
            }
            value = val;
            render();
        }
    })
}
function observer(data){
    if(Array.isArray(data)){
        //改變數組原型指向
        data.__proto__ = arrayMethods;
        return;
    }
    if(typeof data === 'object'){
        for(let key in data){
            defineReactive(data, key, data[key]);
        }
    }
}

function render(){
    console.log('頁面渲染了');
}

//實現$set方法
function $set(data, key, value){
    //修改數組
    if(Array.isArray(data)){
        data.splice(key, 1, value);
        return value;
    }
    //修改對象
    defineReactive(data, key, value);
    render();
    return value;
}

//實現$delete
function $delete(data, key){
    if(Array.isArray(data)){
        data.splice(key, 1);
        return;
    }
    delete data[key];
    render();
}

observer(data);
data.arr.push(100);
console.log(data.arr);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章