Skip to content

Vue双向数据绑定实现 #18

Open
@isNeilLin

Description

@isNeilLin
<!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">
        <h2>{{title}}</h2>
        <h1>{{name}}</h1>
        <input type="text" v-model="name" v-on:blur="blur">
        <button v-on:click="clickMe">click me!</button>
    </div>
<script>
    // 监听器
    function observe(data){
        if(Object.prototype.toString.call(data)!=='[object Object]'){
            return;
        }
        Object.keys(data).forEach(key=>{
            defineReactive(data,key,data[key])
        })
    }
    function defineReactive(data,key,value){
        observe(value);
        var dep = new Dep();
        Object.defineProperty(data,key,{
            enumerable: true,
            configurable: true,
            get: function(){
                if(Dep.target){
                    dep.addSub(Dep.target); // 添加一个订阅者
                }
                return value;
            },
            set: function(newVal){
                if(newVal === value){
                    return;
                }
                console.log('value changed,\nthe old value is '+ value + ',\nthe new value is ' + newVal);
                value = newVal;
                dep.notify(); // 如果数据变化,通知所有订阅者
            }
        })
    }
    
    // 消息订阅器
    function Dep(){
        this.subs = [];
    }
    Dep.prototype = {
        constructor: Dep,
        addSub: function(sub){
            this.subs.push(sub);
        },
        notify: function(){
            this.subs.map(sub=>{
                sub.update();
            })
        }
    }

    // 订阅器
    function Watcher(vm,exp,cb){
        this.vm = vm;
        this.exp = exp;
        this.cb = cb;
        this.value = this.get();
    }
    Watcher.prototype = {
        constructor: Watcher,
        get: function(){
            Dep.target = this;
            var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
            Dep.target = null;
            return value;
        },
        update: function(){
            this.run();
        },
        run: function(){
            var value = this.vm.data[this.exp];
            var oldVal = this.value;
            if(value!==oldVal){
                this.value = value;
                this.cb.call(this.vm,value,oldVal);
            }
        }
    }
    
    // 解析器
    /* 将需要解析的dom节点存入fragment片段 */
    function Compile(el,vm){
        this.vm = vm;
        console.log(this.vm);
        this.el = document.querySelector(el);
        this.fragment = null;
        this.init();
    }
    Compile.prototype = {
        constructor: Compile,
        init: function(){
            if(!this.el){
                return;
            }
            this.fragment = this.nodeToFragment(this.el);
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        },
        nodeToFragment: function(el){
            var fragment = document.createDocumentFragment();
            var child = el.firstChild;
            while(child){
                fragment.appendChild(child);
                child = el.firstChild;
            }
            return fragment;
        },
        compileElement: function(el){
            var childNodes = el.childNodes;
            var self = this;
            [].slice.call(childNodes).forEach(child=>{
                var text = child.textContent;
                var reg = /\{\{(.*)\}\}/;
                if(self.isElementNode(child)){
                    self.compileDirective(child);
                }
                if(self.isTextNode(child) && reg.test(text)){
                    self.compileText(child, reg.exec(text)[1]);
                }
                if(child.childNodes && child.childNodes.length){
                    self.compileElement(child);
                }
            })
        },
        compileDirective: function(node){
            var attrs = node.attributes;
            var self = this;
            [].slice.call(attrs).forEach(attr=>{
                var name = attr.name;
                if(self.isDirective(name)){
                    var exp = attr.value;
                    var dir = name.substring(2);
                    if(self.isEventDirentive(dir)){ // 事件指令
                        console.log('event')
                        self.compileEvent(node,self.vm,exp,dir)
                    }else{ // v-model指令
                        self.compileModel(node,self.vm,exp,dir)
                    }
                    node.removeAttribute(name);
                }
            })
        },
        compileText: function(node,exp){
            var self = this;
            var initText = this.vm[exp];
            this.updateText(node,initText);
            new Watcher(this.vm,exp,function(value){
                self.updateText(node,value)
            })
        },
        compileEvent: function(node,vm,exp,dir){
            var eventName = dir.split(':')[1];
            var cb = this.vm.methods && this.vm.methods[exp];
            if(eventName && cb){
                console.log('bind event')
                node.addEventListener(eventName,cb.bind(vm),false);
            }
        },
        compileModel: function(node,vm,exp,dir){
            var self = this;
            var val = this.vm[exp];
            this.modelUpdater(node, val);
            new Watcher(this.vm,exp,function(value){
                self.modelUpdater(node,value);
            })
            node.addEventListener('input',function(e){
                var newValue = e.target.value;
                if(newValue===val){
                    return
                }
                self.vm[exp] = newValue;
                val = newValue;
            })
        },
        updateText: function(node,text){
            node.textContent = typeof text === 'undefined' ? '' : text;
        },
        modelUpdater: function(node,text){
            node.value = typeof text === 'undefined' ? '' : text;
        },
        isTextNode: function(node){
            return node.nodeType === 3;
        },
        isElementNode: function(node){
            return node.nodeType === 1;
        },  
        isDirective: function(directive){
            return directive.indexOf('v-')===0;
        },
        isEventDirentive: function(directive){
            return directive.indexOf('on:')===0;
        }
    }

    function SelfVue(options){
        var self = this;
        this.data = options.data;
        this.methods = options.methods;
        Object.keys(this.data).forEach(key=>{
            self.proxyKey(key);
        })
        observe(this.data);
        new Compile(options.el,this);
        options.mounted.call(this);
    }
    SelfVue.prototype.proxyKey = function(key){
        var self = this;
        Object.defineProperty(self,key,{
            get: function(){
                return self.data[key];
            },
            set: function(newVal){
                self.data[key] = newVal;
            }
        })
    }
    var selfVue = new SelfVue({
        el: '#app',
        data: {
            title: 'hello world',
            name: 'canfoo'
        },
        methods: {
            clickMe: function () {
                this.title = 'hello world';
            }
        },
        mounted: function () {
            window.setTimeout(() => {
                this.title = '你好';
            }, 1000);
        }
    })
</script>
</body>
</html>

Metadata

Metadata

Assignees

No one assigned

    Labels

    框架&工具库Vue和React等前端框架或工具库相关

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions