RainZhai / rainzhai.github.com

宅鱼
http://rainzhai.github.io
Apache License 2.0
2 stars 0 forks source link

MVVM实现 #12

Open RainZhai opened 7 years ago

RainZhai commented 7 years ago

对一个现有demo进行修改和注释,方便阅读

<!DOCTYPE html>
<html>

<head>
</head>

<body>
    <!--多维数组-->
    <div id="root">
        <div v-model="user"></div>
        <ul v-list="todos">
            <h1 v-model="title"></h1>
            <div>
                <li v-list-item="todos">
                    <p v-class="todos:done" v-model="todos:creator"></p>
                    <p v-model="todos:content"></p>
                    <ul v-list="todos:members">
                        <li v-list-item="todos:members">
                            <span v-model="todos:members:name"></span>
                        </li>
                    </ul>
                </li>
            </div>
        </ul>
    </div>
    <!--双向数据绑定-->
    <div id="bind">
        <h1 v-model="title"></h1>
        <input v-model="content" v-event="input" type="text" />
        <p v-model="content"></p>
        <p v-model="content"></p>
    </div>
    <!--数组深度嵌套-->
    <div id="arr">
        <ul v-list="todos:words:arr">
            <li v-list-item="todos:words:arr">
                <div>
                    <span v-model="todos:words:arr"></span>
                </div>
            </li>
        </ul>
    </div>
