手写实现一个Vue中MVVM

103 阅读2分钟

MVVM 可以写成 MV-VM,是 Model View - ViewModel 的缩写, ViewModel 主要靠 DataBinding 把 View 和 Model 做了自动关联,框架替应用开发者实现数据变化后的视图更新,下面我们就来进行一个简单的实现:

class Vue {
     constructor(options){
        this.$el = options.el;
        this.$data = options.data;
        let methods = options.methods;
        let computed = options.computed;
        if(this.$el){
            new Observer(this.$data);
            //将data定义到当前实例上面
            this.proxyVm(this.$data)
            //设置计算机属性,有依赖关系
            for(let key in computed){
                Object.defineProperty(this.$data,key,{
                    get:()=>{
                        return computed[key].call(this)
                    }
                })
            }

            //设置方法
            for(let key in methods){
                Object.defineProperty(this,key,{
                    get:()=>{
                        return methods[key].bind(this)
                    }
                })
            }

            //编译模板
            new Compiler(this.$el,this)
        }
    }
    proxyVm(data){
        for(let key in data){
            //实现可以通过vm取到对应的内容
            Object.defineProperty(this,key,{
                get(){
                    // console.log('是咧上面的数据:',key)
                    return data[key]
                },
                set(newVal){
                    if(data[key] !== newVal){
                        data[key] = newVal
                    }
                }
            })
        }
    }

}
class Dep {
    constructor(){
        this.subs = [];//这里面存放所有的watcher
    }
    //订阅
    addSub(watcher){ // 添加watcher
        this.subs.push(watcher);
    }
    //发布
    notify(){
        this.subs.forEach(watch=>{
            return watch.update();
        })
    }
}

class Watch {
    //属性一变化 , 就会执行cb回调函数
    constructor(vm,expr,cb){
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        //默认会先存放一个老值
        this.oldValue = this.get();
    }
    get(){
        Dep.target = this;
        let value =  CompileUtil.getValue(this.vm,this.expr);
        Dep.target = null;//?
        return value
    }

    //更新操作,数据变化后,会调用观察者的update方法
    update(){
        let newValue = CompileUtil.getValue(this.vm,this.expr);
        if(newValue !== this.oldValue){
            this.cb(newValue)
        }
    }
}

class Compiler {
    constructor(el,vm){
        //用户有可能传入的el是一个元素
        this.el = this.isElement(el) ? el : document.querySelector(el);
        // console.log('dom:',dom);
        this.vm = vm;
        let fragment = this.nodeToFragment( this.el);
        // console.log('fragment:',fragment);
        this.compile(fragment);
        this.el.appendChild(fragment)
    }
    compile(node){
        let childNode = node.childNodes;
        Array.from(childNode).forEach(child =>{
            if(this.isElement(child)){
                this.compileElementNode(child);
                //有可能子节点下面还有子节点
                this.compile(child)
            }else if(this.isText(child)){
                this.compileTextNode(child)
            }
        })
    }
    //处理元素节点
    compileElementNode(node){
        let attrs = Array.from(node.attributes);
        // console.log('attrs:',attrs)
        attrs.forEach(attr =>{
            let {name,value:expr} = attr
            if(this.isDirective(name)){
                //如果是指令
                let [, directive] = name.split('-');
                //需要调用不同指令来处理
                CompileUtil[directive](node, expr, this.vm);
            }else if(this.isEvent(name)){
                //如果是事件 @click='handler'
                let directive = name.slice(1);
                CompileUtil['eventHandle'](node,directive, expr, this.vm);
            }
        })
    }
    isDirective(attr){
        return attr.startsWith('v-')
    }
    isEvent(attr){
        return attr.startsWith('@')
    }
    //处理文本节点 {{a}} {{b}}
    compileTextNode(node){
        let content = node.textContent;
        if (/\{\{(.+?)\}\}/.test(content)) {
            // console.log('ss:', content);
            //文本节点
            CompileUtil['textInserted'](node, content, this.vm);//{{a}}{{b}}
        }
    }
    nodeToFragment(node){
        let fragment = document.createDocumentFragment();
        let firstChild;
        while (firstChild = node.firstChild) {
            fragment.appendChild(firstChild)
        }
        return fragment
    }

