vue2原理初探-資料代理和資料劫持

2023-09-15 18:00:37
本篇文章主要想簡單聊聊vue如何實現資料修改,頁面聯動的底層原理
當然,篇幅有限,只是自己一些淺顯的認知而已,我會從一下幾個方面去聊,希望對你有所幫助。
  • 幾個基礎知識點
  • 資料代理
  • 資料劫持
  • 完整demo
 

一、幾個基礎知識點

1.普通函數和箭頭函數的區別

我們知道,每個函數執行都會形成一條作用域鏈[[scopes]],函數內的所有變數其實都是在這條鏈上找的。

 如上圖所示,a函數定義在全域性,其作用域鏈,只有GO物件,當其執行的時候會臨時產生一個aAO物件,所以b函數的作用域鏈就是 aAO -> GO

函數每次執行都會產生一個新的AO物件掛在作用域鏈頭部,函數被解釋執行的時候,其內部識別符號的檢索都是在作用域鏈上檢索的。

根據以上理論,我們來執行b函數。

 控制檯輸出如下:

 為什麼b函數中看到的this是window呢?是因為其順著作用域鏈找,bBO -> aAO -> GO,只有GO上有this,就是window。

當然對於普通函數,我們可以改變其this指向:

 控制檯輸出如下:

 於是我們得到一個結論:

函數執行生成的臨時AO物件中,包含了arguments隱式變數來儲存實參列表。

函數執行看到的this變數,可以修改,通過物件呼叫,call,apply來修改。

但是,但是,但是。。。。

箭頭函數,它就不是這樣的。。。

 控制檯輸出:

 箭頭函數,沒有arguments隱式變數了。而且,this它居然修改不了。。

那麼說白了,this只能在其作用域鏈上找了,生成的臨時AO物件上沒有this,沒有this。

所以:
  是否有arguments隱式變數 是否能改變this指向
普通函數
箭頭函數
 
 
 
 
 
 

2.閉包

 
其實,理解了作用域鏈就理解了閉包。

由於plus,minus,showCount三個函數的作用域鏈中有aaa的AO物件,所以當他們被返回後,形成了閉包。

所以三個函數都能看見count變數。
 
 
 

3.defineProperty函數的使用

該函數可以說是Vue2中底層實現的基礎。
我們一般定義物件都像如下這樣:

 但是其實除了這樣給物件加屬性外,我們也可以通過defineProperty來給物件加屬性。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>demo01-defineProperty的使用</title>
</head>
<body>
    <script type="application/javascript">
        // defineProperty()
        let obj = {
            name: 'zhangsan',
            age: 33,
            showInfo() {
                console.log(this.name + "--" + this.age)
            }
        }
        obj.showInfo();

        Object.defineProperty(obj, 'ccc', {
            value: 10,
            enumerable: true,  // 是否能列舉
            configurable: true, // 是否能刪除
            writable: true  // 是否能寫入
        })
        // 列舉
        var keys = Object.keys(obj);
        console.log(keys);
        // 寫入
        obj.ccc = 100;
        console.log(obj);
        // 刪除
        delete obj.ccc;
        console.log(obj);

    </script>
</body>
</html>

上述程式碼控制檯輸入如下:

 其實這樣的話,定義屬性和我們直接寫屬性沒什麼太大區別,關鍵是下面這樣的寫法:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>demo01-defineProperty的使用</title>
</head>
<body>
    <script type="application/javascript">
        // defineProperty()
        let obj = {
            name: 'zhangsan',
            age: 33,
            showInfo() {
                console.log(this.name + "--" + this.age)
            }
        }

        let ccc = 10;
        Object.defineProperty(obj, 'ccc', {
            // value: 10,
            enumerable: true,  // 是否能列舉
            configurable: true, // 是否能刪除
            // writable: true,  // 是否能寫入
            get: function proxyGet() {
                return ccc;
            },
            set: function proxySet(value) {
                ccc = value;
            }
        })
        console.log(obj.ccc);
        obj.ccc = 100;
        console.log(obj.ccc);

        console.log(obj);

    </script>
</body>
</html>

注意,如果我們要定義屬性的get/set,那麼就不能定義value和writable了,否則會報錯。

此時我們對屬性ccc的寫入和讀取將走get/set方法了。

控制檯輸出如下:

 這個ccc屬性的三個點,是不是特別想我們使用vue的時候點開的元件物件裡面的一些屬性。

在這裡我插一句,我點開set/get給大家看看,其實能看見[[scopes]]作用域鏈了。

如下圖:

 當然,這裡我們看見了,set/get函數定義的時候的作用域鏈[[scopes]],其實是SO -> GO,這個SO其實就是外層包裹的script標籤。

可以理解成,script標籤執行流程就像一個函數執行一樣,也會產生作用域物件掛在[[scopes]]上。

其實講到這裡,這個set/get是資料代理和劫持的關鍵。
 
 

