欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

使用Proxy和defineProperty分别构建一款MVVM框架

程序员文章站 2022-07-12 21:55:00
...

导读

这些天呢,作为前端界比较火的一件事情就是,vue 3.0的诞生,vue 3.0除了在用法上有些许变化外,最主要的变化,莫过于数据劫持的方式的改变;vue 3.0使用的是es6Proxy进行数据拦截的,而2.x的版本呢,则是采用的Object.defineProperty()这样的方式进行对数据的监听,所以呢,今天我们做个实验,什么样的实验呢?我们分别来使用这个ProxydefineProperty来造一个类似于简单版的vue这种的mvvm框架*,好,接下来呢,我们开发阶段。
首先把这个ProxydefineProperty文档的地址给贴上:

使用defineProperty开发的MVVM框架


// 发布订阅Dep
class Dep{
    constructor(){
        this.subs = [];
    }

    // 添加订阅
    addSub(watcher){
        this.subs.push(watcher);
    }

    //通知执行
    notify(){
        this.subs.forEach(watcher => watcher.update())
    }
}

// 观察者 Watcher 类实现
class Watcher{
    constructor(vm, exp, callback){
        this.vm = vm;
        this.exp = exp;
        this.callback = callback;

        // 获取更改前的值
        this.oldValue = this.get();
    }

    get(){
        // 将当前的 watcher 添加到 Dep 类的静态属性上
        Dep.target = this;

        // 获取data上的值
        let val = CompileUtil.getVal(this.vm, this.exp);

        // 清空 Dep 上的 Watcher,防止重复添加
        Dep.target = null;
        return val;
    }

    update(){
       // 获取新的值
       let newValue = CompileUtil.getVal(this.vm, this.exp);
       // 获取旧的值
       let oldValue = this.oldValue;
       // 如果新值和旧值不相等,就执行 callback 对 dom 进行更新
       if(newValue !== oldValue){
           this.callback(newValue);
       }
    }
}

// 模板编译类
class Compile{
    constructor(el, vm){
        // 获取元素
        this.el = this.isElementNode(el) ? el: document.querySelector(el);
        // 获取mwwm实例
        this.vm = vm;

        // 如果元素存在 才开始编译
        if(this.el){
            let fragment = this.nodeToFragment(this.el);
            
            // 1. 把模板中的指令中的变量和{{}}中的变量替换成真实的数据
            this.compileTemplate(fragment);

            //2. 把编译好的 fragment 再塞回页面中
            this.el.appendChild(fragment);
        }
    }

    // 检测是否为元素节点、
    isElementNode(el){
        return el.nodeType === 1;
    }

    // 核心 将根节点转换成内存文档碎片
    nodeToFragment(el){
        let fragment = document.createDocumentFragment();
        let firstChild;

        // 循环取出节点 存放在我们的文档碎片中
        while(firstChild = el.firstChild){
            fragment.appendChild(firstChild);
        }
        return fragment;
    }

    // 解析文档碎片 编译模板中的变量
    compileTemplate(fragment){
        // 获取所有子节点(包括元素节点和文本节点)
        let childNodes = fragment.childNodes;

        //循环遍历每个节点
        Array.from(childNodes).forEach(node => {
            // 如果是元素节点
            if(this.isElementNode(node)){
                // 递归遍历子节点
                this.compileTemplate(node);
                // 处理元素节点
                this.compileElement(node);
            }
            // 如果是文本节点
            else{
                this.compileText(node);
            }
        });
    }

    // 编译处理元素节点
    compileElement(node){
        
        //取出所有属性
        let attrs = node.attributes;
        // 遍历属性 检测是否具备`v-`
        Array.from(attrs).forEach(attr => {
            // console.log(attr)
            let attrName = attr.name;
            if(attrName.includes('v-')){
                // 如果是v-attr指令,去到该属性值对应的变量exp在data里面的值
                let exp = attr.value;
                // 取出方法名
                let [, type] = attrName.split("-");
                // 调用指令对应得方法 渲染页面数据
                CompileUtil[type](node, this.vm, exp);
            }
        })
    }

    // 编译处理文本节点
    compileText(node){
        // 获取文本节点内容
        let exp = node.textContent;
        let re = /{{([^}]+)}}/g;

        if(re.test(exp)){
            // 渲染页面数据
            CompileUtil['text'](node, this.vm, exp);
        }
    }
}

// 数据劫持类
class Observer{
    constructor(data){
        this.observe(data);
    }