    //判断是元素节点
    isElement(node){
        return node.nodeType === 1
    }
    //判断是文本节点
    isText(node){
        return node.nodeType === 3
    }
}
const CompileUtil = {
    //获取值
    getValue(vm,expr){
        return expr.split('.').reduce((current,key)=>{
            return current[key]
        },vm.$data)
    },
    //设置值
    setValue(newValue,expr,vm){
        return expr.split('.').reduce((current,key,index,arr)=>{
            if(index === arr.length - 1){
                return current[key] = newValue
            }
            return current[key]
        },vm.$data)
    },
    //处理{{}}
    textInserted(node,expr,vm){
        // console.log('content:',content)
        let fn = this.updater['textInsertedUpdater'];
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            new Watch(vm,args[1],()=>{
                fn(node,this.getContentValue(vm,expr));//返回一个全的字符串
            });
            return this.getValue(vm, args[1])
        });
        fn(node, content)
    },
    getContentValue(vm,expr){
        //遍历表达式,将内容重新替换成一个完整的内容,返回回去
        return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            return this.getValue(vm, args[1])
        })
    },
    text(node,expr,vm){
        let fn = this.updater['textUpdater'];
        // console.log('expr:',expr)
        let value = this.getValue(vm,expr);
        new Watch(vm,expr,(newValue)=>{
            fn(node,newValue)
        })
        // console.log('expr text:',value);
        fn(node,value)
    },
    model(node,expr,vm){
        let fn = this.updater['modelUpdater'];
        let value = this.getValue(vm,expr)
        // console.log('model:',node.nodeName);
        new Watch(vm,expr,(newVal)=>{
            fn(node,newVal)
        });
        if(node.nodeName === 'INPUT' || node.nodeName === 'TEXTAREA'){
            node.addEventListener('input',(e)=>{
                this.setValue(e.target.value,expr,vm)
            })
        }
        fn(node, value)
        // node.addEventListener('')
    },
    html(node,expr,vm){
        let fn = this.updater['htmlUpdater'];
        let value = this.getValue(vm,expr);
        new Watch(vm,expr,(newVal)=>{
            fn(node,newVal)
        });
        fn(node,value)
    },
    //处理事件
    eventHandle(node,eventName,expr,vm){
        node.addEventListener(eventName,(e)=>{
            vm[expr] && vm[expr].call(vm,e)
        })
    },
    updater:{
        textUpdater(node,value){
            node.innerText = value
        },
        textInsertedUpdater(node,content){
            node.textContent = content
        },
        htmlUpdater(node,value){
            node.innerHTML = value;
        },
        modelUpdater(node,value){
            node.value = value;
        }
    }

}

//定义响应式数据
class Observer {
    constructor(data){
        this.observerData(data)
    }
    observerData(data){
        if(data && typeof data === 'object'){
            for(let key in data){
                this.defineReactive(key,data[key],data)
            }
        }

    }
    defineReactive(key , value, data){
        this.observerData(value);//有可能value也是一个对象,也需要进行响应式处理
        let dep = new Dep() ;//给每一个属性都加上一个具有发布订阅的功能
        Object.defineProperty(data,key,{
            get:()=>{
                //创建watcher时会取到对应的内容,并且把watcher放到全局上面
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set:(newValue)=> {
                if(value !== newValue){
                    value = newValue;
                    //有可能用户重新修改的数据也是一个对象
                    this.observerData(newValue);
                    dep.notify();
                }
            }
        })
    }

}

下面我们新建一个html页面来测试这个vue文件 html代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<style>
    .red{
        color: red;
    }
</style>
<body>
<div id="app">
    <input type="text" v-model="school.name">
    <div>{{school.age}}</div>
    <div>{{school.name}} {{school.age}}</div>
    <span>1</span>
    <span>{{getName}}</span>
    <span v-text="school.text"></span>
    <button @click="changeValue">点我</button>
    <div v-html="school.message">
    </div>
</div>
<script src="./vue.js"></script>
<script>
    let vm = new Vue({
        el:'#app',
        data:{
            school:{
                age:20,
                name:'你好啊',
                text:'这个是v-text',
                message:'<h1>欢迎大家</h1>'
            }
        },
        methods:{
            changeValue(e){
                this.school.name = 'click事件'
                this.school.message = '<h3>变了没有呢</h3>'
            }
        },
        computed:{
            getName(){
                return this.school.name + 'hello world'
            }
        }
    });
</script>
</body>
</html>

同学们可以自己测试一下,一些模仿vue的基本功能是实现了的哦