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 。

(二)通过 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 方法

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 的某个属性,具体的过程就不再细说了。

原文地址:https://www.cnblogs.com/jonas-von/p/9974107.html