用defineProperty实现MVVM的双向绑定

196 阅读4分钟

MVVM的最大表现在于数据绑定。不同的框架实现数据绑定的方式有所不同,vue使用的是数据劫持。下面使用defineProperty,自己动手实现一个MVVM。

先上代码吧

<!DOCTYPE html>
<html lang="ch">
    <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>T-MVVM</title>
    </head>
    <body>
        <div id="app">
            <div>{{name}} ----  {{msg}}----<span t-text="name"></span></div>
            <div t-text="name">13131</div>
            <input id="inputBlock" t-model="msg"/>
            <input id="inputBlock" t-model="msg"/>
            <input id="inputBlock" t-model="name"/>
        </div>
        <script src="util.js"></script>
        <script src="Dep.js"></script>
        <script src="Watcher.js"></script>
        <script src="Compile.js"></script>
        <script src="Observe.js"></script>
        <script src="Mvvm.js"></script>
        <script>
            let vm = new Mvvm('#app',{
                data : {
                    name:'lala',
                    msg:'baba'
                }
            })
        </script>
    </body>
</html>

实现的效果如下:

当数据改变时,其他绑定该数据的地方会同步变化。本例子只简单实现了双括号、t-text、t-model这三种指令,只作学习使用。

结构图

在动手敲代码前,先构思个整体结构图,捋清思路再动手,可以提高敲代码的效率。

上图中,MVVM主要要实现两块:指令解析(Compile)和数据劫持(Observe)。

Compile用于初始化HTML代码中使用的指令;

Observe用于劫持数据的get、set方法;

Watcher用于链接数据与视图,Dep用于存储watcher。

下面,我们先从Observe下手

Observe

class Observe{
    constructor(data){
        this.$data = data;
        // 劫持数据
        this.observe(this.$data);
    }
    /**
     * 递归对数据进行劫持
     * @param {object} data 要进行劫持的数据对象
     */
    observe(data){
        if (!data || Object.prototype.toString.call(data).match(/\[object (.+)\]/)[1] !== 'Object') {
            return;
        }
        let keys = Object.keys(data);
        for(const key of keys){
            this.observe(data[key]);
            this.setDefine(data,key);
        }
    }
    /**
     * 对单个数据进行劫持
     * @param {object} data 要进行劫持的数据
     * @param {string} key 要进行劫持的数据的key
     */
    setDefine(data,key){
        let oldValue = data[key];
        // 每个数据对应一个dep,当劫持到数据之后通知dep更新数据
        let dep = new Dep();
        Object.defineProperty(data,key,{
            // 可配置,可以修改以及删除等
            configurable: true,
            // 可枚举
            enumerable: true,
            get:() => {
                //Dep.target为watcher对象,初始化watcher时会给Dep.target赋值并触发这个get
                Dep.target ? dep.addObserve(Dep.target) : '';
                return oldValue;
            },
            set: (newValue) => {
                if(oldValue !== newValue){
                    this.observe(newValue);
                    oldValue = newValue;
                    // 通知更新
                    dep.notify();
                }
            }
        })
    }
}

Compile