    // 添加数据监听
    observe(data){
        // 验证data是否为对象
        if(!data || typeof data !== 'object'){
            return 
        }

        // 对data里面的数据进行深度遍历 一一劫持
        Object.keys(data).forEach(key => {
            // 实现数据响应劫持
            this.defineReactive(data, key, data[key]);
            // 递归劫持
            this.observe(data[key]);
        });

    }

    //数据响应绑定
    defineReactive(data, key, value){
        let self = this;
        // 每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
        let dep = new Dep();
        
        //监听每一个key
        Object.defineProperty(data, key, {
            configurable: true,
            enumerable: true,
            get(){ // 取值调用
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set(newValue){
                // console.log(newValue)
                if(newValue !== value){
                    self.observe(newValue); // 重新赋值与劫持
                    console.log(newValue)
                    value = newValue;
                    dep.notify(); // 通知数据更新
                }
            }
        })

    }
}

// 初始化类
class Mvvm{
    constructor(opts){
        // 获取元素和数据
        this.$el = opts.el;
        this.$data = opts.data;

        // 判断是否有模板 如果有模板 就执行编译
        if(this.$el){
            //1. 数据劫持
            new Observer(this.$data);
            //2. 编译模板
            new Compile(this.$el, this);
        }
    }
}


// 存储着所有的指令方法及指令对应的更新方法
let CompileUtil = {
    // 更新节点数据的方法
    updater: {
        // 文本更新
        textUpdater(node, v){
            node.textContent = v;
        },
        // 输入框更新
        modelUpdater(node, v){
            node.value = v;
        }
    },
    // 获取data里面的值
    getVal(vm, exp){
        // 分隔对象引用.
        exps = exp.replace(/\s*/g,'').split('.');
        console.log(vm.$data.me.message);
        return exps.reduce((prev, next) => {
            return prev[next];
        }, vm.$data);
    },
    // 获取文本 {{ msg }} 中 msg 在 data 里面的值
    getTextVal(vm, exp){
        return exp.replace(/{{([^}]+)}}/g,(...args) => {
            let data = this.getVal(vm, args[1]);
            // console.log(data)
            return data;
        })
    },
    // 设置data值的方法、
    setVal(vm, exp, newVal){
        
        // 分隔对象引用.
        exps = exp.split('.');
        
        exps.reduce((prev, next, currentIndex) => {
            // 如果当前归并的为数组的最后一项,则将新值设置到该属性
            if(currentIndex === exps.length -1){
                prev[next] = newVal;
                return;
            }
            // 未到最后一个属性 继续归并
            return prev[next];
        },vm.$data);
        console.log(vm.$data)
    },
    // 处理 v-model 指令的方法
    model(node, vm, exp){
        
        exp = exp.replace(/\s*/g,'');

        // 1. 获取赋值的方法
        let updateFn = this.updater.modelUpdater
        // 2. 获取data中的对应变量的值
        let val = this.getVal(vm, exp);
        
        // 4. 监听变化决定是否更新视图
        new Watcher(vm, exp, newValue => {
            updateFn && updateFn(node, newValue);
        })

        // 5. 给node元素添加input事件监听
        node.addEventListener('input', e => {
            //获取输入的值
            let newVal = e.target.value;
            // 更新到data数据上
            this.setVal(vm, exp, newVal);
        });

        // 3. 初始化设置值
        updateFn&&updateFn(node, val);
    },
    // 处理文本节点上的 {{}} 方法
    text(node, vm, exp){
        
        exp = exp.replace(/\s*/g,'');

        // 获取赋值的方法
        let updateFn = this.updater.textUpdater;

        // 获取 data 中对应的变量的值
        let value = this.getTextVal(vm, exp);
        
        // console.log(value)
        // 通过正则替换,将取到数据中的值替换掉 {{ }}
        exp.replace(/{{([^}]+)}}/g, (...args) => {
            // 解析时遇到了模板中需要替换为数据值的变量时,应该添加一个观察者
            // 当变量重新赋值时,调用更新值节点到 Dom 的方法
            new Watcher(vm, args[1], newValue => {
                // 如果数据发生变化,重新获取新值
                updateFn && updateFn(node, newValue);
            });
        });

        // 第一次设置值
        updateFn && updateFn(node, value);
    }
};

使用Proxy开发的MVVM框架


// 发布订阅Dep
class Dep{
    constructor(){
        this.subs = [];
    }

    // 添加订阅
    addSub(watcher){
        this.subs.push(watcher);
    }

