仿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);

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容