Vue 模板解释

Vue 模板解释

  现在的前端三大框架都有它们独特的模板,模板的做用就是让开发编码变得更加简单,然而我以为 Vue 在这一点上是作得近乎完美的(固然,只是我的观点~~),Vue 模板解释的核心不外乎就是两个玩意儿,一个是双大括号表示式,另外一个是模板指令,这两东西也是咱们在 Vue 项目中都确定会用到的,下面就来详细介绍他们是如何实现的。前端

(一)建立模板解释对象vue

function Vue(options) {
    // 将配置对象保存在实例对象上
    this.$options = options
    // 将配置对象里面的data属性保存在实例对象上
    let data = this._data = options.data
    // 保存实例对象,其实也能够用箭头函数~~
    let me = this
    // 遍历data中的属性,逐一实现数据劫持
    Object.keys(data).forEach(function (key) {
        me._proxy(key)
    })
    // 模板解释
    this.$compile = new Compile(options.el || document.body,this)
}

可见,模板解释是在数据劫持以后实现的,在实现完数据劫持后,建立模板解释对象,而且保存到实例对象中,这里面有两个参数,第一个就是配置对象中的 el ,也就是挂载的 DOM ,第二个就是 vm 。node

(二)经过 Fragment 容器实现初始化正则表达式

function Compile(el, vm) {
    // 保存vm
    this.$vm = vm
    // 保存el,判断是不是元素节点,若是不是则尝试经过选择器字符来解释
    this.$el = this.isElementNode(el) ? el : document.querySelector(el)
    // 确保$el存在
    if(this.$el){
        // 1. 取出el中全部子节点, 封装在一个fragment对象中
        this.$fragment = this.node2Fragment(this.$el)
        // 2. 编译fragment中全部层次子节点,这个就是模板编译的核心方法~~~
        this.init()
        // 3. 将fragment添加到el中
        this.$el.appendChild(this.$fragment)
    }
}

初始化的过程也是很容易理解,分三步,先将全部的元素转移到 fragment 容器中,而后在 fragment 容器中进行初始化,最后将这个 fragment 容器塞回原处。其实 fragment 容器并不进入页面,这里塞回去的仅仅是那些给初始化的节点而已。上面用到的三个定义在原型上的函数,isElementNode 用于判断是不是元素节点;node2Fragment 用于将节点中的全部子节点转移到 fragment 容器中,init 是初始化的核心函数,用于初始化模板数据:数组

Compile.prototype = {
    // 将节点中的全部子节点转移到fragment容器中
    node2Fragment:function(node){
        // 建立一个fragment对象
        let fragment = document.createDocumentFragment()
        // 循环将元素节点中的全部子节点塞入fragment容器中,最终返回塞满子节点的fragment对象
        let child
        while(child = node.firstChild){
            fragment.appendChild(child)
        }
        return fragment
    },
    // 判断是不是元素节点
    isElementNode:function (node) {
        return node.nodeType === 1
    }
}

(三)初始化,详解 init 方法app

Compile.prototype = {
    init:function(){
        // 编译函数
        this.compileElement(this.$fragment)
    },
    compileElement:function(el){
        // 获取全部子节点
        const childNodes = el.childNodes
        // 保存compile对象
        const me = this
        // 将类数组转化为真数组,遍历全部子节点
        Array.prototype.slice.call(childNodes).forEach(function (node) {
            // 获得节点的文本内容
            const text = node.textContent
            // 定义正则表达式,用于匹配大括号表达式
            const reg = /\{\{(.*?)\}\}/
            // 元素节点
            if(me.isElementNode(node)){
                // 编译元素节点的指令属性
                me.compile(node)
            }else if(me.isTextNode(node) && reg.test(text)){ // 若是是一个大括号表达式的文本节点
                me.compileText(node,RegExp.$1)
            }
            // 若是子节点还有子节点,递归调用
            if(node.childNodes && node.childNodes.length){
                me.compileElement(node)
            }
        })
    },
}

首先,init 方法去调用了compileElement 方法,该方法的主要做用就是处理以前准备好的 fragment 容器,将容器中全部子节点取出,而后进行分类处理,若是是一个元素节点,就去编译元素节点中的指令,若是是一个大括号表达式的文本节点,就去编译大括号表达式;若是节点里面还有子节点,则递归调用。顺着这个思路,先来研究比较简单的大括号表达式的状况(就是compileText这个方法):框架

Compile.prototype = {
    // 编译大括号表达式,参数node表明节点,exp表明表达式(就是正则匹配到的那个东西)
    compileText:function(node,exp){
        compileUtil.text(node, this.$vm, exp)
    }
}

const compileUtil = {
    // 解释 v-text 和 双大括号表达式,由此也能够看出其实双大括号表达式跟v-text指令的实现原理是一致的!
    text:function (node, vm, exp) {
        this.bind(node,vm,exp,'text')
    },
    // 真正用于解释指令的函数
    bind:function (node, vm, exp, dir) {
        // 获取更新函数
        const updaterFn = updater[dir + 'Updater']
        updaterFn && updaterFn(node,this._getVMVal(vm,exp))
    },
    // 获得表达式对应的value
    _getVMVal:function (vm, exp) {
        let val = vm._data
        exp = exp.split('.')
        exp.forEach(function (key) {
            val = val[key]
        })
        return val
    }
}
// 更新器
const updater = {
    // 更新节点的textContent
    textUpdater:function (node, value) {
        node.textContent = typeof value === 'undefined' ? '' : value
    }
}

从代码和注释上已经很好的说明了整个流程了,这里再简单的啰嗦一下吧,其实咱们用的双大括号表达式也是一种指令,由于它跟v-text的处理是彻底一致的,都是在操做节点的textContent属性。可能会让人迷糊的是 _getVMVal函数吧,这个函数的做用就是处理多层次对象的,由于表达式不会仅仅是一层的,也多是两层或者多层次的,好比,data里面保存了一个person对象,里面还有name等其余属性,然而咱们极可能会在表达式里面写person.name这样相似的多层次的属性(说句题外话,vue 不会监听到对象内部属性的变化,若是是简单的经过对象.属性名的方式去改变对象,那么vue是不知道的~~),这个函数也正是用于处理这种结构的。由于双大括号跟其余指令都非常相似的思想,都是在操做 DOM 的某个属性,具体的过程就再也不细说了。函数

相关文章
相关标签/搜索