本篇文章主要想简单聊聊vue如何实现数据修改,页面联动的底层原理
当然,篇幅有限,只是自己一些浅显的认知而已,我会从一下几个方面去聊,希望对你有所帮助。
  • 几个基础知识点
  • 数据代理
  • 数据劫持
  • 完整demo
 

一、几个基础知识点

1.普通函数和箭头函数的区别

我们知道,每个函数执行都会形成一条作用域链[[scopes]],函数内的所有变量其实都是在这条链上找的。

vue2原理初探-数据代理和数据劫持-LMLPHP

 如上图所示,a函数定义在全局,其作用域链,只有GO对象,当其执行的时候会临时产生一个aAO对象,所以b函数的作用域链就是 aAO -> GO

函数每次执行都会产生一个新的AO对象挂在作用域链头部,函数被解释执行的时候,其内部标识符的检索都是在作用域链上检索的。

根据以上理论,我们来执行b函数。

vue2原理初探-数据代理和数据劫持-LMLPHP

 控制台输出如下:

vue2原理初探-数据代理和数据劫持-LMLPHP

 为什么b函数中看到的this是window呢?是因为其顺着作用域链找,bBO -> aAO -> GO,只有GO上有this,就是window。

当然对于普通函数,我们可以改变其this指向:

vue2原理初探-数据代理和数据劫持-LMLPHP

 控制台输出如下:

vue2原理初探-数据代理和数据劫持-LMLPHP

 于是我们得到一个结论:

函数执行生成的临时AO对象中,包含了arguments隐式变量来保存实参列表。

函数执行看到的this变量,可以修改,通过对象调用,call,apply来修改。

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

箭头函数,它就不是这样的。。。

vue2原理初探-数据代理和数据劫持-LMLPHP

 控制台输出:

vue2原理初探-数据代理和数据劫持-LMLPHP

 箭头函数,没有arguments隐式变量了。而且,this它居然修改不了。。

那么说白了,this只能在其作用域链上找了,生成的临时AO对象上没有this,没有this。

所以:
 
 
 
 
 
 

2.闭包

 
其实,理解了作用域链就理解了闭包。

vue2原理初探-数据代理和数据劫持-LMLPHP

由于plus,minus,showCount三个函数的作用域链中有aaa的AO对象,所以当他们被返回后,形成了闭包。

所以三个函数都能看见count变量。
 
 
 

3.defineProperty函数的使用

该函数可以说是Vue2中底层实现的基础。
我们一般定义对象都像如下这样:

vue2原理初探-数据代理和数据劫持-LMLPHP

 但是其实除了这样给对象加属性外,我们也可以通过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>

上述代码控制台输入如下:

vue2原理初探-数据代理和数据劫持-LMLPHP

 其实这样的话,定义属性和我们直接写属性没什么太大区别,关键是下面这样的写法:

<!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方法了。

控制台输出如下:

vue2原理初探-数据代理和数据劫持-LMLPHP

 这个ccc属性的三个点,是不是特别想我们使用vue的时候点开的组件对象里面的一些属性。

在这里我插一句,我点开set/get给大家看看,其实能看见[[scopes]]作用域链了。

如下图:

vue2原理初探-数据代理和数据劫持-LMLPHP

 当然,这里我们看见了,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/vue@2.7.14/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对象打印出来:

vue2原理初探-数据代理和数据劫持-LMLPHP

 可以清晰的看到,我们配置的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原理初探-数据代理和数据劫持-LMLPHP

我相信到这里,你应该能理解vue2大体怎么实现响应式的了。
无非就是2点:
1.使用数据代理给每个组件实例添加属性,方便我们编程的时候操作;
2.使用数据劫持,监听对象每个层级属性的变化,内部触发重新渲染。
当然,真正的源码包含了对对象的每个层级的属性的监测,我这里只是简单写个demo,目的是为了方便大家理解。
数据劫持,内部肯定是闭包,使用闭包封装了每个属性,本篇也提到了作用域链[[scopes]]还有我们在编程中经常困惑的箭头函数。
总之,希望对大家有帮助吧。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
09-15 15:22