    //通知执行
    notify(){
        this.subs.forEach(watcher => watcher.update())
    }
}

// 观察者 Watcher 类实现
class Watcher{
    constructor(vm, exp, callback){
        this.vm = vm;
        this.exp = exp;
        this.callback = callback;

        // 获取更改前的值
        this.oldValue = this.get();
    }

    get(){
        // 将当前的 watcher 添加到 Dep 类的静态属性上
        Dep.target = this;

        // 获取data上的值
        let val = CompileUtil.getVal(this.vm, this.exp);

        // 清空 Dep 上的 Watcher,防止重复添加
        Dep.target = null;
        return val;
    }

    update(){
       // 获取新的值
       let newValue = CompileUtil.getVal(this.vm, this.exp);
       // 获取旧的值
       let oldValue = this.oldValue;
    //    console.log(this.vm, newValue, oldValue, newValue !== oldValue)
       // 如果新值和旧值不相等,就执行 callback 对 dom 进行更新
       if(newValue !== oldValue){
           this.callback(newValue);
       }
    }
}

// 模板编译类
class Compile{
    constructor(el, vm){
        // 获取元素
        this.el = this.isElementNode(el) ? el: document.querySelector(el);
        // 获取mwwm实例
        this.vm = vm;
        console.log(vm)

        // 如果元素存在 才开始编译
        if(this.el){
            let fragment = this.nodeToFragment(this.el);
            
            // 1. 把模板中的指令中的变量和{{}}中的变量替换成真实的数据
            this.compileTemplate(fragment);

            //2. 把编译好的 fragment 再塞回页面中
            this.el.appendChild(fragment);
        }
    }

    // 检测是否为元素节点、
    isElementNode(el){
        return el.nodeType === 1;
    }

    // 核心 将根节点转换成内存文档碎片
    nodeToFragment(el){
        let fragment = document.createDocumentFragment();
        let firstChild;

        // 循环取出节点 存放在我们的文档碎片中
        while(firstChild = el.firstChild){
            fragment.appendChild(firstChild);
        }
        return fragment;
    }

    // 解析文档碎片 编译模板中的变量
    compileTemplate(fragment){
        // 获取所有子节点(包括元素节点和文本节点)
        let childNodes = fragment.childNodes;

        //循环遍历每个节点
        Array.from(childNodes).forEach(node => {
            // 如果是元素节点
            if(this.isElementNode(node)){
                // 递归遍历子节点
                this.compileTemplate(node);
                // 处理元素节点
                this.compileElement(node);
            }
            // 如果是文本节点
            else{
                this.compileText(node);
            }
        });
    }

    // 编译处理元素节点
    compileElement(node){
        
        //取出所有属性
        let attrs = node.attributes;
        // 遍历属性 检测是否具备`v-`
        Array.from(attrs).forEach(attr => {
            // console.log(attr)
            let attrName = attr.name;
            if(attrName.includes('v-')){
                // 如果是v-attr指令,去到该属性值对应的变量exp在data里面的值
                let exp = attr.value;
                // 取出方法名
                let [, type] = attrName.split("-");
                // 调用指令对应得方法 渲染页面数据
                CompileUtil[type](node, this.vm, exp);
            }
        })
    }

    // 编译处理文本节点
    compileText(node){
        // 获取文本节点内容
        let exp = node.textContent;
        let re = /{{([^}]+)}}/g;

        if(re.test(exp)){
            // 渲染页面数据
            CompileUtil['text'](node, this.vm, exp);
        }
    }
}

class Observer{
    
    constructor(data){
        // this.observe(data);
    }

    // 添加数据监听
    observe(data){
        // 验证data是否为对象
        if(!data || typeof data !== 'object'){
            return 
        }

        let self = this;
        // 每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
        let dep = new Dep();
        let handler = {
            get: function(target, key, receiver) {
                // 递归创建并返回
                if (typeof target[key] === 'object' && target[key] !== null) {
                    return new Proxy(target[key], handler);
                }
                if(typeof target === 'object' && target !== null){
                    Dep.target && dep.addSub(Dep.target);
                }
                // console.log(dep);
                return Reflect.get(target, key, receiver);
            },
            set: function(target, key, value, receiver) {
                console.log(target[key], value)
                if(target[key] !== value){
                    console.log('update')
                    target[key] = value;
                    dep.notify(); // 通知数据更新
                }
            }
        };
        // 
        let cdata = new Proxy(data, handler);
        return cdata;
    }
}



