mvvm探究

200 阅读3分钟

简介

Observer为模型数据中的每个属性添加访问器属性,同时将页面中的用到的模型数据加入到监视队列watch中,只要修改模型数据的值,就会自动调用set方法,set方法会实际修改模型数据的属性值并通知监视队列中接受此数据影响的元素修改元素内容。Compile编译DOM元素, 把元素的属性与模型数据关联起来,并创建元素的watcher和更新DOM元素的属性。

index.html

<!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>Vue Demo</title>
    </head>
    <body>
        <div id="app">
            <input type="text" v-model="message" />
            <h5>{{ message }}</h5>
        </div>
        <!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
        <script src="./watcher.js"></script>
        <script src="./observer.js"></script>
        <script src="./compile.js"></script>
        <script src="./mvvm.js"></script>
        <script>
            // 编译模板
            // 数据劫持 Object.defineProperty get/set
            // watch观察数据变化
            let vm = new MVVM({
                el: "#app",
                data: {
                    message: "hello world"
                }
            });
        </script>
    </body>
</html>

mvvm.js

class MVVM {
    constructor(options) {
        const { el, data } = options;
        this.$el = el;
        this.$data = data;

        // 如果有要编辑的模板就开始编译
        if (this.$el) {
            // 数据劫持
            new Observer(this.$data);
            // 数据设置代理到this上
            this.proxyData(this.$data);
            // 用数据和元素进行编译
            new Compile(this.$el, this);
        }
    }
    proxyData(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                get() {
                    return data[key];
                },
                set(newValue) {
                    data[key] = newValue;
                }
            });
        });
    }
}

observer.js

class Observer {
    constructor(data) {
        this.observe(data);
    }
    observe(data) {
        // 对data 数据将原有的属性改成 get 和 set 的
        if (!data || typeof data !== "object") {
            return;
        }
        // 将数据一一劫持 先获取data的key 和 value
        Object.keys(data).forEach(key => {
            // 数据劫持
            this.defineReactive(data, key, data[key]);
            this.observe(data[key]); // 深度劫持,递归
        });
    }
    // 定义响应式
    defineReactive(object, key, value) {
        const that = this;
        // 每个变化的数据都对应一个数组, 这个数组存储所有更新的操作
        const dep = new Dep();
        console.log("dep", key);
        // ES6 的语法
        Object.defineProperty(object, key, {
            enumerable: true,
            configurable: true,
            // 取值
            get() {
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            // 设置值
            set(newValue) {
                if (newValue !== value) {
                    // 如果是对象进行劫持
                    that.observe(newValue);
                    value = newValue;
                    dep.notify(); // 更新数据
                }
            }
        });
    }
}

class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(watcher) {
        console.log("watcher", watcher, this.subs.length);
        this.subs.push(watcher);
    }
    notify() {
        this.subs.forEach(watcher => {
            watcher.update();
        });
    }
}

compile.js

class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;

        // 如果元素存在才开始编译
        if (this.el) {
            // 1、先把真实DOM移入到内存中fragment
            let fragment = this.nodeToFragment(this.el);
            // 2、 提取元素节点v-model {{}}
            this.compile(fragment);
            // 把编译好的fragment放回页面中
            this.el.appendChild(fragment);
        }
    }

    /* 写一些辅助方法 */
    isElementNode(node) {
        return node.nodeType === 1;
    }
    // 判断是不是指令
    isDirective(name) {
        return name.includes("v-");
    }

    /* 核心的方法 */
    nodeToFragment(node) {
        const fragment = document.createDocumentFragment();
        let firstChild;
        while ((firstChild = node.firstChild)) {
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
    compile(fragment) {
        let childNodes = fragment.childNodes;
        Array.from(childNodes).forEach(node => {
            if (this.isElementNode(node)) {
                // 编译元素
                this.compileElement(node);
                // 递归获取各级元素
                this.compile(node);
            } else {
                // 编译文本
                this.compileText(node);
            }
        });
    }
    compileElement(node) {
        const attrs = node.attributes;
        Array.from(attrs).forEach(attr => {
            const { name, value } = attr;
            // 判断是不是指令
            if (this.isDirective(name)) {
                // value 赋值到 node 并更新到vm.$data
                const [, type] = name.split("-");
                CompileUtil[type](node, this.vm, value);
            }
        });
    }
    compileText(node) {
        let text = node.textContent; // 获取文本
        const reg = /\{\{([^\}]+)\}\}/;
        if (reg.test(text)) {
            // text 赋值到 node 并更新到 vm.$data
            CompileUtil.text(node, this.vm, text);
        }
    }
}

// 编译工具库
CompileUtil = {
    // 获取data的值
    getData(vm, expr) {
        expr = expr.split(".");
        return expr.reduce((prev, next) => {
            return prev[next];
        }, vm.$data);
    },
    setData(vm, expr, newVal) {
        // expr = a.b.c.d
        expr = expr.split(".");
        expr.reduce((prev, next, index) => {
            if (index === expr.length - 1) {
                return (prev[next] = newVal);
            }
            return prev[next];
        }, vm.$data);
    },
    // 文本处理
    text(node, vm, text) {
        let updateFn = this.updater["textUpdater"];
        // 过滤到 {{}}
        const expr = text.replace(/\{\{\s*([^\}\s]+)\s*\}\}/g, (...args) => {
            new Watcher(vm, args[1], newValue => {
                // 数据变化后,需要重新获取文本节点依赖的属性来更新文本节点
                console.log("text", newValue);

                updateFn && updateFn(node, newValue);
            });
            return args[1];
        });
        updateFn && updateFn(node, this.getData(vm, expr));
    },
    // v-model处理
    model(node, vm, expr) {
        let updateFn = this.updater["modelUpdater"];
        // 数据变化后调用回调函数,回传新值
        new Watcher(vm, expr, newValue => {
            updateFn && updateFn(node, newValue);
        });
        node.addEventListener("input", e => {
            this.setData(vm, expr, e.target.value);
        });
        // 获取dom的
        updateFn && updateFn(node, this.getData(vm, expr));
    },
    updater: {
        // 文本更新
        textUpdater(node, value) {
            node.textContent = value;
        },
        // 输入框更新
        modelUpdater(node, value) {
            node.value = value;
        }
    }
};

watcher.js

// 给需要变化的元素增加一个观察者,当数据变化的时候执行对应的方法
class Watcher {
    constructor(vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 先获取旧值
        Dep.target = this;
        this.value = this.get();
        Dep.target = null;
    }
    get() {
        return CompileUtil.getData(this.vm, this.expr);
    }
    // 对外暴露的方法
    update() {
        const newValue = this.get();
        const oldValue = this.value;
        if (newValue !== oldValue) {
            this.cb(newValue);
        }
    }
}