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

本篇文章主要想简单聊聊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/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对象打印出来:

可以清晰的看到,我们配置的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]]还有我们在编程中经常困惑的箭头函数。
总之,希望对大家有帮助吧。

相关推荐
空中海4 小时前
01 React Native 基础、核心组件与布局体系
javascript·react native·react.js
前端之虎陈随易6 小时前
2年没用Nodejs了,Bun很香
linux·前端·javascript·vue.js·typescript
好运的阿财7 小时前
OpenClaw工具拆解之host_workspace_write+host_workspace_edit
前端·javascript·人工智能·机器学习·ai编程·openclaw·openclaw工具
XiYang-DING8 小时前
JavaScript
开发语言·javascript·ecmascript
空中海9 小时前
02 React Native状态、导航、数据流与设备能力
javascript·react native·react.js
空中海9 小时前
02 状态、Hooks、副作用与数据流
开发语言·javascript·ecmascript
abcnull10 小时前
传统的JavaWeb项目Demo快速学习!
java·servlet·elementui·vue·javaweb
空中海10 小时前
04 React Native工程化、质量、发布与生态选型
javascript·react native·react.js
杨超凡10 小时前
豆包收费了?我特么自己用“意念”搓了一个!
javascript