仿vue使用Proxy实现双向绑定

本来是仿vue2的使用Object.defineProperty来实现的双向绑定,但是后来看到vue3提到用Proxy代替了defineProperty,便使用Proxy修改了一番
相比于defineProperty劫持对象属性的方式,Proxy则是在对象操作之前进行了一个拦截,返回一个操作对象的代理对象来间接的的操作对象

示例如下

    var handler = {
        get: function (target, key, recevier) {
            //Reflect为es6提供的对对象操作的新对象,包含一些对对象操作的方法
            // 此处Reflect.get()为获取对象属性的方法
            return Reflect.get(target, key, recevier);
        },
        //属性被修改时
        set: function (target, key, val, recevier) {
            //Reflect.set()为设置对象属性方法
            Reflect.set(target, key, val, recevier);
        }
    }

//代理对象
var data = {a:1,b:2}
var proxyData =  new Proxy(data, handler);

为什么使用Reflect而不是直接获取属性值,请查看Reflect更多用法

使用proxy,监听对象属性
/**
 * 使用proxy,监听对象属性
 */

function defineReactive(data) {
    var dep = new Dep();
    var handler = {
        get: function (target, key, recevier) {
            if (Dep.target) {
                addDep(target, Dep.target, key);
            }
            // 多层对象,面对多层对象时Proxy需要做个判断并返回一个新的Proxy才能对子对象进行监听
            if (target[key] && typeof target[key] === 'object') {
                return new Proxy(target[key], handler);
            }
            return Reflect.get(target, key, recevier);
        },
        //属性被修改时
        set: function (target, key, val, recevier) {
            Reflect.set(target, key, val, recevier);
            target.Dep.notify(key);
        }
    }
    return new Proxy(data, handler);
}

此处和使用defineProperty差不多,只是不必再去遍历整个data对象

监听器,对对象所有属性进行监听,当有变动时,通知到订阅者
function observe(data) {
    if (!data || typeof data !== "object") {
        return data;
    }
    return defineReactive(data);
}

/**
 * 使用proxy,监听对象属性
 */
function Dep() {
    this.subs = [];
}
Dep.target = null;

Dep本为保存所有订阅者的函数,但是我发现,每次单个属性有变化时,所有订阅者都会被通知到,从而执行一遍所有订阅者方法,所以此处修改了下,在被监听对象上添加了一个保存所有订阅者的对象,这样在有多层对象时就能把每个订阅者和所相关对象进行了一个关联,而不是所有订阅者都保存在一个集合中,具体代码如下:

//把监听对象改变时的动作保存到监听对象上
function addDep(obj, target, key) {
    if (obj.Dep) {
        obj.Dep.addSub(key, target);
    } else {
        Object.defineProperty(obj, 'Dep', {
            value: {
                // 此处使用对象而不是数组,是为了进一步把订阅者和监听的属性对应起来
                // 这样每次监听的属性有变动时,只需通知相关订阅者就行
                subs: {
                    [key]: [target]
                },
                // 添加订阅者方法
                addSub: function (k, sub) {
                    this.subs[k] = this.subs[k] || [];
                    this.subs[k].push(sub);
                    console.log(this.subs);
                },
                // 监听属性变动时,通知相关订阅者
                notify: function (k) {
                    this.subs[k].forEach(function (sub) {
                        sub.update();
                    })
                }
            },
            enumerable: false,
            configurable: false, //不可枚举
        });
    }
}
订阅者方法
function Watcher(target, exp, cb) {
    this.target = target;
    this.exp = exp;
    this.cb = cb;
    this.value = this.get();
}
Watcher.prototype = {
    update: function () {
        this.run();
    },
    run: function () {
        var value = this.target[this.exp];
        var oldValue = this.value;
        if (oldValue !== value) {
            this.value = value;
            this.cb.call(this.vm, value, oldValue);
        }
    },
    get: function () {
        // 保存当前订阅者本身,并手动触发监听对象变化,以便添加当前订阅者
        Dep.target = this;
        var value = this.target[this.exp];
        Dep.target = null;
        return value;
    }
}

指令解析器

