美文网首页vue
深入理解和手写--Vue中的模板解析原理

深入理解和手写--Vue中的模板解析原理

作者: Raral | 来源:发表于2021-01-20 11:29 被阅读0次

    基本流程

    1. 将el的所有子节点取出,添加到一个新建文档fragment对象中
    2. 对fragment 中所有层次子节点递归进行编译解析处理
      • 对表达式文本节点解析
        • 插值指令解析
          • {{msg}}
      • 对元素节点的指令属性进行解析
        • 事件指令解析
          • v-on:click ="handle"
        • 一般指令解析
          • v-text = "msg"
          • v-html = "msg"
          • v-class = "myClass"
          • v-style = "myStyle"
    3. fragment解析完后,把插入根节点中

    Complie实现核心流程

    1. 将原始的dom结构,转移到fragment对象中进行模板解析,这样可以在内存进行dom操作优化。
    node2Fragment: function(el) {
        var child,
            fragment = document.createDocumentFragment();
        while(child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    
    },
    
    1. 整体流程: 先转移到fragment,再编译初始化(第一次渲染的dom结构,没有双向绑定), 编译后在放回根节点中。
     // 我们要借助fragment对象(文档碎片)对dom解析,这样可以优化解析模板性能
    if(this.$el) {
        // 将el 转移到 fragment容器中
        this.$fragment = this.node2Fragment(this.$el);
        // 编译模板初始化
        this.init(this.$fragment);
        //编译后的fragment 放入到el节点里
        this.$el.appendChild(this.$fragment);
    }
    
    1. 节点类型区分 => 解析不同节点的指令
    node.nodeType == 1; //元素节点
    node.nodeType == 3; //文本节点
    node.nodeType == 2; //属性节点
    
     // 节点种类: 元素节点(1) 属性节点(2) 文本节点(3)
        // 遍历子节点并且判断不同的节点有对应的节点编译
        compileNode: function(el) {
            var childNodes = el.childNodes,
                me = this;
            Array.prototype.forEach.call(childNodes, node => {
                var text = node.textContent;
                var reg = /\{\{(.*)\}\}/; //匹配 {{}}
    
                //判断是否元素节点
                if(me.isElementNode(node)) {
                    me.compileElementNode(node);
                }else if(me.isTextNode(node) && reg.test(text)) {
                    //匹配到的 msg 去掉 空格
                    me.complieTextNode(node, RegExp.$1.trim()); 
                }
    
                if(node.childNodes && node.childNodes.length) {
                    me.compileNode(node);
                }
    
            })
    
        },
    
    1. 不同的指令进行对应的更新器
    var complieUtil = {
        // v-text
        //node:当前节点 目的用于视图更新
        //vm : 当前vue的实例 目的用于 匹配 data中的数据
        //exp: 一个模板匹配的依据
        text: function(node, vm, exp) {
            this.bind(node, vm, exp, "text");
        },
    
        //....
    }
    
    1. 根据不同指令选择更新节点信息
    // 根据不同的指令 更新节点信息
    var updater = {
        textUpdater: function(node, value) {
            node.textContent = typeof value == "undefined"? "" : value;
        }
        // ...
    }
    

    手写vue模板编译的Complie

    注意让大家深入理解编译的思路

    
    // 就是把数据正确渲染到页面中
    function Compile(el, vm) {
        //参数配置到vm上
        this.$vm = vm;
        this.$el = this.isElementNode(el)? el : document.querySelector(el);
    
        // 我们要借助fragment对象(文档碎片)对dom解析,这样可以优化解析模板性能
        if(this.$el) {
            // 将el 转移到 fragment容器中
            this.$fragment = this.node2Fragment(this.$el);
            // 编译模板初始化
            this.init(this.$fragment);
            //编译后的fragment 放入到el节点里
            this.$el.appendChild(this.$fragment);
        }
    
    }
    Compile.prototype = {
        contructor: Compile,
        node2Fragment: function(el) {
            var child,
                fragment = document.createDocumentFragment();
            while(child = el.firstChild) {
                fragment.appendChild(child);
            }
            return fragment;
    
        },
        init: function(fragment) {
            this.compileNode(fragment);
        },
        // 节点种类: 元素节点(1) 属性节点(2) 文本节点(3)
        // 遍历子节点并且判断不同的节点有对应的节点编译
        compileNode: function(el) {
            var childNodes = el.childNodes,
                me = this;
            Array.prototype.forEach.call(childNodes, node => {
                var text = node.textContent;
                var reg = /\{\{(.*)\}\}/; //匹配 {{}}
    
                //判断是否元素节点
                if(me.isElementNode(node)) {
                    me.compileElementNode(node);
                }else if(me.isTextNode(node) && reg.test(text)) {
                    //匹配到的 msg 去掉 空格
                    me.complieTextNode(node, RegExp.$1.trim()); 
                }
    
                if(node.childNodes && node.childNodes.length) {
                    me.compileNode(node);
                }
    
            })
    
        },
    
        //解析元素节点  例如  <p>{{msg}}</p> <p v-text="msg"></p>
        compileElementNode: function(node) {
            //获取节点的属性
            var nodeAttrs = node.attributes, //NamedNodeMap 结构非数组
                me = this;
            Array.prototype.forEach.call(nodeAttrs, attr => {
                let attrName = attr.name;
                if(me.isDirective(attrName)) {
                    // 区分事件指令
                    var exp = attr.value; // msg
                    var dir = attrName.substring(2); // text
    
                    //事件指令
                    if(me.isEventDirective(dir)) {
                        complieUtil.eventHandler(node, me.$vm, exp, dir)
                    }else {
                        complieUtil[dir] && complieUtil[dir](node,me.$vm, exp)
    
                    }
    
                }
            })
    
        },
        //解析文本节点
        complieTextNode: function(node,exp) {
            complieUtil.text(node, this.$vm, exp);
        },
    
    
        isElementNode:function(node) {
            return node.nodeType == 1;
        },
        isTextNode:function(node) {
            return node.nodeType == 3;
        },
    
        isDirective(attr) {
            return attr.indexOf("v-") == 0;
        },
        isEventDirective(attr) {
            return attr.indexOf("on") == 0;
        }
    
    };
    // 根据不同指令进行对应的解析
    var complieUtil = {
        // v-text
        //node:当前节点 目的用于视图更新
        //vm : 当前vue的实例 目的用于 匹配 data中的数据
        //exp: 一个模板匹配的依据
        text: function(node, vm, exp) {
            this.bind(node, vm, exp, "text");
        },
        html: function(node, vm, exp) {
            this.bind(node, vm, exp, "html");
        },
        model: function(node, vm, exp) {
            console.log(node, vm, exp);
            //数据初始化渲染
            this.bind(node, vm, exp, "model");
            //获取初始的值
            var oldVal = vm._data[exp];
            //根据用户输入框改变 从而改vm的数据
            node.addEventListener("input", function(e) {
                var newVal = e.target.value;
                //判断是否变化
                if(newVal === oldVal) return;
                vm._data[exp] = newVal;
                oldVal = newVal;
                //注意:
                /**
                 * 由于我们还有没有实现 View层和Model层双向绑定,现在还有效果
                 * 
                 */
            })
    
        },
    
        class: function(node, vm, exp) {
            // 阅读这么久  请阅读者自我实现 (●'◡'●)
        },
    
        style: function(node, vm, exp) {
            // 阅读这么久 请阅读者自我实现 (●'◡'●)
        },
    
        // 将节点(视图)中的数据 和 vue实例data中的数据 实现双向绑定
        //node:当前节点 目的用于视图更新
        //vm : 当前vue的实例 目的用于 匹配 data中的数据
        //exp: 一个模板匹配的依据
        //dir: 区分不同指令对应不同的更新器
        bind: function(node, vm, exp, dir) {
            //获取对应的更新节点的函数
            var updaterFn = updater[dir + "Updater"];
            // 获取对应的vue实例_data的值
            var value = vm._data[exp];
            updaterFn && updaterFn(node, value);
    
            //注意
            /**
             *由于我们没有实现数据和视图双向绑定
             * 等我们实现 依赖收集后,这里代码进行改写
             *  new Watcher(vm, exp, function(value, oldValue) {
                    //当表达式对应的一个属性值变化,更新界面中的节点
                    updaterFn && updaterFn(node, value, oldValue);
                });
             */
    
    
        },
        //v-on
        eventHandler: function(node, vm, exp, dir) {
            //获取事件类型
            var eventType = dir.split(":")[1];
            // 获取事件在vue实例中的回调函数
            var fn = vm.$options.methods && vm.$options.methods[exp];
    
            if(eventType && fn) {
                //给对应的节点添加 监听事件
                node.addEventListener('click', fn.bind(vm), false);
            }
        }
    }
    
    // 根据不同的指令 更新节点信息
    var updater = {
        textUpdater: function(node, value) {
            node.textContent = typeof value == "undefined"? "" : value;
        },
        htmlUpdater: function(node, value) {
            node.innerHTML = typeof value == "undefined"? "" : value;
        },
        modelUpdater: function(node, value) {
            node.value = typeof value == "undefined"? "" : value;
        },
        classUpdater: function(node, val, oldVal) {
              // 阅读这么久  请阅读者自我实现 (●'◡'●)
        },
        styleUpdater: function(node, val, oldVal) {
             // 阅读这么久 请阅读者自我实现 (●'◡'●)
        }
    
    }
    
    

    MVVM 结构

    (function() {
        function Vue(options) {
            // 配置对象保存到vm
            this.$options = options || {};
            var data = this._data = this.$options.data;
            var me = this;
            //数据代理 vm.xxx => vm._data.xxx 不需要递归
            this.convertData(data);
    
            // 发布订阅模式--数据劫持--依赖收集
            // observe(data);
    
            //模板编译
            this.$compile = new Compile(options.el || document.body, this);
    
        }
    
        Vue.prototype = {
            constructor: Vue,
            convertData: function(data) {
                var me = this;
                Object.keys(data).forEach(key => {
                    me._proxyData(key);
                })
            },
            _proxyData: function(key) {
                var me = this;
                Object.defineProperty(me, key, {
                    configurable: false,
                    enumerable: true,
                    // writable: true,
                    get: function proxyGetter() {
                        return me._data[key];
                    },
                    set: function proxySetter(newVal) {
                        me._data[key] = newVal;
                    }
                })
            }
    
    
        }
        return window.Vue = Vue;
    })()
    
    • 模板解析的html
     <div id="app">
            <h5>模板解析---插值指令/普通指令/事件指令</h5>
            <p>{{msg}}</p>
            <p v-html="msg">解析v-html</p>
            <p v-text="msg">解析v-text</p>
            <div v-on:click="handle">事件</div>
            <h5>模板解析---v-modle指令</h5>
            <input v-model="msg" placeholder="请输入指令对应的值">
    
    </div>
    
    • 模板解析的script
    <script src="./js/compile.js"></script>
    <script src="./js/min_vue.js"></script>
    
    <script>
        const app = new Vue({
            el:"#app",
            data: {
                msg: "hello world",
                person: {
                    name: "lisi",
                    age: 12
                }
            },
            methods: {
                handle() {
                    alert(123)
                }
            },
        })
    
    </script>
    
    • 最终实现的效果


      1611113463(1).png

    实现整体MVVM流程哪一部分呢?

    是图中所标记的,接下来我会依次总结其他部分实现原理。

    整体流程图_1.jpg
    图有点丑,万水千山总是情,点播关注行不行,阅读者!!!

    相关文章

      网友评论

        本文标题:深入理解和手写--Vue中的模板解析原理

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