</body>
<script>
    class mvvm {
        constructor(mvvm) {
            if (!mvvm.el || !mvvm.data) {
                throw new Error('mvvm need an object to observe.');
            }
            //数据
            this.$data = mvvm.data;
            //观察器
            this.$watcher = new Watcher();
            //查询dom对象
            this.$el = document.querySelector(mvvm.el);
            //事件列表
            this.$eventList = mvvm.eventList;
            //dom片段
            this.$fragment = this.nodeToFragment(this.$el);
            //解析dom片段
            this.parser(this.$fragment);
            //dom追加到页面
            this.$el.appendChild(this.$fragment);
            //开始观察
            this.$watcher.build();
        }

        /** 获取节点并将节点下子内容追加到一个DocumentFragment */
        nodeToFragment(el) {
            const fragment = document.createDocumentFragment();
            let child = el.firstChild;
            while (child) {
                fragment.appendChild(child);
                child = el.firstChild;
            }
            return fragment;
        }

        /** 扫描并解析html节点 */
        parser(node) {
            if (node === this.$fragment || !node.getAttribute('v-list')) {
                let childs = [];
                //将子节点转为数组
                if (node.children) {
                    childs = [...node.children];
                } else {
                    [...node.childNodes].forEach((child) => {
                        if (child.nodeType === 1) {
                            childs.push(child);
                        }
                    });
                }
                console.log(childs);
                childs.forEach((child) => {
                    //设置path
                    if (node.path) {
                        child.path = node.path;
                    }
                    this.parseEvent(child);
                    this.parseClass(child);
                    this.parseModel(child);
                    if (child.children.length) {
                        //解析子节点
                        this.parser(child);
                    }
                });
            } else {
                this.parseList(node);
            }
        }

        /** 解析数据 
         * param 属性值{string}
         * param 节点对象{HTMLElement}
         * return 对象{path:属性解析为数组['todos',':','creator'], data:数据对象对应的值}
         */
        parseData(str, node) {
            const list = str.split(':');
            let data;
            let nowPath;
            let arrayCounter = 1;
            const path = [];
            list.forEach((key, index) => {
                if (index === 0) {
                    data = this.$data[key];
                } else if (node.path) {
                    nowPath = node.path[arrayCounter];
                    arrayCounter += 1;
                    if (nowPath === key) {
                        data = data[key];
                    } else {
                        path.push(nowPath);
                        data = data[nowPath][key];
                        arrayCounter += 1;
                    }
                } else {
                    data = data[key];
                }
                path.push(key);
            });
            if (node.path && node.path.length > path.length) {
                const lastPath = node.path[node.path.length - 1];
                //if (typeof lastPath === 'number') {
                data = data[lastPath];
                path.push(lastPath);
                //}
            }
            //if (!node.path || node.path !== path) {
            node.path = path;
            //}
            return {
                path,
                data
            };
        }

        parseEvent(node) {
            if (node.getAttribute('v-event')) {
                const eventName = node.getAttribute('v-event');
                const type = this.$eventList[eventName].type;
                const fn = this.$eventList[eventName].fn.bind(node);
                if (type === 'input') {
                    let cmp = false;
                    node.addEventListener('compositionstart', () => {
                        cmp = true;
                    });
                    node.addEventListener('compositionend', () => {
                        cmp = false;
                        node.dispatchEvent(new Event('input'));
                    });
                    node.addEventListener('input', function input() {
                        if (!cmp) {
                            const start = this.selectionStart;
                            const end = this.selectionEnd;
                            fn();
                            this.setSelectionRange(start, end);
                        }
                    });
                } else {
                    node.addEventListener(type, fn);
                }
            }
        }

        /*
         * 解析自定class属性 并添加class
         * param html节点{HTMLElement}
         *
         */
        parseClass(node) {
            if (node.getAttribute('v-class')) {
                const className = node.getAttribute('v-class');
                const dataObj = this.parseData(className, node);
                if (!node.classList.contains(dataObj.data)) {
                    node.classList.add(dataObj.data);
                }
                this.$watcher.regist(this.$data, dataObj.path, (old, now) => {
                    node.classList.remove(old);
                    node.classList.add(now);
                });
            }
        }

        parseModel(node) {
            if (node.getAttribute('v-model')) {
                const modelName = node.getAttribute('v-model');
                const dataObj = this.parseData(modelName, node);
                if (node.tagName === 'INPUT') {
                    node.value = dataObj.data;
                } else {
                    node.innerText = dataObj.data;
                }
                this.$watcher.regist(this.$data, dataObj.path, (old, now) => {
                    if (node.tagName === 'INPUT') {
                        node.value = now;
                    } else {
                        node.innerText = now;
                    }
                });
            }
        }

        parseList(node) {
            console.log('NODE')
            console.log(node.path)
            const parsedItem = this.parseListItem(node);
            const itemEl = parsedItem.itemEl;
            const parentEl = parsedItem.parentEl;
            const list = node.getAttribute('v-list');
            const listData = this.parseData(list, node);
            listData.data.forEach((_dataItem, index) => {
                const copyItem = itemEl.cloneNode(true);
                copyItem.$data = _dataItem;
                //if (node.path) {
                copyItem.path = node.path.slice();
                //}
                copyItem.path.push(index);
                this.parseEvent(copyItem);
                this.parseClass(copyItem);
                this.parseModel(copyItem);
                this.parser(copyItem);
                parentEl.insertBefore(copyItem, itemEl);
            });
            parentEl.removeChild(itemEl);
            this.$watcher.regist(this.$data, listData.path, () => {
                while (parentEl.firstChild) {
                    parentEl.removeChild(parentEl.firstChild);
                }
                const thisListData = this.parseData(list, node);
                parentEl.appendChild(itemEl);
                thisListData.data.forEach((_dataItem, index) => {
                    const copyItem = itemEl.cloneNode(true);
                    copyItem.$data = _dataItem;
                    if (node.path) {
                        copyItem.path = node.path.slice();
                    }
                    copyItem.path.push(index);
                    this.parseEvent(copyItem);
                    this.parseClass(copyItem);
                    this.parseModel(copyItem);
                    this.parser(copyItem);
                    parentEl.insertBefore(copyItem, itemEl);
                });
                parentEl.removeChild(itemEl);
            });
        }

        parseListItem(node) {
            const me = this;
            let target;
            (function getItem(nodeToScan) {
                [...nodeToScan.children].forEach((thisNode) => {
                    if (nodeToScan.path) {
                        thisNode.path = nodeToScan.path.slice();
                    }
                    if (thisNode.getAttribute('v-list-item')) {
                        target = {
                            itemEl: thisNode,
                            parentEl: nodeToScan
                        }
                    } else {
                        me.parseEvent(thisNode);
                        me.parseClass(thisNode);
                        me.parseModel(thisNode);
                        getItem(thisNode);
                    }
                });
            }(node));
            return target;
        }
    }

    class Watcher {
        constructor() {
            this.routes = [];
        }

        regist(obj, k, fn) {
            const route = this.routes.find((el) => {
                let result;
                if ((el.key === k || el.key.toString() === k.toString()) && Object.is(el.obj, obj)) {
                    result = el;
                }
                return result;
            });
            if (route) {
                route.fn.push(fn);
            } else {
                this.routes.push({
                    obj,
                    key: k,
                    fn: [fn],
                });
            }
        }

        //对每个route的object 进行观察
        build() {
            this.routes.forEach((route) => {
                observer(route.obj, route.key, route.fn);
            });
        }
    }

    /** 观察数据变化 
     * param data 数据对象{Object}
     * param path 节点属性数组或对象{Array || Object}
     * param 回调函数{Function}
     */
    function observer(obj, k, callback) {
        if (!obj || (!k && k !== 0)) {
            throw new Error('Please pass an object to the observer.');
        }
        if (Object.prototype.toString.call(k) === '[object Array]') {
            observePath(obj, k, callback);
        } else {
            let old = obj[k];
            if (!old) {
                throw new Error('The key to observe is undefined.');
            }
            if (Object.prototype.toString.call(old) === '[object Array]') {
                observeArray(old, callback);
            } else if (old.toString() === '[object Object]') {
                observeAllKey(old, callback);
            } else {
                /** 当数据发生变化时会自动调用set方法, 获取数据时会自动调用get方法 */
                Object.defineProperty(obj, k, {
                    enumerable: true,
                    configurable: true,
                    get: () => old,
                    set: (now) => {
                        if (now !== old) {
                            callback.forEach((fn) => {
                                fn(old, now);
                            });
                        }
                        old = now;
                    },
                });
            }
        }
    }

    /*
     * 对path数组进行处理后再调用observer
     * param data 数据对象{Object}
     * param path 节点属性数组{Array}
     * param 回调函数{Function}
     */
    function observePath(obj, paths, callback) {
        let nowPath = obj;
        let key;
        paths.forEach((path, index) => {
            const path2Num = parseInt(path, 10);
            let pathArr = path;
            if (path2Num === pathArr) {
                pathArr = path2Num;
            }
            if (index < paths.length - 1) {
                nowPath = nowPath[pathArr];
            } else {
                key = pathArr;
            }
        });
        observer(nowPath, key, callback);
    }

    /*
     * 观察对数组对象操作的处理
     * param Array 数组对象{Array}
     * param 回调函数{Function}
     */
    function observeArray(arr, callback) {
        const oam = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
        const arrayProto = Array.prototype;
        const hackProto = Object.create(Array.prototype);
        oam.forEach((method) => {
            Object.defineProperty(hackProto, method, {
                writable: true,
                enumerable: true,
                configurable: true,
                value: function operateArray(...arg) {
                    const old = arr.slice();
                    const now = arrayProto[method].call(this, ...arg);
                    callback.forEach((fn) => {
                        fn(old, this, ...arg);
                    });
                    return now;
                },
            });
        });
        Object.setPrototypeOf(arr, hackProto);
    }

    function observeAllKey(obj, callback) {
        Object.keys(obj).forEach((key) => {
            observer(obj, key, callback);
        });
    }

    let data = {
        title: 'todo',
        user: 'mirone',
        todos: [{
            creator: 'keal',
            done: 'undone',
            content: 'writeMVVM',
            members: [{
                name: 'kaito'
            }]
        }]
    }
    new mvvm({
        el: '#root',
        data
    })

    data.todos[0].done = 'done'
    data.todos[0].members[0].name = 'eeeee'
    data.todos[0].members.push({
        name: 'hitoriqqqq'
    })
    data.title = 'todo list!'
    data.todos.push({
        creator: 'k',
        done: 'undone',
        content: 'get mvvm',
        members: [{
            name: 'hito'
        }]
    })
    data.todos[1].members.push({
        name: 'asuna'
    })

    //============
    let bind = {
        title: 'data bind',
        content: 'qaq'
    }
    let e = {
        'input': {
            type: 'input',
            fn: function() {
                bind.content = this.value
            }
        }
    }
    new mvvm({
        el: '#bind',
        data: bind,
        eventList: e
    })

    //===============
    let arr = {
        todos: {
            words: {
                arr: ['It\'s been a long day', 'with out you', 'my friend']
            }
        }
    }
    new mvvm({
        el: '#arr',
        data: arr,
    })
    arr.todos.words.arr.push('hello')
</script>

</html>