Vue的模版解析

Posted orochiz-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue的模版解析相关的知识,希望对你有一定的参考价值。

1.大括号表达式

(1)在MVVM()中接收并保存配置对象
(2)调用Compile编译函数,将el和vm传入

function MVVM (option) {
    this.$option = option || {}
    this._data = option.data
    // 调用编译函数
    new Compile(option.el || document.body, this)
}

(3)在Compile()中保存vm,并根据el获取对应的dom,将这个dom元素的所有子节点移动到fragment中
(4)遍历fragment中的所有子节点,使用compileElement()编译此节点
(5)如果子节点是文本节点且匹配到{{}}格式的文本,则将{{}}替换成对应的值,如果该节点存在子节点,则递归调用compileElement()编译此节点
(6)最后将将编译好的fragment插入到el对应的dom中

function Compile (el,vm) {
    this.$vm = vm
    this.$el = document.querySelector(el)

    // 如果这个dom存在
    if(this.$el) {
        // 则将其所有子节点移动到fragment中
        this.$fragment = this.nodeToFragment(this.$el)
        // 调用初始化函数,编译fragment
        this.init()
        // 将编译好的fragment插入到el中
        this.$el.appendChild(this.$fragment)
    }
}
Compile.prototype = {
    nodeToFragment (el) {
        //创建fragment对象
        var fragment = document.createDocumentFragment()
        var child
        while(child = el.firstChild) {
            // 将原生节点移动到fragment中
            fragment.appendChild(child)
        }

        // 返回fragment
        return fragment;
    },

    init: function() {
        // 调用编译某个节点的函数
        this.compileElement(this.$fragment);
    },

    // 这个函数用来编译传入元素的子节点,且会被递归调用
    compileElement (el) {
        var me = this
        // 获取所有子节点
        var childNodes = el.childNodes
        // 遍历所有子节点
        Array.prototype.slice.call(childNodes).forEach(node => {
            // 创建匹配{{}}格式的正则
            // 禁止贪婪{{xxx}}--{{ddd}} => xxx 和 ddd(会匹配到2个)
            // 如果不禁止贪婪 就会变成{{xxx}}--{{ddd}} => xxx}}--{{ddd
            var reg = /{{(.*?)}}/
            
            // 使用循环将此节点的所有{{xxx}}依次替换成vm._data对应的值
            while(node.nodeType == 3 && reg.test(node.textContent)) {
                // 获取{{}}中变量在vm.data中对应的值 RegExp.$1就是第一个匹配的值
                var val = me.getVMVal(this.$vm, RegExp.$1.trim())
               // 获取原始的值
                var oldVal = node.textContent
                // 用vm.data中对应的值将{{xxx}}替换掉
                node.textContent = oldVal.replace(reg,val)
            }
            // 如果该节点存在 且 有子节点 则调用递归 编译此节点
            if(node.childNodes && node.childNodes.length) {
                me.compileElement(node)
            }
        })
    },

    // 获取变量在vm.data中对应的值
    getVMVal (vm,exp) {
        var val = vm._data
        // exp值可能是a.b.c
        expArr = exp.split('.')
        expArr.forEach(function(key){
            val = val[key]
        })
        return val
    }
}
<!-- html模版 -->
<div id="app">
    <p>名字:{{person.name}} -- 年纪:{{person.age}}</p>
</div>
//实例化
var vm = new MVVM({
    el:'#app',
    data: {
        person:{
            name: '子龙',
            age: 20
        }
    }
})

渲染结果:

<div id="app">
    <p>名字:子龙 -- 年纪:20</p>
</div>

2.解析v-html/v-on指令

(1)在compileElement()中遍历节点时,如果此节点是元素节点,则调用compileOrder()编译此节点的属性节点(只有元素节点才有可能存在属性节点)
(2)compileOrder()中遍历该元素节点的所有属性节点,如果属性名是符合定义的指令,则根据指令类型进行相应的操作
(3)如果是v-html指令,则操作该元素节点的innerHTML属性
(4)如果是v-on指令,则为该节点添加事件监听
(5)指令编译完成之后移除指令

