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

基本流程

  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

图有点丑,万水千山总是情,点播关注行不行,阅读者!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容