// 数据劫持类
class Observer2{
    constructor(data){
        this.observe(data);
    }

    // 添加数据监听
    observe(data){
        // 验证data是否为对象
        if(!data || typeof data !== 'object'){
            return 
        }

        // 对data里面的数据进行深度遍历 一一劫持
        Object.keys(data).forEach(key => {
            // 实现数据响应劫持
            this.defineReactive(data, key, data[key]);
            // 递归劫持
            this.observe(data[key]);
        });

    }

    //数据响应绑定
    defineReactive(data, key, value){
        let self = this;
        // 每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
        let dep = new Dep();
        
        //监听每一个key
        Object.defineProperty(data, key, {
            configurable: true,
            enumerable: true,
            get(){ // 取值调用
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set(newValue){
                // console.log(newValue)
                if(newValue !== value){
                    self.observe(newValue); // 重新赋值与劫持
                    console.log(newValue)
                    value = newValue;
                    dep.notify(); // 通知数据更新
                }
            }
        })

    }
}

// 初始化类
class Mvvm{
    constructor(opts){
        // 获取元素和数据
        this.$el = opts.el;
        this.$data = opts.data;

        // 判断是否有模板 如果有模板 就执行编译
        if(this.$el){
            //1. 数据劫持
            let obs = new Observer();
            this.$data = obs.observe(this.$data);

            //2. 编译模板
            new Compile(this.$el, this);
        }
    }
}


// 存储着所有的指令方法及指令对应的更新方法
let CompileUtil = {
    // 更新节点数据的方法
    updater: {
        // 文本更新
        textUpdater(node, v){
            node.textContent = v;
        },
        // 输入框更新
        modelUpdater(node, v){
            node.value = v;
        }
    },
    // 获取data里面的值
    getVal(vm, exp){
        // 分隔对象引用.
        exps = exp.replace(/\s*/g,'').split('.');
        let result = exps.reduce((prev, next) => {
            return prev[next];
        }, vm.$data)
        console.log(vm.$data.me.message)
        return result;
    },
    // 获取文本 {{ msg }} 中 msg 在 data 里面的值
    getTextVal(vm, exp){
        return exp.replace(/{{([^}]+)}}/g,(...args) => {
            let data = this.getVal(vm, args[1]);
            // console.log(data)
            return data;
        })
    },
    // 设置data值的方法、
    setVal(vm, exp, newVal){
        
        // 分隔对象引用.
        exps = exp.split('.');
        
        exps.reduce((prev, next, currentIndex) => {
            // 如果当前归并的为数组的最后一项,则将新值设置到该属性
            if(currentIndex === exps.length -1){
                prev[next] = newVal;
                return;
            }
            // 未到最后一个属性 继续归并
            return prev[next];
        }, vm.$data);
        console.log(vm.$data)
    },
    // 处理 v-model 指令的方法
    model(node, vm, exp){
        
        exp = exp.replace(/\s*/g,'');

        // 1. 获取赋值的方法
        let updateFn = this.updater.modelUpdater
        // 2. 获取data中的对应变量的值
        let val = this.getVal(vm, exp);
        
        // 4. 监听变化决定是否更新视图
        new Watcher(vm, exp, newValue => {
            updateFn && updateFn(node, newValue);
        })

        // 5. 给node元素添加input事件监听
        node.addEventListener('input', e => {
            // console.log(e.target.value)
            //获取输入的值
            let newVal = e.target.value;
            // 更新到data数据上
            this.setVal(vm, exp, newVal);
        });

        // 3. 初始化设置值
        updateFn&&updateFn(node, val);
    },
    // 处理文本节点上的 {{}} 方法
    text(node, vm, exp){
        
        exp = exp.replace(/\s*/g,'');

        // 获取赋值的方法
        let updateFn = this.updater.textUpdater;

        // 获取 data 中对应的变量的值
        let value = this.getTextVal(vm, exp);
        
        // console.log(value)
        // 通过正则替换,将取到数据中的值替换掉 {{ }}
        exp.replace(/{{([^}]+)}}/g, (...args) => {
            // 解析时遇到了模板中需要替换为数据值的变量时,应该添加一个观察者
            // 当变量重新赋值时,调用更新值节点到 Dom 的方法
            new Watcher(vm, args[1], newValue => {
                // 如果数据发生变化,重新获取新值
                updateFn && updateFn(node, newValue);
            });
        });

        // 第一次设置值
        updateFn && updateFn(node, value);
    }
};