美文网首页
JS实现一个简易版的vue

JS实现一个简易版的vue

作者: 小小的开发人员 | 来源:发表于2019-06-18 14:54 被阅读0次

    上一篇简析vue实现原理,我们知道在 Vue 的 MVVM 设计中,我们主要针对 Observer(数据劫持)、Dep(发布订阅)、Watcher(数据监听)和 Compile(模板编译)几个部分来实现,下面来实现它们。

    // MVVM.js,入口文件,整合上面的几部分

    class MVVM {
        constructor(options) {
            // 先把 el 和 data 挂在 MVVM 实例上
            this.$el = options.el;
            this.$data = options.data;
    
            if (this.$el) {
                // $data数据劫持
                new Observer(this.$data);
    
                // 将数据代理到实例上 vm.message = "hello"
                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(newVal) {
                        data[key] = newVal;
                    }
                });
            });
        }
    }
    

    // Compile.js
    在 Vue 中的模板编译的主要就是两部分,元素节点中的指令和文本节点中的 Mustache 语法(双大括号),这是浏览器无法解析的部分。

    class Compile {
        constructor(el, vm) {
            //dom
            this.el = this.isElementNode(el) ? el : document.querySelector(el);
            this.vm = vm;
    
            // 如过传入的根元素存在,才开始编译
            if (this.el) {
                // 1、把这些真实的 Dom 移动到内存中,即 fragment(文档碎片)
                let fragment = this.node2fragment(this.el);
    
                // ********** 以下为新增代码 **********
                // 2、将模板中的指令中的变量和 {{}} 中的变量替换成真实的数据
                this.compile(fragment);
    
                // 3、把编译好的 fragment 再塞回页面中
                this.el.appendChild(fragment);
                // ********** 以上为新增代码 **********
            }
        }
    
        /* 辅助方法 */
        // 判断是否是元素节点
        isElementNode(_node) {
            return _node.nodeType === 1;
        }
    
        // ********** 以下为新增代码 **********
        // 判断属性是否为指令
        isDirective(name) {
            return name.includes("v-");
        }
        // ********** 以上为新增代码 **********
    
        /* 核心方法 */
        // 将根节点转移至文档碎片
        node2fragment(el) {
            // 创建文档碎片
            let fragment = document.createDocumentFragment();
            // 第一个子节点
            let firstChild;
    
            // 循环取出根节点中的节点并放入文档碎片中
            while (firstChild = el.firstChild) {
                fragment.appendChild(firstChild);
            }
            return fragment;
        }
    
        // ********** 以下为新增代码 **********
        // 解析文档碎片
        compile(fragment) {
            // 当前父节点节点的子节点,包含文本节点,类数组对象
            let childNodes = fragment.childNodes;
    
            // 转换成数组并循环判断每一个节点的类型
            Array.from(childNodes).forEach(node => {
                if (this.isElementNode(node)) { // 是元素节点
                    // 递归编译子节点
                    this.compile(node);
    
                    // 编译元素节点的方法
                    this.compileElement(node);
                } else { // 是文本节点
                    // 编译文本节点的方法
                    this.compileText(node);
                }
            });
        }
        // 编译元素
        compileElement(node) {
            // 取出当前节点的属性,类数组
            let attrs = node.attributes;
            Array.from(attrs).forEach(attr => {
                // 获取属性名,判断属性是否为指令,即含 v-
                let attrName = attr.name;
    
                if (this.isDirective(attrName)) {
                    // 如果是指令,取到该属性值得变量在 data 中对应得值,替换到节点中
                    let exp = attr.value;
    
                    // 取出方法名
                    let [, type] = attrName.split("-");
    
                    // 调用指令对应得方法
                    CompileUtil[type](node, this.vm, exp);
                }
            });
    
        }
        // 编译文本
        compileText(node) {
            // 获取文本节点的内容
            let exp = node.textContent;
    
            // 创建匹配 {{}} 的正则表达式
            //let reg = /\{\{([^}+])\}\}/g;
            //“.”表示任意字符。“+”表示前面表达式一次乃至多次。“?”表示匹配模式是非贪婪的。
            let reg = /\{\{(.+?)\}\}/g;
    
            // 如果存在 {{}} 则使用 text 指令的方法
            if (reg.test(exp)) {
                CompileUtil["text"](node, this.vm, exp);
            }
        }
        // ********** 以上为新增代码 **********
    }
    

    // CompileUtil.js
    CompileUtil 中存储着所有的指令方法及指令对应的更新方法,由于 Vue 的指令很多,我们这里只实现典型的 v-model 和 “{{ }}” 对应的方法,考虑到后续更新的情况,我们统一把设置值到 Dom 中的逻辑抽取出对应上面两种情况的方法,存放到 CompileUtil 的 updater 对象中。

    CompileUtil = {};
    
    // 更新Dom节点方法
    CompileUtil.updater = {
        // 文本更新
        textUpdater(node, value) {
            node.textContent = value;
        },
        // 输入框更新
        modelUpdater(node, value) {
            node.value = value;
        }
    };
    
    // 获取 data 值的方法
    CompileUtil.getVal = function (vm, exp) {
        // 将匹配的值用 . 分割开,如 vm.data.a.b
        exp = exp.split(".");
    
        // 归并取值
        return exp.reduce((prev, next) => {
            return prev[next];
        }, vm.$data);
    };
    
    // 获取文本 {{}} 中变量在 data 对应的值
    CompileUtil.getTextVal = function (vm, exp) {
        // 使用正则匹配出 {{ }} 间的变量名,再调用 getVal 获取值
        return exp.replace(/\{\{([^}]+)\}\}/g, (...args) => {
            return this.getVal(vm, args[1]);
        });
    };
    
    // 设置 data 值的方法
    CompileUtil.setVal = function (vm, exp, newVal) {
        exp = exp.split(".");
        return exp.reduce((prev, next, currentIndex) => {
            // 如果当前归并的为数组的最后一项,则将新值设置到该属性
            if(currentIndex === exp.length - 1) {
                return prev[next] = newVal
            }
    
            // 继续归并
            return prev[next];
        }, vm.$data);
    }
    
    // 处理 v-model 指令的方法
    CompileUtil.model = function (node, vm, exp) {
        // 获取赋值的方法
        let updateFn = this.updater["modelUpdater"];
    
        // 获取 data 中对应的变量的值
        let value = this.getVal(vm, exp);
    
        // 添加观察者,作用与 text 方法相同
        new Watcher(vm, exp, newValue => {
            updateFn && updateFn(node, newValue);
        });
    
        // v-model 双向数据绑定,对 input 添加事件监听
        node.addEventListener('input', e => {
            // 获取输入的新值
            let newValue = e.target.value;
    
            // 更新到节点
            this.setVal(vm, exp, newValue);
        });
    
        // 第一次设置值
        updateFn && updateFn(node, value);
    };
    
    
    // 处理文本节点 {{}} 的方法
    CompileUtil.text = function (node, vm, exp) {
        // 获取赋值的方法
        let updateFn = this.updater["textUpdater"];
    
        // 获取 data 中对应的变量的值
        let value = this.getTextVal(vm, exp);
    
        // 通过正则替换,将取到数据中的值替换掉 {{ }}
        exp.replace(/\{\{(.+?)\}\}/g, (...args) => {
            // 解析时遇到了模板中需要替换为数据值的变量时,应该添加一个观察者
            // 当变量重新赋值时,调用更新值节点到 Dom 的方法
            new Watcher(vm, args[1], newValue => {
                // 如果数据发生变化,重新获取新值
                updateFn && updateFn(node, newValue);
            });
        });
    
        // 第一次设置值
        updateFn && updateFn(node, value);
    };
    

    // Watcher.js
    Watcher 类内部要做的事情,获取更改前的值存储起来,并创建一个 update 实例方法,在值被更改时去执行实例的 callback 以达到视图的更新。

    class Watcher {
        constructor(vm, exp, callback) {
            this.vm = vm; // MVVM 的实例
            this.exp = exp; // 模板绑定数据的变量名 exp 
            this.callback = callback;
    
            // 更改前的值
            this.value = this.get();
        }
        get() {
            // 将当前的 watcher 添加到 Dep 类的静态属性上
            Dep.target = this;
    
            // 获取值触发数据劫持
            let value = CompileUtil.getVal(this.vm, this.exp);
    
            // 清空 Dep 上的 Watcher,防止重复添加
            Dep.target = null;
            return value;
        }
        update() {
            // 获取新值
            let newValue = CompileUtil.getVal(this.vm, this.exp);
            // 获取旧值
            let oldValue = this.value;
    
            // 如果新值和旧值不相等,就执行 callback 对 dom 进行更新
            if(newValue !== oldValue) {
                this.callback(newValue);
            }
        }
    }
    

    // Dep.js
    发布订阅将要执行的函数统一存储在一个数组中管理,当达到某个执行条件时,循环这个数组并执行每一个成员。

    class Dep {
        constructor() {
            this.subs = [];
        }
        // 添加订阅
        addSub(watcher) {
            this.subs.push(watcher);
        }
        // 通知
        notify() {
            this.subs.forEach(watcher => watcher.update());
        }
    }
    

    // Observer.js 数据劫持 Observer 类

    class Observer {
        constructor (data) {
            this.observe(data);
        }
        // 添加数据监听
        observe(data) {
            if(!data || typeof data !== 'object') {
                return;
            }
    
            Object.keys(data).forEach(key => {
                // 劫持(实现数据响应式)
                this.defineReactive(data, key, data[key]);
                this.observe(data[key]); // 深度劫持
            });
        }
        // 数据响应式
        defineReactive (object, key, value) {
            let _this = this;
            // 每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
            let dep = new Dep();
    
            // 获取某个值被监听到
            Object.defineProperty(object, key, {
                enumerable: true,
                configurable: true,
                get () { // 当取值时调用的方法
                    Dep.target && dep.addSub(Dep.target);
                    return value;
                },
                set (newValue) { // 当给 data 属性中设置的值适合,更改获取的属性的值
                    if(newValue !== value) {
                        _this.observe(newValue); // 重新赋值如果是对象进行深度劫持
                        value = newValue;
                        dep.notify(); // 通知所有人数据更新了
                    }
                }
            });
        }
    }
    

    来验证下我们写的MVVM。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>MVVM</title>
    </head>
    <body>
    <div id="app">
        <!-- 双向数据绑定 靠的是表单 -->
        <input type="text" v-model="message">
        <div>{{message}}</div>
        <ul>
            <li>{{message}}</li>
        </ul>
        {{message}}
    </div>
    
    <!-- 引入依赖的 js 文件 -->
    <script src="./Watcher.js"></script>
    <script src="./Observer.js"></script>
    <script src="./Compile.js"></script>
    <script src="./CompileUtil.js"></script>
    <script src="./Dep.js"></script>
    <script src="./MVVM.js"></script>
    <script>
        let vm = new MVVM({
            el: '#app',
            data: {
                message: 'hello world!'
            }
        })
    </script>
    </body>
    </html>
    

    相关文章

      网友评论

          本文标题:JS实现一个简易版的vue

          本文链接:https://www.haomeiwen.com/subject/drixqctx.html