二、資料代理

 
再講資料代理之前,我們可以使用vue來寫個demo,目的看看我們每次設定元件的時候,data物件去哪裡了?
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>demo01-vue簡單使用</title>
    <!--引入vue-->
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>

</head>
<body>

    <div id="app">
        <h3>姓名:{{name}}</h3>
        <h3>年齡:{{age}}</h3>
        <button @click="agePlusOne">年齡+1</button>
    </div>

    <script type="application/javascript">

        let vm = new Vue({
            el: '#app',
            data(){
                return {
                    name: '張三',
                    age: 33
                }
            },
            methods: {
                agePlusOne(){
                    this.age ++;
                    console.log(this)
                }
            }
        });

    </script>
</body>
</html>

點選按鈕,我把Vue物件列印出來:

 可以清晰的看到,我們設定的data物件中的屬性都被定義在了Vue元件物件中。

起碼,這裡看到了,vue做了資料代理,我們在元件物件中對data中同名屬性的set和get都走了其對應的代理方法。

到這裡我們可以這樣理解,options.data物件傳入之後,vue生成了一個_data物件掛在了範例身上,而且vm._data === options.data。
然後,vue通過defineProperty方法在範例身上定義了data中定義的屬性,並set/get都指向了_data中的對應屬性。
 

三、資料劫持

我理解的資料劫持,就是屬性在設定或者獲取的時候,做點什麼?
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>demo01-資料劫持</title>
</head>
<body>
    <div id="app">
    </div>
    <script type="application/javascript">
        function setAppInnerText (value) {
            document.querySelector("#app").innerText = value;
        }

        let obj = {
            name: '張三',
            age: 100,
            showInfo() {
                return this.name + "---" + this.age;
            }
        };

        setAppInnerText(obj.showInfo())
        // 資料劫持
        Object.keys(obj).forEach(key => {
            let value = obj[key];
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                set(newValue) {
                    if(newValue === value) {
                        return ;
                    } else {
                        value = newValue;
                        setAppInnerText(obj.showInfo());
                    }
                },
                get() {
                    return value;
                }
            })
        })
        obj.age = 1000;
    </script>
</body>
</html>

上面的程式碼,就是資料劫持,每次屬性設定的時候,都觸發了setAppInnerText函數的呼叫。

上述程式碼執行完之後,只要我們對obj物件的屬性進行修改,都會觸發頁面的變化。

 
 

四、完整demo

當然,任何一個框架的程式碼都是很複雜的,因為要考慮很多東西。
這裡我只是基於自己的理解,寫一個簡單的demo,目的只是為了幫助大家理解vue,理解它如何做到資料修改,頁面變化的。
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>data_observer</title>
</head>

<body>
<div id="root">
    a = {{a}}
    <br>
    b = {{b}}
</div>
<script>

    function Vue(config) {
        this._data = config.data;
        // 資料代理 方便程式設計師操作
        for (let key in config.data) {
            Object.defineProperty(this, key, {
                enumerable: true,
                get: function proxyGet() {
                    return this._data[key];
                },
                set: function proxySet(value) {
                    this._data[key] = value;
                }
            })
        }
        this.mounted = false;
        if (config.el) {
            this.$mount(config.el);
        }
    }

    Vue.prototype.$mount = function (id) {
        if (!this.mounted) {
            this.originInnerHtml = document.getElementById(id).innerHTML;
            // 編譯模板生成render
            let _self = this;
            function render() {
                let innerHtml = _self.originInnerHtml;
                for (let key in _self._data) {
                    innerHtml = innerHtml.replaceAll('{{' + key + '}}', _self._data[key]);
                }
                document.getElementById(id).innerHTML = innerHtml;
            }

            // 資料劫持
            for (let key in this._data) {
                let value = this._data[key];
                Object.defineProperty(this._data, key, {
                    enumerable: true,
                    configurable: true,
                    get: function getObserver() {
                        return value;
                    },
                    set: function setObserver(newValue) {
                        if (value !== newValue) {
                            value = newValue;
                            render();
                        }
                    }
                })
            }
            // 執行render
            render();
            this.mounted = true;
        }
    }
    let config =  {
        el: 'root',
        data: {
            a: '牛逼的訊息',
            b: '學習vue2底層實現'
        }
    };
    let vm = new Vue(config);
</script>
</body>

</html>

控制檯列印如下:

 

我相信到這裡,你應該能理解vue2大體怎麼實現響應式的了。
無非就是2點:
1.使用資料代理給每個元件範例新增屬性,方便我們程式設計的時候操作;
2.使用資料劫持,監聽物件每個層級屬性的變化,內部觸發重新渲染。
當然,真正的原始碼包含了對物件的每個層級的屬性的監測,我這裡只是簡單寫個demo,目的是為了方便大家理解。
資料劫持,內部肯定是閉包,使用閉包封裝了每個屬性,本篇也提到了作用域鏈[[scopes]]還有我們在程式設計中經常困惑的箭頭函數。
總之,希望對大家有幫助吧。