function Compile(el, vm) {
    this.vm = vm;
    this.el = document.querySelector(el);
    this.fragment = null;
    this.init();
}
Compile.prototype = {
    init: function () {
        if (this.el) {
            this.fragment = this.nodeToFragment(this.el);
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        }
    },
    // v-model 指令处理方法
    compile: function (node) {
        var nodeAttrs = node.attributes;
        var self = this;
        Array.prototype.forEach.call(nodeAttrs, function (attr) {
            var attrName = attr.name;
            if (self.isDirective(attrName)) {
                var exp = attr.value;
                var dir = attrName.substring(2);
                self.compileModel(node, self.vm, exp, dir);
            }
        });
    },
/**
* 创建一个虚拟dom,并把当前作用dom的内容添加到虚拟dom中
*/
    nodeToFragment: function (el) {
        // 创建虚拟dom
        var fragment = document.createDocumentFragment();
        var child = el.firstChild;
        while (child) {
            // 把原dom结构添加到虚拟dom中
            fragment.appendChild(child);
            child = el.firstChild;
        }
        return fragment;
    },
    //遍历节点,对包含有指令的节点进行特殊处理
    compileElement: function (el) {
        var childNodes = el.childNodes;
        var self = this;
        [].slice.call(childNodes).forEach(function (node) {
            var reg = /\{\{[a-zA-Z0-9\.]*\}\}/g;
            var text = node.textContent;
            // 判断是否为v-model指令
            if (self.isElementNode(node)) {
                self.compile(node);
            } else if (self.isTextNode(node) && reg.test(text)) {// 判读是否为{{}}指令
                var exps = text.match(reg).map(function (exp) {
                    return exp.replace(/\{\{|\}\}/g, '');
                });
                self.compileText(node, exps);
            }
            if (node.childNodes && node.childNodes.length) {
                self.compileElement(node);
            }
        });
    },
    // 解析指令,根据指令把监听数据和dom对应起来,添加订阅者
    compileText: function (node, exps) {
        var self = this;
        var initText = {};
        exps.forEach(function (exp) {
            var expInfo = self.handlerExps(exp);
            console.log(expInfo);
            initText[exp] = typeof expInfo.value === 'object' ? JSON.stringify(expInfo.value) : expInfo.value;
            new Watcher(expInfo.target, expInfo.exp, function (value) {
                initText[exp] = typeof value === 'object' ? JSON.stringify(value) : value;
                self.updateText(node, Object.values(initText).join(' '));
            });
        });
        self.updateText(node, Object.values(initText).join(' '));
    },
    compileModel: function (node, vm, exp, dir) {
        console.log(node, vm, exp, dir);
        var self = this;
        self.compileText(node, [exp]);
    },
    updateText: function (node, value) {
        node.textContent = typeof value === 'undefined' ? '' : value;
    },
    isTextNode: function (node) {
        return node.nodeType === 3;
    },
    isDirective: function (attr) {
        return attr.indexOf('v-') === 0;
    },
    isElementNode(node) {
        return node.nodeType === 1;
    },
    // 处理指令
    handlerExps: function (exps) {
        var self = this;
        var expInfo = {
            target: self.vm,
            input: exps,
            exp: exps,
            value: self.vm[exps]
        };
        if (exps.indexOf('.') !== -1) {// 指令的值为多层级时,例:a.b.v
            var expAry = exps.split('.');
            expInfo.exp = expAry[expAry.length - 1];
            expAry.slice(0, -1).forEach(function (exp) {
                if (Object.prototype.toString.call(expInfo.target) === "[object Object]") {
                    expInfo.target = expInfo.target[exp];
                } else {
                    expInfo.target = undefined
                }
            });
            expInfo.value = expInfo.target[expInfo.exp];
        }
        return expInfo;
    }
}

整合以上方法

function SelfVue(opt) {
    var self = this;
    this.data = observe(opt.data);
    Object.keys(this.data).forEach(function (key) {
        self.proxyKey(key);
    })
    new Compile(opt.el, this);
}
SelfVue.prototype = {
    /**
    * data中方法代理到实例中,使之能通过this.[属性名]访问
    */
    proxyKey: function (key) {
        var self = this;
        Object.defineProperty(self, key, {
            enumerable: false, 
            configurable: true,
            get: function () {
                return Reflect.get(self.data,key);
            },
            set: function (newValue) {
                Reflect.set(self.data,key,newValue);
            }
        });
    }
}

使用示例

var data = {
            name:"小明",
            age:18,
            fun:function(){
                console.log(123);
            },
            children:{
                name:"小李",
                age:""
            },
            childrens:[
                {
                    name:"",
                    age:""
                }
            ]
        }
        var vue = new SelfVue({
            data:data,
            el:"#app"
        });
        setTimeout(function(){
            vue.age = '小碗'
        },2000);

以上本人对双向绑定的实现,有不合理错误之处,或可改进之处请大家多多留言,相互学习共同进步

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容