Compile.prototype = {
    nodeToFragment (el) {
        //创建fragment对象
        var fragment = document.createDocumentFragment()
        var child
        while(child = el.firstChild) {
            // 将原生节点移动到fragment中
            fragment.appendChild(child)
        }

        // 返回fragment
        return fragment;
    },

    init: function() {
        // 调用编译某个节点的函数
        this.compileElement(this.$fragment);
    },

    // 这个函数用来编译传入元素的子节点,且会被递归调用
    compileElement (el) {
        // 获取所有子节点
        var childNodes = el.childNodes
        // 遍历所有子节点
        Array.prototype.slice.call(childNodes).forEach(node => {
            // 创建匹配{{}}格式的正则
            // 禁止贪婪{{xxx}}--{{ddd}} => xxx 和 ddd(会匹配到2个)
            // 如果不禁止贪婪 就会变成{{xxx}}--{{ddd}} => xxx}}--{{ddd
            var reg = /{{(.*?)}}/

            // 如果该节点是 元素节点
            if(node.nodeType === 1){
                // 编译此元素属性中的指令
                this.compileOrder(node)
            }else{
                // 使用循环将此节点的所有{{xxx}}依次替换成vm._data对应的值
                while(node.nodeType == 3 && reg.test(node.textContent)) {
                    // 获取{{}}中变量在vm.data中对应的值 RegExp.$1就是第一个匹配的值
                    var val = this.getVMVal(this.$vm, RegExp.$1.trim())
                    // 获取原始的值
                    var oldVal = node.textContent
                    // 用vm.data中对应的值替换 {{xxx}}
                    node.textContent = oldVal.replace(reg,val)
                }
            }
            // 如果该节点存在 且 有子节点 则调用递归 编译此节点
            if(node.childNodes && node.childNodes.length) {
                this.compileElement(node)
            }
        })
    },

    // 获取变量在vm.data中对应的值
    getVMVal (vm,exp) {
        var val = vm._data
        // exp值可能是a.b.c
        expArr = exp.split('.')
        expArr.forEach(function(key){
            val = val[key]
        })
        return val
    },
    compileOrder: function(node){
        // 获取该节点所有属性节点
        var nodeAttrs = node.attributes
        // 遍历所有属性
        Array.from(nodeAttrs).forEach(attr => {
            // 获取属性名
            var attrName = attr.name
            // 判断属性是否是我们自定的指令
            if(this.isDirective(attrName)){
                // 获取指令对应的表达式
                var exp = attr.value
                // 获取指令 v-text => text (截去前两个字符)
                var dir = attrName.substring(2)
                // 判断指令类型 是否是事件指令
                if(this.isEventDirective(dir)){
                    // 调用指令处理对象的相应方法 dir == on:click
                    compileUtil.eventHandler(node,dir,exp,this.$vm)
                }else {
                    // 普通指令 v-text
                    compileUtil[dir] && compileUtil[dir](node,this.getVMVal(this.$vm, exp))
                }
                // 指令编译完成之后移除指令
                node.removeAttribute(attrName)
            }
        })
    },
    isDirective: function(attrName){
        // 只有 v- 开头的属性名才是我们定义的指令
        return attrName.indexOf('v-') == 0
        // attrName.startsWith("v-")
    },
    isEventDirective: function(dir){
        // 事件指令以 on 开头
        return dir.indexOf('on') == 0
    }
}

// 指令处理集合
var compileUtil = {
    text: function(node,value){
        node.textContent = typeof value == 'undefined' ? '' : value
    },
    html: function(node,value){
        node.innerHTML = typeof value == 'undefined' ? '' : value
    },
    eventHandler: function(node,dir,exp,vm){
        // 为节点绑定事件 (哪个节点,哪个事件,触发哪个回调)

        // 获取事件名称 on:click => click
        var eventName = dir.split(':')[1]
        // 根据exp获取其在在vm中对应的函数
        var fn = vm.$option.methods[exp]

        // 只有事件名称和回调同时存在才添加事件监听
        if(eventName && fn){
            // 回调函数强制绑定this为vm
            node.addEventListener(eventName,fn.bind(vm),false)
        }
    }
}
<div id="app">
    <div v-html="html"></div>
    <div v-text="html"></div>
    <button v-on:click="test">点我</button>
</div>
var vm = new MVVM({
    el:'#app',
    data: {
        html:"<h3>我是h3</h3>",
    },
    methods:{
        test(){
            console.log('click test')
        }
    }
})

以上是关于Vue的模版解析的主要内容,如果未能解决你的问题,请参考以下文章

vue template compiler模版解析模块源码解析

Vue的模版解析

前端技能树,面试复习第 45 天—— Vue 基础 | 模版编译原理 | mixin | use 原理 | 源码解析

vue-cli的webpack模版,相关配置文件dev-server.js与webpack.config.js配置解析

使用Visual Studio Code自定义代码模版

使用 Git 来管理 Xcode 中的代码片段