zoukankan      html  css  js  c++  java
  • 实现一个简易的vue的mvvm(defineProperty)

    这是一个最近一年很火的面试题,很多人看到这个题目从下手,其实查阅一些资料后,简单的模拟还是不太难的:

    vue不兼容IE8以下是因为他的实现原理使用了 Object.defineProperty 的get和set方法,首先简单介绍以下这个方法

    我们看到控制台打印出了这个对象的 key 和 value:

     这时候,我们删除这个 name :

            let obj = {};
           Object.defineProperty( obj, 'name', {
               value: 'langkui'
           })
           delete obj.name;
           console.log(obj)    
    

      查看控制台,其实并没有删除:

    添加 configurable属性:

    let obj = {};
    Object.defineProperty( obj, 'name', {
        configurable: true,
        value: 'langkui'
    })
    delete obj.name;
    console.log(obj)

    我们发现 name 被删除了: 

    此时,注释掉删除 name 的代码,继续添加修改 name 属性的值

    let obj = {};
    Object.defineProperty( obj, 'name', {
        configurable: true,
        value: 'langkui'
    })
    // delete obj.name;
    obj.name = 'xiaoming';
    console.log(obj)

    打开控制台,我们发现 name 的值并没有被修改

    我们添加writable: true 的属性:

    let obj = {};
    Object.defineProperty( obj, 'name', {
        configurable: true,
        writable: true,
        value: 'langkui'
    })
    // delete obj.name;
    obj.name = 'xiaoming';
    console.log(obj)

    此时obj.name的值被修改了,

    我们试着循环obj: 

    let obj = {};
    Object.defineProperty( obj, 'name', {
        configurable: true,
        writable: true,
        value: 'langkui'
    })
    // delete obj.name;
    // obj.name = 'xiaoming';
    for(let key in obj) {
        console.log(obj[key])
    }
    console.log(obj)

    但是控制台什么也没有输出;

    添加 enumerable: true 属性后, 控制台显示执行了循环

    let obj = {};
    Object.defineProperty( obj, 'name', {
        configurable: true,
        writable: true,
        enumerable: true,
        value: 'langkui'
    })
    // delete obj.name;
    // obj.name = 'xiaoming';
    for(let key in obj) {
        console.log(obj[key])
    }
    console.log(obj)

    我们还可以给Object.defineProperty 添加 get 和 set 的方法:

    let obj = {};
    Object.defineProperty( obj, 'name', {
        configurable: true,
        // writable: true,
        enumerable: true,
        get() {
            console.log('正在获取name的值')
            return 'langming'
        },
        set(newVal) {
            console.log(`正在设置name的值为${newVal}`)
        }
    })
    // delete obj.name;
    // obj.name = 'xiaoming';
    for(let key in obj) {
        console.log(obj[key])
    }
    console.log(obj)

    然后我们试着在控制台改变 name 的值为100

     这些就是Object.defineProperty一些常用设置。

    接下来我们用它来实现一个简单的mvvm:

    有如下一个简单的看似很像vue的东西:

    <!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>Document</title>
    </head>
    <body>
        <div id="app">
            {{a}}
        </div>
        <script src="1.js"></script>
        <script>
        // 数据劫持 Observe
           let vue = new Vue({
               el: 'app',
               data: {
                   a: 1,
               }
           });
        </script>
    </body>
    </html>

    首先我们创建一个Vue的构造函数,并把_data和$options作为他的属性,同时我们希望有个一observe的函数来监听_data的变化,在_data发生变化的时候我们修改Vue构造函数上添加一个对应相同key的属性的值并且同时监听这个新的key的值的变化:

    function Vue( options = {} ) {
        this.$options = options;
        // this._data;
        var data = this._data = this.$options.data;
        // 监听 data 的变化
        observe(data);
        // 实现代理  this.a 代理到 this._data.a
        for(let name in data) {
            Object.defineProperty( this, name, {
                enumerable: true,
                get() {
                    // this.a 获取的时候返回 this._data.a
                    return this._data[name];   
                },
                set(newVal) {
                    // 设置 this.a 的时候相当于设置  this._data.a
                    this._data[name] = newVal;
                }
            })
        }
    }
    
    function Observe(data) {
        for(let key in data) {
            let val = data[key];
            observe(val)
            Object.defineProperty(data, key, {
                enumerable: true,
                get() {
                    return val;
                },
                set(newVal) {
                    if(newVal === val) {
                        return;
                    }
                    // 设置值的时候触发
                    val = newVal;
                    // 实现赋值后的对象监测功能
                    observe(newVal);
                }
            })
        }
    }
    
    // 观察数据,给data中的数据object.defineProperty
    function observe(data) {
        if(typeof data !== 'object') {
            return;
        }
        return new Observe(data);
    }

    我们在控制台查看vue 并且 修改 vue.a 的值为100 并再次查看 vue:

    接下来我们通过正则匹配页面上的{{}} 并且获取 {{}} 里面的变量 并把 vue上对应的key 替换进去 :

    function Vue( options = {} ) {
        this.$options = options;
        // this._data;
        var data = this._data = this.$options.data;
        // 监听 data 的变化
        observe(data);
        // 实现代理  this.a 代理到 this._data.a
        for(let name in data) {
            Object.defineProperty( this, name, {
                enumerable: true,
                get() {
                    // this.a 获取的时候返回 this._data.a
                    return this._data[name];   
                },
                set(newVal) {
                    // 设置 this.a 的时候相当于设置  this._data.a
                    this._data[name] = newVal;
                }
            })
        }
        // 实现魔板编译  
        new Compile(this.$options.el, this)
    }
    
    // el:当前Vue实例挂载的元素, vm:当前Vue实例上data,已代理到 this._data
    function Compile(el, vm) {
        // $el  表示替换的范围
        vm.$el = document.querySelector(el);
        let fragment = document.createDocumentFragment();
        // 将 $el 中的内容移到内存中去
        while( child = vm.$el.firstChild ) {
            fragment.appendChild(child);
        }
        replace(fragment);
        // 替换{{}}中的内容 
        function replace(fragment) {
            Array.from(fragment.childNodes).forEach( function (node) {
                let text = node.textContent;
                let reg = /{{(.*)}}/;
                // 当前节点是文本节点并且通过{{}}的正则匹配
                if(node.nodeType === 3 && reg.test(text)) {
                    console.log(RegExp.$1);  // a.a b
                    let arr = RegExp.$1.split('.');   // [a,a] [b]
                    let val = vm;
                    arr.forEach( function(k) {
                        // 循环层级
                        val = val[k];
                    })
                    // 赋值
                    node.textContent = text.replace(reg, val);
                }
                vm.$el.appendChild(fragment)
                // 如果当前节点还有子节点,进行递归操作
                if(node.childNodes) {
                    replace(node);
                }
            })
        }
    }
     
    function Observe(data) {
        for(let key in data) {
            let val = data[key];
            observe(val)
            Object.defineProperty(data, key, {
                enumerable: true,
                get() {
                    return val;
                },
                set(newVal) {
                    if(newVal === val) {
                        return;
                    }
                    // 设置值的时候触发
                    val = newVal;
                    // 实现赋值后的对象监测功能
                    observe(newVal);
                }
            })
        }
    }
    
    // 观察数据,给data中的数据object.defineProperty
    function observe(data) {
        if(typeof data !== 'object') {
            return;
        }
        return new Observe(data);
    }

    这时我们剩下要做的就是在data改变的时候进行一次页面更新, 此时需要提一下订阅发布模式:

    订阅模式其实就是就是一个队列,我们把需要执行的函数推进一个数组,在需要用的时候依次去执行这个数组中方法:

    // 发布订阅模式   先订阅 再有发布   一个数组的队列  [fn1, fn2, fn3]
    
    // 约定绑定的每一个方法,都有一个update属性
    function Dep() {
        this.subs = [];
    }
    Dep.prototype.addSub = function (sub) {
        this.subs.push(sub);
    }
    
    Dep.prototype.notify = function () {
        this.subs.forEach( sub => sub.update());
    }
    
    // Watch是一个类,通过这个类创建的实例都有update的方法ßß
    function Watcher (fn) {
        this.fn = fn
    }
    Watcher.prototype.update = function() {
        this.fn();
    }
    
    let watcher = new Watcher( function () {
        console.log('开始了发布');
    })
    
    let dep = new Dep();
    dep.addSub(watcher);
    dep.addSub(watcher);
    console.log(dep.subs);
    dep.notify();     //  订阅发布模式其实就是一个数组关系,订阅就是讲函数push到数组队列,发布就是以此的执行这些函数 

    执行这个文件:

    这个就是简单的订阅发布模式,我们把这个应用到们的mvvm中,在数据改变的时候进行实时的更新页面操作:

    function Vue( options = {} ) {
        this.$options = options;
        // this._data;
        var data = this._data = this.$options.data;
        // 监听 data 的变化
        observe(data);
        // 实现代理  this.a 代理到 this._data.a
        for(let name in data) {
            Object.defineProperty( this, name, {
                enumerable: true,
                get() {
                    // this.a 获取的时候返回 this._data.a
                    return this._data[name];   
                },
                set(newVal) {
                    // 设置 this.a 的时候相当于设置  this._data.a
                    this._data[name] = newVal;
                }
            })
        }
        // 实现魔板编译  
        new Compile(this.$options.el, this)
    }
    
    // el:当前Vue实例挂载的元素, vm:当前Vue实例上data,已代理到 this._data
    function Compile(el, vm) {
        // $el  表示替换的范围
        vm.$el = document.querySelector(el);
        let fragment = document.createDocumentFragment();
        // 将 $el 中的内容移到内存中去
        while( child = vm.$el.firstChild ) {
            fragment.appendChild(child);
        }
        replace(fragment);
        // 替换{{}}中的内容 
        function replace(fragment) {
            Array.from(fragment.childNodes).forEach( function (node) {
                let text = node.textContent;
                let reg = /{{(.*)}}/;
                // 当前节点是文本节点并且通过{{}}的正则匹配
                if(node.nodeType === 3 && reg.test(text)) {
                    // RegExp $1-$9 表示 最后使用的9个正则
                    console.log(RegExp.$1);  // a.a b
                    let arr = RegExp.$1.split('.');   // [a,a] [b]
                    let val = vm;
                    arr.forEach( function(k) {
                        // 循环层级
                        val = val[k];
                    })
                    // 赋值
                    new Watcher( vm, RegExp.$1, function(newVal) {
                        node.textContent = text.replace(reg, newVal);
                    })
                    node.textContent = text.replace(reg, val);
                }
                vm.$el.appendChild(fragment)
                // 如果当前节点还有子节点,进行递归操作
                if(node.childNodes) {
                    replace(node);
                }
            })
        }
    }
     
    function Observe(data) {
        // 开启订阅发布模式
        let dep = new Dep();
        for(let key in data) {
            let val = data[key];
            observe(val)
            Object.defineProperty(data, key, {
                enumerable: true,
                get() {
                    Dep.target && dep.addSub(Dep.target);
                    return val;
                },
                set(newVal) {
                    if(newVal === val) {
                        return;
                    }
                    // 设置值的时候触发
                    val = newVal;
                    // 实现赋值后的对象监测功能
                    observe(newVal);
                    // 让所有的watch的update方法都执行
                    dep.notify();
                }
            })
        }
    }
    
    // 观察数据,给data中的数据object.defineProperty
    function observe(data) {
        if(typeof data !== 'object') {
            return;
        }
        return new Observe(data);
    }
    
    // 发布订阅模式
    function Dep() {
        this.subs = [];
    }
    Dep.prototype.addSub = function (sub) {
        this.subs.push(sub);
    }
    
    Dep.prototype.notify = function () {
        this.subs.forEach( sub => sub.update());
    }
    
    // watcher
    function Watcher (vm, exp, fn) {
        this.vm = vm;
        this.exp = exp;
        this.fn = fn
        // 将watch添加到订阅中
        Dep.target = this;
        let val = vm;
        let arr = exp.split('.');
        arr.forEach(function (k) {   // 取值,也就是取 this.a.a/this.b 此时会调用 Object.defineProperty的get的方法
            val = val[k];
        });
        Dep.target = null;
    }
    Watcher.prototype.update = function() {
        let val = this.vm;
        let arr = this.exp.split('.');
        arr.forEach( function (k) {
            val = val[k];
        })
        // 需要传入newVal
        this.fn(val);
    }

    在控制台修改数据页面出现了更新:

    一个简单的mvvm就实现了。

    源码已经放到了我的github: https://github.com/Jasonwang911/vueMVVM   如果对你有帮助,可以star~~

  • 相关阅读:
    第十一篇:Mysql系列
    mysql八:ORM框架SQLAlchemy
    mysql七:视图、触发器、事务、存储过程、函数
    mysql六:数据备份、pymysql模块
    工厂方法模式
    execution表达式
    CentOS系统下安装SVN及常用命令
    Spring Boot 表单验证、AOP统一处理请求日志、单元测试
    SSH文件上传代码片段
    JPA 实体映射
  • 原文地址:https://www.cnblogs.com/jasonwang2y60/p/9398710.html
Copyright © 2011-2022 走看看