class Compile{
    constructor(el,data){
        this.$data = data;
        this.$el = Compile.isElementNode(el) ? el : document.querySelector(el);
        if(this.$el){
            this.compile(this.$el);
        }
    }
    /**
     * 递归每个元素,解析含有指令的元素
     * @param {object} el dom节点
     */
    compile(el){
        let text = el.textContent;
        let child = el.childNodes;
        for(const node of Array.from(child)){
            // 如果是element节点,则进行attributes的解释,并进行下一步递归
            if(Compile.isElementNode(node)){
                this.compileElement(node);
                this.compile(node);
            }else if(Compile.isTextNode(node)){
                // 如果是text节点,则进行t-text的处理
                this.compileText(node);
            }
        }
    }
    /**
     * 判断是否是元素节点
     * @param {*} node  dom节点
     */
    static isElementNode(node){
        return node.nodeType === 1;
    }
    /**
     * 判断是否是文本节点
     * @param {*} node dom节点
     */
    static isTextNode(node){
        return node.nodeType === 3;
    }
    /**
     * 编译文本节点
     * @param {*} node dom节点
     */
    compileText(node){
        let allContent = node.textContent;
        let newRes = allContent.replace(/\{\{([^}]+)\}\}/g,(word,content,i,str) => {
            let watcher = new Watcher(this.$data,content,(newValue) => {
                node.textContent = util.replaceValueByoldData(allContent,this.$data);
            })
            return util.getValueByKeyFromData(content,this.$data);
        });
        node.textContent = newRes;
    }
    /**
     * 编译元素节点(遍历编译attribt)
     * @param {*} node dom节点
     */
    compileElement(node){
        let reg = /^t-/;
        for(const attr of node.attributes){
            if(reg.test(attr.nodeName)){
                let key = attr.nodeValue;
                switch(attr.nodeName){
                    case 't-text':
                        new Watcher(this.$data,key,(newValue) => {
                            node.textContent = util.getValueByKeyFromData(key,this.$data);
                        })
                        node.textContent = util.getValueByKeyFromData(key,this.$data);
                        break;
                    case 't-model':
                        let that = this;
                        node.value = util.getValueByKeyFromData(attr.nodeValue,this.$data);
                        new Watcher(this.$data,key,(newValue) => {
                            node.value = util.getValueByKeyFromData(attr.nodeValue,this.$data);
                        })
                        node.addEventListener('input',function(e){
                            util.setDataByKey(that.$data,key,e.target.value)
                        })

                }
            }
        }
    }
}

下面就要实现连接Compile和Observe的Watcher和Dep了

Watcher

class Watcher{
    constructor(data,key,callback){
        this.data = data
        this.key = key;
        this.callback = callback;

        Dep.target = this;
        // 这一步会触发Observe劫持到的set方法
        this.oldVale = util.getValueByKeyFromData(this.key,data);
        Dep.target = null;
    }
    upData(){
        let newValue = util.replaceValueByoldData(this.key,this.data)
        this.callback ? this.callback(newValue) : '';
    }
}

Dep

class Dep{
    static target = null;
    constructor(){
        /** 观察列表,用于存储多个warcher */
        this.observes = [];
    }
    addObserve(watcher){
        this.observes.push(watcher);
    }
    notify(){
        for(const watcher of this.observes){
            watcher.upData();
        }
    }
}

Util

util存放公用的函数

const util = {
    /**
     * 通过key获取data中的响应数据
     * @param {string} content 数据的key,例如"name""animal.cat"
     * @param {object} data 数据集
     */
    getValueByKeyFromData(content,data){
        let keys = content.split('.');
        return keys.reduce((pre,next,index) => {
            return data[next]
        },data)
    },
    /**
     * 解析content中带有双括号绑定的内容,返回替换后的内容
     * @param {string} content 要替换的字符串,例如"{{name}}""{{animal.cat}}-----{{name}}"
     * @param {object} data 数据集
     */
    replaceValueByoldData(content,data){
        let newRes = content.replace(/\{\{([^}]+)\}\}/g,(word,p1,i,str) => {
            return util.getValueByKeyFromData(p1,data);
        });
        return newRes;
    },
    /**
     * 更新data中的值
     * @param {object} data 数据集
     * @param {string} key 数据的key,例如"name""animal.cat"
     * @param {string} newValue 替换值
     */
    setDataByKey(data,key,newValue){
        let keys = key.split('.');
        let setData =  keys.reduce((pre,next,index) => {
            if(index === keys.length - 1){
                return data[next] = newValue;
            }
            return data[next]
        },data)
    }
};

MVVM

class Mvvm{
    constructor(dom,per){
        this.$dom = dom;
        this.$per = per;
        new Observe(this.$per.data);
        new Compile(this.$dom,this.$per.data)
    }
}

总结

到这里,一个简单的MVVM就基本实现了。详细的解释都写在代码注释里面了。代码还有很多不完善的地方,还请小伙伴们能指出,一起学习一起进步。