面试-高级前端之VUE数据响应式实现

765 阅读1分钟

什么是数据响应式

数据响应式既数据双向绑定,就是把model绑定到view,当我们用javascript代码更新model时,view就是自动更新;如果用户更新了view,model的数据也自动更新了,这种情况就是双向绑定;

数据响应式原理

vue实现数据响应式的原理就是利用了Object.defineProperty()这个方法重新定义了对象获取属性(get )和设置属性(set)的操作实现的。但是在vue3.0版本中采用了ES6的proxy对象来实现。

数据响应式实现

1、首先根据上图实现整体的一个架构(包括MVVM类或者VUE类,Watcher类),这里用到一个订阅发布者设计模式;
2、实现model到view,把模型里面的数据绑定到视图;
3、最后实现view到model,当视图更新时,同时更新相对应的模型;

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>响应式实现</title>
    <script>
        //发布者
        class Vue {
            constructor(options) {
                this.$data = options.data;
                this.$el = document.querySelector(options.el);
                //容器,保存订阅者信息
                this._deps = {};
                this.Observer(this.$data);
                this.Compile(this.$el);
            }
            //劫持数据
            Observer(data) {
                for (let key in data) {
                    this._deps[key] = [];
                    var val = data[key];
                    let watchers = this._deps[key];
                    Object.defineProperty(this.$data, key, {
                        get: function () {
                            return val;
                        },
                        set: function(newVal){
                            if (newVal !== val) {
                                val = newVal;
                                watchers.forEach(watcher=>{
                                    watcher.update();
                                });
                            }
                        }
                    })
                }
            }
            //解析指令,找到指令以后进行依赖收集-----更新视图---订阅
            Compile(el) {
                var nodes = el.children;
                for (let i = 0; i < nodes.length; i++) {
                    var node = nodes[i];
                    if (node.children.length > 0) {
                        this.Compile(node);
                    }
                    if (node.hasAttribute('v-text')) {
                        var attVal = node.getAttribute('v-text');
                        this._deps[attVal].push(new Watcher(node, this, attVal, 'innerHTML'));
                    }
                    if (node.hasAttribute('v-model')) {
                        var attVal = node.getAttribute('v-model');
                        this._deps[attVal].push(new Watcher(node, this, attVal, 'value'));
                        node.addEventListener('input', () => {
                            this.$data[attVal] = node.value;
                        });
                    }
                }

            }
        }
        //订阅者,自己更新视图
        class Watcher {
            constructor(el, vm, attVal, attr) {
                this.el = el;
                this.vm = vm;
                this.attVal = attVal;
                this.attr = attr;
                this.update();
            }
            update() {
                this.el[this.attr] = this.vm.$data[this.attVal];
            }
        }

    </script>
</head>

<body>
    <div id="app">
        <h1>
            数据响应式
        </h1>
        <div>
            <!-- <div v-text="myText"></div> -->
            <div v-text="myBox"></div>
            <!-- <input type="text" v-model="myText"> -->
            <input type="text" v-model="myBox">
        </div>
    </div>
    <script>
        const app = new Vue({
            el: '#app',
            data: {
                // myText: '我是一个文本',
                myBox: '我是一个盒子'
            }
        });
    </script>
</body>

</html>

效果图:

proxy

由于Object.defineProperty()以下缺陷:

  1. 不能检测对象属性的添加或删除的;
  2. 不能检测到数组长度变化(通过改变length而增加的长度不能监测到);
  3. 不是因为defineProperty的局限性,而是出于性能考量的,不会对数组每个元素都监听; vue 3.0会采用proxy来实现数据的劫持,proxy是Object.defineProperty()的增强版

1、Proxy可以直接监听对象而非属性;
2、 Proxy可以直接监听数组的变化;
3、Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的;

<!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>proxy模拟实现vue的双向数据绑定</title>
</head>
<body>
    <div id="app">
        <span v-text="myText"></span>
        <input type="text" v-model="myText"/>
    </div>
    <script>
        class Vue{
            constructor(options){
                this.el = document.querySelector(options.el);
                this.data = options.data;
                this.deps = {};
                this.Observer(this.data);
                this.Compiler(this.el);
            }
            Observer(data){
                for(let key in data){
                    this.deps[key] = [];
                }
                var deps = this.deps;
                var proxy = new Proxy(data,{
                    get:function(target,key,receiver){
                        return Reflect.get(target,key,receiver);
                    },
                    set:function(target,key,newVal,receiver){
                        var oldVal = data[key];
                        if(oldVal != newVal){
                            let watchers = deps[key];
                            watchers.forEach(function(watcher){
                                watcher.update();
                            });
                        }
                        return Reflect.set(target,key,newVal,receiver);
                    }
                })
                this.newData = proxy;
            }
            Compiler(el){
                var childs = el.children;
                for(let i=0;i<childs.length;i++){
                    var node = childs[i];
                    if(node.children.length>0){
                        this.Compiler(node);
                    }
                    if(node.hasAttribute('v-text')){
                        var attrVal = node.getAttribute('v-text');
                        var watcher = new Watcher(node,this,attrVal,'innerHTML')
                        this.deps[attrVal].push(watcher);
                    }
                    if(node.hasAttribute('v-model')){
                        var attrVal = node.getAttribute('v-model');
                        var watcher = new Watcher(node,this,attrVal,'value');
                        this.deps[attrVal].push(watcher);
                        node.addEventListener('input', () => {
                            this.newData[attrVal] = node.value;
                        });

                    }
                }
            }          
        }
         //订阅者,自己更新视图
         class Watcher {
            constructor(el, vm, attVal, attr) {
                this.el = el;
                this.vm = vm;
                this.attVal = attVal;
                this.attr = attr;
                this.update();
            }
            update() {
                this.el[this.attr] = this.vm.data[this.attVal];
            }
        }

        new Vue({
            el:'#app',
            data:{
                myText:'这个是输入框显示的内容'
            }
        })
    </script>
</body>
</html>