JS实现MVVM

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <input type="text" value="" v-model="user.name"/>
        {{user.name}}哈哈{{user.sex}}
        <select v-model="selected">
            <option value="">请选择学校</option>
            <option value="a">学校A</option>
            <option value="b">学校B</option>
        </select>
        <div>
            <div style="display: inline-block;">性别</div>
            <div>
                <label for="man">男:</label>
                <input id="man" type="radio" v-model="user.sex" value="man" name="sex">
                <label for="woman">女:</label>
                <input id="woman" type="radio" v-model="user.sex" value="woman" name="sex">
            </div>
        </div>
    </div>
    <script src="vm.js"></script>
    <script>
        let vm = new Vue({
            el:'#app'
            ,data:{
                user:{
                    name:'杨文宇'
                    ,sex:'man'
                }
                ,selected:'a'
            }
        })
    </script>
</body>
</html>
class Vue{
    constructor(options){
        this.$el = Vue.utils.getEl(options.el);
        this.$data = options.data;
        new Observe(this,this.$data);
        new Compile(this,this.$el);
    }

    static utils = {
        getEl(selector){
            return selector.nodeType == Node.ELEMENT_NODE ? selector : document.querySelector(selector)
        }
        ,isDirect(attrName){
            return /^v-/.test(attrName)
        }
        ,isTxtTpl(node){
            return node.nodeType == Node.TEXT_NODE && /^s*{{.*}}s*$/.test(node.textContent)
        }
        ,getVal(data,expOrFn){
            return expOrFn.split(".").reduce((data,cur)=>{
                return data[cur];
            },data);
        }
        ,setVal(data,expOrFn,value){
            expOrFn.split(".").reduce((data,cur,index,arr)=>{
                if(index == arr.length-1){
                    return data[cur] = value;
                }
                return data[cur];
            },data);
        }
    }
}

//劫持类
class Observe{
    constructor(vm,data){
        this.observer(data);
        //创建代理
        this.dataProxy(vm,data);
    }
    //对数据进行劫持
    observer(data){
        Object.keys(data).forEach(key=>{
            this.reactive(data,key,data[key])
        })
    }
    //响应式函数
    reactive(obj,key,value){
        typeof value == 'object' && this.observer(value)
        let _self = this
        //为每一个属性创建依赖容器,因为有可能一个属性会被多个地方依赖
        , dep = new Dep();
        Object.defineProperty(obj,key,{
            get(){
                //添加订阅者
                dep.depend();
                return value;
            }
            ,set(newVal){
                if(value == newVal){return}
                typeof newVal == 'object' && _self.observer(newVal);
                value = newVal;
                dep.notify();
            }
        })
    }

    dataProxy(obj,data){
        Object.keys(data).forEach(key=>{
            //let value = data[key];
            typeof value == 'object' && this.dataProxy(obj,value)
            Object.defineProperty(obj,key,{
                get(){
                    return data[key]
                }
                ,set(newVal){
                    data[key] = newVal;
                }
            })
        })
    }
}

class Compile{

    constructor(vm,el){
        this.el = el;
        this.vm = vm;
        this.ready();
    }

    static tagAttrEvent = {
        text:['value','input']
        ,textarea:['value','input']
        ,checkbox:['checked','change']
        ,radio:['checked','change']
        ,'select-one':['value','change']
        ,'select-multiple':['value','change']
    }

    //指令集
    static directSet = {
        model(vm,node,expOrFn){
            let tag = Compile.tagAttrEvent[node.type];
            new Watcher(vm,expOrFn,newVal=>{
                node[tag[0]] = newVal;
            });
            node[tag[0]] = Vue.utils.getVal(vm.$data,expOrFn);
            node.addEventListener(tag[1],e=>{
                Vue.utils.setVal(vm.$data,expOrFn,e.target[tag[0]])
            })
        }
        ,text(vm,node,expOrFn){
            let content = expOrFn.replace(/{{(.+?)}}/g,(...arg)=>{
                new Watcher(vm,arg[1],newVal=>{
                    node.textContent = this.getContentVal(vm,expOrFn);
                })
                return Vue.utils.getVal(vm.$data,arg[1])
            })
            node.textContent = content;
        }
        ,getContentVal(vm,expOrFn){
            return expOrFn.replace(/{{(.+?)}}/g,(...arg)=>{
                let value = Vue.utils.getVal(vm.$data,arg[1]) 
                return typeof value == 'object'? JSON.stringify(value) : value;
            })
        }
    }

    ready(){
        let fragment = this.node2fragment(this.el)
        this.compiler(fragment);
        this.el.appendChild(fragment);
    }

    //编译
    compiler(mountEl){
        let childNodes = [...mountEl.childNodes];
        childNodes.forEach(node=>{
            //获取不是文本节点或不是空文本的节点
            if(node.nodeType == Node.ELEMENT_NODE){
                [...node.attributes].forEach(attr=>{
                    let {name,value:expOrFn} = attr;
                    if(!Vue.utils.isDirect(name)){return}
                    Compile.directSet[name.split('-')[1]](this.vm,node,expOrFn)
                })
            }
            if(Vue.utils.isTxtTpl(node)){
                Compile.directSet['text'](this.vm,node,node.textContent)
            }
            node.childNodes.length > 0 && this.compiler(node)
        })
    }

    //将节点放到文档碎片流中
    node2fragment(el){
        let fragment = new DocumentFragment()
        , child;
        while(child = el.firstChild){
            fragment.appendChild(child)
        }
        return fragment;
    }
}


/**
 * 依赖收集--->收集的就是订阅对象
 */
class Dep{
    
    static target = null;

    constructor(){
        this.subs = [];
    }
    //添加订阅者
    //谁用到这个数据,谁就是订阅者,反映在html中就是使用数据的这个dom元素
    addSub(watcher){
        this.subs.push(watcher)
    }

    depend(){
        Dep.target && this.subs.indexOf(Dep.target)<=-1 && this.addSub(Dep.target)
    }

    //通知所有订阅者
    notify(){
        this.subs.forEach(sub=>{
            sub.update();
        })
    }
}

/**
 * 谁用到这个数据,谁依赖这个数据,谁就是订阅者
 * 订阅者-->更新视图
 */
class Watcher{
    constructor(vm,expOrFn,cb){
        this.vm = vm;
        this.expOrFn = expOrFn;
        this.cb = cb;
        this.oldVal = this.get()
    }
    get(){
        Dep.target = this;
        let value = Vue.utils.getVal(this.vm.$data,this.expOrFn);
        Dep.target = null;
        return value;
    }
    //更新视图
    update(){
        let value = Vue.utils.getVal(this.vm.$data,this.expOrFn);
        if(this.oldVal != value){
            this.cb(value);
            this.oldVal = value
        }
    }
}
原文地址:https://www.cnblogs.com/littleboyck/p/13744521.html